はじめに
AWSでインフラをコードとして管理するには、CloudFormation、CDK、Terraformなど複数のツールがあります。これらのツールがどんな特徴を持っていて、どういう場面で使うのが最適なのかを比較しながら学んでいきたいと思います!
今回は以下のシステムを題材に、CloudFormationで構築してみました。シンプルなシステムではありますが、循環依存の問題など躓いたポイントがありましたので共有したいと思います!
構築したシステム
簡単に説明すると、画像のリサイズを行うサーバーレスシステムです。
処理フロー:
- 画像をS3にアップロード
- S3イベントトリガーでLambda関数を実行
- Lambda関数が画像をリサイズ
- リサイズ済み画像を別のS3に保存
- SNSでメールを送信
CloudFormationとSAMについて
CloudFormationはAWSリソースをYAMLファイルで宣言的に定義・管理できるサービスです。インフラをスタックという単位で管理し、一括作成/変更/削除をすることができます。
AWS SAMは、サーバーレスアプリケーション向けにCloudFormationを拡張したフレームワークです。LambdaやS3イベントなどの設定を簡潔に記述することができます。
今回はサーバーレス構成のため、SAMを使用して実装しました。
躓いた点:循環依存問題
テンプレートを一通り作成してデプロイしようとすると、以下のエラーが発生しました。
Error: Circular dependency between resources:
[SourceBucket, ImageProcessFunction, ImageProcessFunctionRole, ImageProcessFunctionS3EventPermission]
その時のテンプレートの記述内容の抜粋です。
Resources:
SourceBucket:
Type: AWS::S3::Bucket
# (S3イベントでLambda関数を呼び出す設定)
ImageProcessFunction:
Type: AWS::Serverless::Function
Properties:
Policies:
- S3ReadPolicy:
BucketName: !Ref SourceBucket # ← S3バケットを参照
Events:
S3Event:
Properties:
Bucket: !Ref SourceBucket # ← 同じS3バケットを参照
この記述により、以下の循環が発生していました。
- S3:Lambda Permissionに依存(S3イベント設定のため)
- Lambda Permission:Lambda関数に依存
- Lambda関数:Lambda実行ロールに依存
- Lambda実行ロール:S3に依存(ポリシーで
!Ref SourceBucket
を参照)
つまり、S3とLambda関数の相互参照により、CloudFormationがどちらを先に作成すべきか判断できない状態になっていました!
この問題を解決するために!Ref
による動的参照をやめて、S3のバケット名を一部ハードコードして対策しました。
Resources:
SourceBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'learning-imageproc-source-${Environment}'
ImageProcessFunction:
Type: AWS::Serverless::Function
Properties:
Policies:
- Statement:
- Effect: Allow
Action: s3:GetObject
# !Refを使わずに記述
Resource: !Sub 'arn:aws:s3:::learning-imageproc-source-${Environment}/*'
この方法で依存関係が一方向になり、循環依存が解消されました!
このような問題はよくあるようで、試してはいないですが他の対策方法もあります。
-
DependsOnを使った対処法:
S3とLambda関数を先に作成し、別途S3BucketNotificationリソースでイベント設定を後から追加する方法。DependsOnでLambda→S3Bucket→S3BucketNotificationの順序を明示的に制御し、循環依存を回避する。 -
スタック分離での解決法
S3を作成するスタックとLambda関数を作成するスタックを分離し、ImportValue/Exportで連携する方法。先にインフラスタック(S3)をデプロイし、後でアプリケーションスタック(Lambda+イベント設定)をデプロイする。
Lambda関数の実装
主要な部分だけ紹介。大半をClaudeに書いてもらっています。
def lambda_handler(event, context):
"""S3イベントトリガーによる画像リサイズ処理"""
try:
# 環境変数から設定値を取得
dest_bucket = os.environ.get('DESTINATION_BUCKET')
sns_topic_arn = os.environ.get('SNS_TOPIC_ARN')
# S3イベントからファイル情報を取得
source_bucket, object_key = extract_s3_info_from_event(event)
# 画像ファイルかどうかをチェック
if not is_supported_image_file(object_key):
return {'statusCode': 200, 'body': 'File skipped'}
# 画像処理を実行
result = process_image(source_bucket, object_key, dest_bucket)
return {'statusCode': 200, 'body': json.dumps(result)}
except Exception as e:
return {'statusCode': 500, 'body': json.dumps({'error': str(e)})}
def resize_image(image_data, size=300):
"""画像を300x300にリサイズ"""
image = Image.open(io.BytesIO(image_data))
# 透明度対応(RGBA → RGB変換)
if image.mode in ('RGBA', 'P'):
background = Image.new('RGB', image.size, (255, 255, 255))
# 適切な合成処理...
image = background
# アスペクト比を維持してリサイズ
image.thumbnail((size, size), Image.LANCZOS)
# JPEG形式で出力
output = io.BytesIO()
image.save(output, format='JPEG', quality=85, optimize=True)
return output.getvalue()
Dockerを使ったLambda関数のビルド
Lambda関数でPillowライブラリを使用するため、適切なバイナリを含むzipファイルを作成する必要があり、Dockerを使用してビルドしました。このzipファイルをS3にアップロードしてから、CloudFormationテンプレートでその場所を参照する形で実装しました!
作成したCloudFormationテンプレート
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: 'Image Resize Processing System'
Parameters:
Environment:
Type: String
Default: dev
AllowedValues: [dev, staging, prod]
Description: Environment name for resource naming
LambdaCodeBucket:
Type: String
Description: S3 bucket containing Lambda deployment package
Default: lambda-artifacts-bucket
LambdaCodeKey:
Type: String
Description: S3 key for Lambda deployment package
Default: lambda-code/image-resize-processor.zip
Resources:
# 元画像保存先S3
SourceImageBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'learning-imageproc-source-${Environment}'
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Environment
Value: !Ref Environment
- Key: Purpose
Value: ImageProcessing
- Key: BucketType
Value: Source
# リサイズ画像保存先S3
ResizedImageBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'learning-imageproc-resized-${Environment}'
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
Tags:
- Key: Environment
Value: !Ref Environment
- Key: Purpose
Value: ImageProcessing
- Key: BucketType
Value: Destination
# SNSトピック
ImageProcessingNotificationTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Sub 'image-processing-notifications-${Environment}'
DisplayName: !Sub 'Image Processing Notifications (${Environment})'
Tags:
- Key: Environment
Value: !Ref Environment
- Key: Purpose
Value: ImageProcessing
# メールのサブスクリプション
EmailNotificationSubscription:
Type: AWS::SNS::Subscription
Properties:
Protocol: email
TopicArn: !Ref ImageProcessingNotificationTopic
Endpoint: XXXXX@gmail.com #簡単のため、ハードコード
# Lambda関数
ImageResizeProcessorFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub 'image-resize-processor-${Environment}'
Runtime: python3.9
Handler: lambda_function.lambda_handler
CodeUri:
Bucket: !Ref LambdaCodeBucket
Key: !Ref LambdaCodeKey
Timeout: 60
MemorySize: 512
Environment:
Variables:
DESTINATION_BUCKET: !Sub 'learning-imageproc-resized-${Environment}'
SNS_TOPIC_ARN: !Ref ImageProcessingNotificationTopic
THUMBNAIL_SIZE: '300'
Policies:
- Statement:
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
Resource: !Sub 'arn:aws:s3:::learning-imageproc-source-${Environment}/*'
- Effect: Allow
Action:
- s3:PutObject
- s3:PutObjectAcl
Resource: !Sub 'arn:aws:s3:::learning-imageproc-resized-${Environment}/*'
- Effect: Allow
Action:
- sns:Publish
Resource: !Ref ImageProcessingNotificationTopic
- Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
Events:
S3ImageUpload:
Type: S3
Properties:
Bucket: !Ref SourceImageBucket
Events: 's3:ObjectCreated:*'
Tags:
Environment: !Ref Environment
Purpose: ImageProcessing
Outputs:
SourceBucketName:
Description: Name of the source S3 bucket for original images
Value: !Ref SourceImageBucket
Export:
Name: !Sub '${AWS::StackName}-SourceBucket'
ResizedBucketName:
Description: Name of the destination S3 bucket for resized images
Value: !Ref ResizedImageBucket
Export:
Name: !Sub '${AWS::StackName}-ResizedBucket'
LambdaFunctionName:
Description: Name of the image resize processor Lambda function
Value: !Ref ImageResizeProcessorFunction
Export:
Name: !Sub '${AWS::StackName}-LambdaFunction'
LambdaFunctionArn:
Description: ARN of the image resize processor Lambda function
Value: !GetAtt ImageResizeProcessorFunction.Arn
Export:
Name: !Sub '${AWS::StackName}-LambdaFunctionArn'
SNSTopicArn:
Description: ARN of the SNS notification topic
Value: !Ref ImageProcessingNotificationTopic
Export:
Name: !Sub '${AWS::StackName}-SNSTopic'
SourceBucketUploadCommand:
Description: AWS CLI command to upload test image
Value: !Sub 'aws s3 cp test-image.jpg s3://learning-imageproc-source-${Environment}/ --region ${AWS::Region}'
BucketNamingInfo:
Description: Bucket naming pattern used
Value: !Sub 'learning-imageproc-{purpose}-${Environment}'
デプロイと動作確認
Lambda関数のビルドとアップロード
- Dockerでzipファイルを作成
- Lambdaのzipファイル保存先S3にアップロード
CloudFormationでのデプロイ
sam deploy \
--template-file image-processing-template.yaml \
--stack-name image-processing-system \
--capabilities CAPABILITY_IAM \
--parameter-overrides \
Environment=dev \
LambdaCodeBucket=lambda-artifacts-XXXXX \
LambdaCodeKey=lambda-code/image-resize-processor.zip \
--region ap-northeast-1
~~~省略~~~
Successfully created/updated stack - image-processing-system in ap-northeast-1
最終的にSuccessfullyと出力されていればデプロイ完了です!
エンドエンドの試験
実際にテスト画像をアップロードして動作確認を行い、正常に動作することを確認しています。詳細は割愛。
まとめ
実際に試してみて色々と学びがありました!循環依存の問題もそうですし、デプロイしてみないことにはエラーの有無が分からないとい点には少し使いづらさを感じました。。。
次はCDKで同じシステムを作ってみて、比較できたらと思っています。