0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

実践して学ぶAWSのIaC【CloudFormation編】

Last updated at Posted at 2025-07-13

はじめに

AWSでインフラをコードとして管理するには、CloudFormation、CDK、Terraformなど複数のツールがあります。これらのツールがどんな特徴を持っていて、どういう場面で使うのが最適なのかを比較しながら学んでいきたいと思います!

今回は以下のシステムを題材に、CloudFormationで構築してみました。シンプルなシステムではありますが、循環依存の問題など躓いたポイントがありましたので共有したいと思います!

構築したシステム

image.png

簡単に説明すると、画像のリサイズを行うサーバーレスシステムです。

処理フロー

  1. 画像をS3にアップロード
  2. S3イベントトリガーでLambda関数を実行
  3. Lambda関数が画像をリサイズ
  4. リサイズ済み画像を別のS3に保存
  5. 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バケットを参照

この記述により、以下の循環が発生していました。

  1. S3:Lambda Permissionに依存(S3イベント設定のため)
  2. Lambda Permission:Lambda関数に依存
  3. Lambda関数:Lambda実行ロールに依存
  4. 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で同じシステムを作ってみて、比較できたらと思っています。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?