仕事でAWSのCloudFormationを使わなければいけないことがたまにあるものの、正直あんまり慣れていないところでよく分かんない。
とは言え、分からないばっかりではしょうがないので多少は使いこなせるようにはしておきたい。
そのためには、色々簡単なシステムを自分で手を動かしながら構築して数をこなしていくのが一番なんじゃないかということで、複数のAWSのサービスを絡めた単純な機構を思い付き次第作っていくことにする。
今回はその第一弾。
作成するもの
機能の概要
S3バケット間で画像ファイルを自動コピーするシステムを作る。
- バケットA内の「images」フォルダに画像をアップロード
- ファイルがアップロードされたことを検知してファイルコピー処理Lambdaを起動
- コピー処理LambdaはバケットB内の「images」フォルダに同じ画像を登録する
- バケットA内の「images」直下以外にアップロードされたものはコピー対象外である
作成するリソース
サービス | 論理ID | 内容 |
---|
S3::Bucket | BucketA | 画像アップロード元のバケット |
S3::Bucket | BucketB | 画像コピー先のバケット |
Lambda::Function | CopyLambda | 画像コピー処理を行うスクリプト |
IAM::Role | CopyLambdaRole | S3バケットやCloudWatchなどへアクセスする権限をLambdaに与える |
Events::Rule | EventBridgeRule | S3バケットへのファイルアップロードイベントを検知してLambdaを呼び出すルール |
Lambda::Permission | LambdaPermission | EventBridgeルールからLambdaを呼び出すことを許可する |
Lambda関数のソースはPythonで書いたコードをzipに圧縮し、S3上のlambda-sources-test1
というバケットに置いておく。
lambda-sources-test1
バケットはあらかじめ用意しておく想定なので今回の生成対象には含めない。
テンプレートはyaml形式かjson形式のいずれかで定義可能だが今回はyamlにする。
パラメータの定義
パラメータの定義は必須ではないが、!Ref
を使って同じテンプレート内から参照できるので複数箇所で同じ値を使いたい場合などに便利。
ここではLambdaのソースコードの格納フォルダと圧縮ファイル名、作成するS3バケット名(物理ID)を定義している。
Parameters:
LambdaSourceBucket:
Type: String
Default: lambda-sources-test1
Description: Lambda関数を格納するS3バケット名
LambdaSourceKey:
Type: String
Default: copy_image.zip
Description: Lambda関数のソースコードのS3キー(zipの配置場所)
SourceBucketName:
Type: String
Default: bucket-a-20250421
Description: アップロード元S3バケット名
DestinationBucketName:
Type: String
Default: bucket-b-20250421
Description: コピー先S3バケット名
S3関連の定義
S3バケットを2つ定義する。
重要なのはファイルアップロード元のBucketA
の方で、アップロードのイベントを検知するためにEventBridgeの有効化の設定を追加する必要がある。
NotificationConfiguration
(バケット通知設定)はいくつかプロパティを設定できるが、今回はEventBridgeConfiguration
を設定する。
EventBridgeConfiguration
はEventBridgeEnabled
というBooleanのプロパティがあり、これをtrueにしないと何も起きない。
BucketB
の方は存在しているだけでいいのでとりあえず名前だけつけておく。
BucketA:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref SourceBucketName
NotificationConfiguration:
EventBridgeConfiguration:
EventBridgeEnabled: true # EventBridgeを有効化
BucketB:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref DestinationBucketName
Lambda関連の定義
Lambdaでバケット内のオブジェクトを操作するので、そのための権限をLambda関数に与えるCopyLambdaRole
を定義する。
AssumeRolePolicyDocument
のところは、Lambdaサービスがsts:AssumeRole
を実行する(つまりロールを受け取る)ことを許可している。
Policies
には具体的な権限を個々に定義する。
Lambda関数はBacketA
のimages
配下に対してオブジェクトの取得を行うのでs3:GetObject
を許可し、BacketB
のimages
配下に対してオブジェクトの作成を行うのでs3:PutObject
を許可する。
この辺りが正しく設定されていないと権限エラーで所定の処理を実行できない。
また、ログ出力の権限も与えているのでLambda関数内でprint()
で出力している内容がCloudWatchで確認できるようになる。
CopyLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: copy-lambda-execution-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: lambda-s3-access-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource: !Sub arn:aws:s3:::${SourceBucketName}/images/*
- Effect: Allow
Action:
- s3:PutObject
Resource: !Sub arn:aws:s3:::${DestinationBucketName}/images/*
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
次はスクリプト本体のCopyLambda
の定義。
ソースコードを格納しているバケットの指定や権限の設定などを行う。
Role
の指定は必須であり、前述のCopyLambdaRole
のARNを指定することでこのLambda関数にバケット操作の権限が与えられる。(権限は装備しなきゃ意味がないぞ!)
テンプレートからLambda関数へパラメータを渡したい時はEnvironment
に定義すればよく、ここではDEST_BUCKET
というパラメータでコピー先バケットの物理IDを渡してLambda関数の中で使えるようにしている。
CopyLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: copy-image-lambda
Handler: copy_image.lambda_handler
Runtime: python3.10
Role: !GetAtt CopyLambdaRole.Arn
Timeout: 30
Code:
S3Bucket: !Ref LambdaSourceBucket
S3Key: !Ref LambdaSourceKey
Environment:
Variables:
DEST_BUCKET: !Ref DestinationBucketName
EventBridge関連の定義
BucketA
へのファイルアップロードイベントを検知してCopyLambda
に伝えるEventBridgeのルールを定義する。
ルールを設定する対象はS3なのでsource
はaws.s3
、アップロードイベント(ファイルオブジェクトの作成)を検知するのでdetail-type
はObject Created
、detail
にはイベント検知対象のバケット名やオブジェクトのキーを指定する。
Targets
はイベントの発生を伝える先のLambda関数のARNを指定する。
EventBridgeRule:
Type: AWS::Events::Rule
Properties:
Name: s3-image-upload-trigger-rule
EventPattern:
source:
- aws.s3
detail-type:
- Object Created
detail:
bucket:
name:
- !Ref SourceBucketName
object:
key:
- prefix: images/
Targets:
- Id: lambda-target
Arn: !GetAtt CopyLambda.Arn
EventBridgeルールからLambda関数を呼び出せるようにするための権限も必要。
Lambda関数を呼び出したいのでAction: lambda:InvokeFunction
を設定する。
FunctionName
は対象Lambda関数の名前またはARNであるが、関数を呼び出すリソース(つまり今回はEventBridgeルール)を指定するSourceArn
はARNのみなのでこの辺りの違いがややこしいと感じるところ。
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref CopyLambda
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt EventBridgeRule.Arn
テンプレート全体
s3_image_copy_template.yaml
というファイル名にしておく。
AWSTemplateFormatVersion: "2010-09-09"
Description: S3バケットAへの画像アップロードをトリガーに、Lambda関数でバケットBへ自動コピーする構成。
Parameters:
LambdaSourceBucket:
Type: String
Default: lambda-sources-test1
Description: Lambda関数を格納するS3バケット名
LambdaSourceKey:
Type: String
Default: copy_image.zip
Description: Lambda関数のソースコードのS3キー(zipの配置場所)
SourceBucketName:
Type: String
Default: bucket-a-20250421
Description: アップロード元S3バケット名
DestinationBucketName:
Type: String
Default: bucket-b-20250421
Description: コピー先S3バケット名
Resources:
BucketA:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref SourceBucketName
NotificationConfiguration:
EventBridgeConfiguration:
EventBridgeEnabled: true # EventBridgeを有効化
BucketB:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref DestinationBucketName
CopyLambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: copy-lambda-execution-role
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: lambda-s3-access-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- s3:GetObject
Resource: !Sub arn:aws:s3:::${SourceBucketName}/images/*
- Effect: Allow
Action:
- s3:PutObject
Resource: !Sub arn:aws:s3:::${DestinationBucketName}/images/*
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: arn:aws:logs:*:*:*
CopyLambda:
Type: AWS::Lambda::Function
Properties:
FunctionName: copy-image-lambda
Handler: copy_image.lambda_handler
Runtime: python3.10
Role: !GetAtt CopyLambdaRole.Arn
Timeout: 30
Code:
S3Bucket: !Ref LambdaSourceBucket
S3Key: !Ref LambdaSourceKey
Environment:
Variables:
DEST_BUCKET: !Ref DestinationBucketName
EventBridgeRule:
Type: AWS::Events::Rule
Properties:
Name: s3-image-upload-trigger-rule
EventPattern:
source:
- aws.s3
detail-type:
- Object Created
detail:
bucket:
name:
- !Ref SourceBucketName
object:
key:
- prefix: images/
Targets:
- Id: lambda-target
Arn: !GetAtt CopyLambda.Arn
LambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref CopyLambda
Action: lambda:InvokeFunction
Principal: events.amazonaws.com
SourceArn: !GetAtt EventBridgeRule.Arn
Lambdaスクリプトのソース
ファイルコピー処理のソースコードはPython3.10で記述している。
テンプレートのCopyLambda
のEnvironment
で定義したパラメータはos.environ.get('変数名')
のように受け取って使う。
lambda_handler()
の引数として渡ってくるevent
には、テンプレートのEventBridgeRule
のEventPattern
で定義した情報が含まれているので、そこからイベント発生元の情報などを参照することができる。
ファイルがコピー対象かどうかの判定はいい具合に書いておき、s3.copy_object()
でコピー先のバケットを指定すればOK。
このスクリプトを圧縮したcopy_image.zip
をS3のlambda-sources-test1
にあらかじめ置いておけばLambdaソースの準備は完了。
copy_image.py
import boto3
import os
import urllib.parse
s3 = boto3.client('s3')
destination_bucket = os.environ.get('DEST_BUCKET')
def lambda_handler(event, context):
try:
detail = event['detail']
source_bucket = detail['bucket']['name']
key = urllib.parse.unquote_plus(detail['object']['key'])
# 「images/」直下のファイルのみ許可(サブディレクトリは対象外)
if not key.startswith('images/') or '/' in key.removeprefix('images/'):
print(f'"{key}" is not located directly under the "images/". Skip copying.')
return
# jpeg, png, gif, webp 以外のファイルはスキップ
if not key.endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp')):
print(f'"{key}" is unsupported file type. Skip copying.')
return
s3.copy_object(
CopySource={'Bucket': source_bucket, 'Key': key},
Bucket=destination_bucket,
Key=key
)
print(f'Copied {key} from {source_bucket} to {destination_bucket}')
except Exception as e:
print(f'Error occurred: {e}')
raise
デプロイ用のスクリプト
作成したs3_image_copy_template.yaml
をデプロイしてスタックを作成する。
aws cloudformation deploy ~
のコマンドを直接打ってもいいが、毎回やるのは面倒なのでshell scriptなどで実行スクリプトを組んでおくと楽。
環境変数の定義やテンプレートの構文チェックなどもまとめて記述しておけるので便利。
deploy_cf.sh
#!/bin/bash
# 環境変数など
TEMPLATE_FILE='s3_image_copy_template.yaml'
REGION='ap-northeast-1'
# スタック名が指定されているか確認
if [ -z "$1" ]; then
echo 'specify stack name: $0 <STACK_NAME>'
exit 1
fi
STACK_NAME="$1"
# テンプレートのバリデーション
aws cloudformation validate-template \
--template-body file://$TEMPLATE_FILE
if [ $? -ne 0 ]; then
echo 'Template validation failed.'
exit 1
fi
# デプロイ実行
aws cloudformation deploy \
--template-file $TEMPLATE_FILE \
--stack-name $STACK_NAME \
--capabilities CAPABILITY_NAMED_IAM \
--region $REGION
if [ $? -eq 0 ]; then
echo 'Stack deployment successful!'
else
echo '[ERROR] Stack deployment failed.'
exit 1
fi
スタック名を付けてスクリプトを実行するとデプロイ処理が走る。
./deploy_cf.sh image-copy
デプロイ状況はAWSコンソールのCloudFormationの当該スタックのページで随時確認でき、エラーなどがあれば詳細が表示される。
エラーがあればCloudFormationのリファレンスと睨めっこしたりChatGPTなどに聞いたりしながら地道にテンプレートを修正していきましょう。
デプロイ完了して想定通りの動きになっていることを確認できたら完了🎉
参考
CloudFormationの公式リファレンス