Amazon Q Developer CLIに画像からCloudFormationテンプレートを生成させる

本記事は 夏休みクラウド自由研究2025 8/22付の記事です

皆さまこんにちは。お元気ですか?
最近、暑すぎて頭が働かなくなっちゃいますよね。

でも、頭が働かなくてもCloudFormationテンプレートを作成したいことって、往々にしてありますよね。
そういうわけで、Amazon Q Developer CLIを使って頭を使わずにスタックを作成してみようというのが、今回の自由研究のテーマです。

Amazon Q Developer CLIが画像入力をサポート

2025年5月のアップデートにより、Amazon Q Developer CLIに「画像サポート機能」が実装され、画像ファイルをターミナルから直接入力できるようになりました。JPEG、PNG、WEBP、GIFの形式をサポートしており、また、1回のリクエストで最大10枚の画像をアップロード可能です。この機能を活用することで、視覚的なアイデアを文書やコードといった成果物に落としこむことが可能になります。

以下にご紹介する公式ブログでは、アーキテクチャ図からのIaC作成、ER図からのDBスキーマ作成、手描き図からの設計書作成、スクリーンショットからUIのモックアップ作成という4つのユースケースが紹介されています。

 

実際にやってみた

そこで今回は、手描きのアーキテクチャ図からYAML形式のCloudFormationテンプレートを出力させ、想定した振る舞いをするかを確かめてみます。なお、以降は読みやすさのため(&親しみを込めて)Amazon Q Developer CLIのことはQdevくんと呼ぶこととします。

準備:手書きのアーキテクチャ図

まずはQdevくんに読み込ませるサービス構成の絵を描きます。今回はこんなのを書いてみました(日本語を正しく読み込むか分からなかったので保険で英語にしました)。処理の流れはざっくり以下の通りです。

  • S3にファイルをアップロード→イベント通知がEventBridgeへ渡される
  • ファイルがCSV形式ならSNSを介してメールで通知。また、常にLambdaで処理してメタデータ情報をDynamoDBに格納する。

(あくまで構成図通りに一連のサービスを組み立てられるかのテストを目的としているため、上の構成が有用/望ましい構成であるかは一旦置いておきます。)

Amazon Q Devにテンプレートを生成させる

準備ができたら「test-diagram.jpeg」として保存し、後はQdevくんにお任せしましょう。

🤖 You are chatting with claude-4-sonnet
> test-diagram.jpegを実現するためのCloudFormationテンプレートを作成して。図に載っていない詳細についてはAWSの
ベストプラクティスに従うこととします。それでもなお確認が必要であれば、ユーザに質問してください

> まず、test-diagram.jpegファイルを確認させていただきます。

🛠️  Using tool: fs_read (trusted)
 ⋮ 
 ● Reading images: ■■■■/qdev/test-diagram.jpeg

 ✓ Successfully read image

 ⋮ 
 ● Completed in 0.0s

> 図を確認しました。この「Uploaded data process flow」を実現するCloudFormationテンプレートを作成します。

フローは以下のようになっています:
1. ユーザーがS3にCSVファイルをアップロード
2. S3イベントがEventBridgeをトリガー
3. EventBridgeがSNSに通知
4. SNSからEmailで通知
5. EventBridgeが常にLambda関数もトリガー
6. Lambda関数がメタデータをDynamoDBに保存

CloudFormationテンプレートを作成します:
(略)
 ⋮ 
 ↳ Purpose: データ処理フローのCloudFormationテンプレートを作成

Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]:

> y

Creating: ■■■■/qdev/data-process-flow.yaml

 ⋮ 
 ● Completed in 0.2s


> CloudFormationテンプレートを作成しました。このテンプレートは図に示されたデータ処理フローを実現します。

## 主な機能

1. S3バケット: CSVファイルのアップロード先
2. EventBridge: S3イベントをルーティング
3. SNS: CSVファイルアップロード時のEmail通知
4. Lambda関数: 全ファイルのメタデータをDynamoDBに保存
5. DynamoDB: ファイルメタデータの永続化

## AWSベストプラクティスの適用

• S3バケットの暗号化とバージョニング有効化
• パブリックアクセスブロック設定
• IAMロールの最小権限原則
• DynamoDBのポイントインタイム復旧有効化
• Lambda関数のタイムアウト設定
• リソース名の一意性確保

## デプロイ方法

bash
aws cloudformation create-stack \
  --stack-name data-process-flow \
  --template-body file://data-process-flow.yaml \
  --parameters ParameterKey=NotificationEmail,ParameterValue=your-email@example.com \
  --capabilities CAPABILITY_IAM


テンプレートをデプロイする前に、NotificationEmailパラメータに実際のメールアドレスを指定してください。
デプロイ後、SNSサブスクリプションの確認メールが送信されます。

何か追加の設定や修正が必要でしたらお知らせください。
作成されたdata-process-flow.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Uploaded data process flow infrastructure'

Parameters:
  NotificationEmail:
    Type: String
    Description: Email address for notifications
    AllowedPattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
    ConstraintDescription: Must be a valid email address

Resources:
  # S3 Bucket for data uploads
  DataUploadBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-data-upload-${AWS::AccountId}'
      NotificationConfiguration:
        EventBridgeConfiguration:
          EventBridgeEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      VersioningConfiguration:
        Status: Enabled

  # EventBridge Custom Bus
  DataProcessEventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: !Sub '${AWS::StackName}-data-process-bus'

  # SNS Topic for notifications
  DataProcessNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${AWS::StackName}-data-process-notifications'
      DisplayName: Data Process Notifications

  # SNS Subscription for email notifications
  EmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      TopicArn: !Ref DataProcessNotificationTopic
      Endpoint: !Ref NotificationEmail

  # DynamoDB Table for metadata storage
  MetadataTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub '${AWS::StackName}-file-metadata'
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: fileKey
          AttributeType: S
        - AttributeName: uploadTimestamp
          AttributeType: S
      KeySchema:
        - AttributeName: fileKey
          KeyType: HASH
        - AttributeName: uploadTimestamp
          KeyType: RANGE
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true

  # IAM Role for Lambda function
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: DynamoDBAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                Resource: !GetAtt MetadataTable.Arn
        - PolicyName: S3Access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectMetadata
                Resource: !Sub '${DataUploadBucket}/*'

  # Lambda function for processing metadata
  MetadataProcessorFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-metadata-processor'
      Runtime: python3.11
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 60
      Environment:
        Variables:
          METADATA_TABLE: !Ref MetadataTable
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          from datetime import datetime
          import urllib.parse

          dynamodb = boto3.resource('dynamodb')
          s3 = boto3.client('s3')
          table = dynamodb.Table(os.environ['METADATA_TABLE'])

          def lambda_handler(event, context):
              print(f"Received event: {json.dumps(event)}")
              
              try:
                  # Parse the EventBridge event
                  detail = event['detail']
                  bucket_name = detail['bucket']['name']
                  object_key = urllib.parse.unquote_plus(detail['object']['key'])
                  
                  # Get object metadata from S3
                  response = s3.head_object(Bucket=bucket_name, Key=object_key)
                  
                  # Prepare metadata for DynamoDB
                  metadata = {
                      'fileKey': object_key,
                      'uploadTimestamp': datetime.utcnow().isoformat(),
                      'bucketName': bucket_name,
                      'fileSize': response['ContentLength'],
                      'contentType': response.get('ContentType', 'unknown'),
                      'lastModified': response['LastModified'].isoformat(),
                      'etag': response['ETag'].strip('"'),
                      'processedAt': datetime.utcnow().isoformat()
                  }
                  
                  # Store metadata in DynamoDB
                  table.put_item(Item=metadata)
                  
                  print(f"Successfully stored metadata for {object_key}")
                  
                  return {
                      'statusCode': 200,
                      'body': json.dumps({
                          'message': 'Metadata processed successfully',
                          'fileKey': object_key
                      })
                  }
                  
              except Exception as e:
                  print(f"Error processing metadata: {str(e)}")
                  raise e

  # EventBridge Rule for S3 CSV uploads to SNS
  S3ToSNSRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-csv-to-sns'
      EventBusName: !Ref DataProcessEventBus
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
          object:
            key:
              - suffix: .csv
      State: ENABLED
      Targets:
        - Arn: !Ref DataProcessNotificationTopic
          Id: SNSTarget
          RoleArn: !GetAtt EventBridgeToSNSRole.Arn

  # EventBridge Rule for S3 uploads to Lambda (always)
  S3ToLambdaRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-to-lambda'
      EventBusName: !Ref DataProcessEventBus
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
      State: ENABLED
      Targets:
        - Arn: !GetAtt MetadataProcessorFunction.Arn
          Id: LambdaTarget

  # IAM Role for EventBridge to SNS
  EventBridgeToSNSRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SNSPublishPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref DataProcessNotificationTopic

  # Lambda permission for EventBridge
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref MetadataProcessorFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt S3ToLambdaRule.Arn

  # EventBridge Rule to route S3 events to custom bus
  S3EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-event-router'
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
      State: ENABLED
      Targets:
        - Arn: !GetAtt DataProcessEventBus.Arn
          Id: CustomBusTarget
          RoleArn: !GetAtt EventBridgeRouterRole.Arn

  # IAM Role for EventBridge routing
  EventBridgeRouterRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: EventBridgePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - events:PutEvents
                Resource: !GetAtt DataProcessEventBus.Arn

Outputs:
  S3BucketName:
    Description: Name of the S3 bucket for data uploads
    Value: !Ref DataUploadBucket
    Export:
      Name: !Sub '${AWS::StackName}-S3Bucket'

  SNSTopicArn:
    Description: ARN of the SNS topic for notifications
    Value: !Ref DataProcessNotificationTopic
    Export:
      Name: !Sub '${AWS::StackName}-SNSTopic'

  DynamoDBTableName:
    Description: Name of the DynamoDB table for metadata
    Value: !Ref MetadataTable
    Export:
      Name: !Sub '${AWS::StackName}-MetadataTable'

  LambdaFunctionName:
    Description: Name of the Lambda function for metadata processing
    Value: !Ref MetadataProcessorFunction
    Export:
      Name: !Sub '${AWS::StackName}-LambdaFunction'

  EventBusName:
    Description: Name of the custom EventBridge bus
    Value: !Ref DataProcessEventBus
    Export:
      Name: !Sub '${AWS::StackName}-EventBus'

ほとんど追加の説明を渡さずに依頼を投げたので、テンプレートを作成する前に何回か確認の往復があることを予想していたのですが、意外とあっさり作成してくれました。

- PolicyName: S3Access
  PolicyDocument:
    Version: '2012-10-17'
    Statement:
      - Effect: Allow
        Action:
          - s3:GetObject
          - s3:GetObjectMetadata
        Resource: !Sub '${DataUploadBucket.Arn}/*'

(よく見るとLambdaの実行ロールの中でs3:GetObjectMetadataという存在しないIAMアクションを許可してしまっているようですが、その辺はご愛嬌としておきましょう。)

生成されたテンプレートからスタックを起動してみる

テンプレートができたので次はこれをCloudFormationにアップロードします。
通知先のメールアドレス部分がパラメータになっているので、自分のメールアドレスを入力します。あとは「AWS CloudFormation によって IAM リソースが作成される場合があることを承認します。」にチェックを入れること以外はデフォルトの設定のまま作成に進み、しばらく待ちます。

……スタックの作成を待っていたら、途中でロールバックしてしまいました。見ると、次のようなエラーが出ています。

リソース LambdaExecutionRole は CREATE_FAILED 状態です。
Resource handler returned message: “Resource q-image-stack-20250822-data-upload-111111111111/* must be in ARN format or “*”.(Service: Iam, Status Code: 400, Request ID: ****-****-****-****-************) (SDK Attempt Count: 1) (RequestToken: ****-****-****-****-************, HandlerErrorCode: InvalidRequest)
IAMリソースのARN指定が原因のようですが、今回の自由研究はなるべく頭を使わないことが目標なので、もちろんこんなエラーは読みません。全部Qdevくんに丸投げしましょう。
🤖 You are chatting with claude-4-sonnet
> 作ってもらったdata-process-flow.yamlを展開したら次のようなエラーが出てCREATE_FAILEDになりました。
Resource handler returned message: "Resource q-image-stack-20250822-data-upload-111111111111/* 
must be in ARN format or "*".(Service: Iam, Status Code: 400, Request ID: ****-****-****-****-************) 
(SDK Attempt Count: 1) (RequestToken: ****-****-****-****-************, HandlerErrorCode: InvalidRequest)
適切にテンプレートファイルを修正してくれる?

> エラーメッセージを見ると、IAMポリシーでリソースARNの形式が正しくないことが原因です。S3バケットのリソース指定で /* を使う場合は、
完全なARN形式にする必要があります。

まず現在のテンプレートファイルを確認して、適切に修正しましょう。


> 問題を特定しました。LambdaExecutionRoleのS3Accessポリシーで、リソースARNの形式が正しくありません。!Sub '${DataUploadBucket}/*' 
は正しいARN形式ではありません。S3バケットのARNは arn:aws:s3:::bucket-name/* の形式である必要があります。

テンプレートを修正します:
 ⋮ 
 ● Completed in 0.0s


> 修正が完了しました。主な変更点は以下の通りです:

1. S3リソースARNの修正: !Sub '${DataUploadBucket}/*' を !Sub '${DataUploadBucket.Arn}/*' に変更しました。
これにより、正しいS3バケットのARN形式(arn:aws:s3:::bucket-name/*)が生成されます。

2. ListBucket権限の追加: S3バケット自体への s3:ListBucket 権限も追加しました。これはLambda関数がバケット内の
オブジェクトにアクセスする際に必要になる場合があります。

これで、IAMポリシーのリソースARNが正しい形式になり、CloudFormationスタックのデプロイが成功するはずです。
修正されたテンプレートを使って再度デプロイしてみてください。
修正されたdata-process-flow.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Uploaded data process flow infrastructure'

Parameters:
  NotificationEmail:
    Type: String
    Description: Email address for notifications
    AllowedPattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
    ConstraintDescription: Must be a valid email address

Resources:
  # S3 Bucket for data uploads
  DataUploadBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-data-upload-${AWS::AccountId}'
      NotificationConfiguration:
        EventBridgeConfiguration:
          EventBridgeEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      VersioningConfiguration:
        Status: Enabled

  # EventBridge Custom Bus
  DataProcessEventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: !Sub '${AWS::StackName}-data-process-bus'

  # SNS Topic for notifications
  DataProcessNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${AWS::StackName}-data-process-notifications'
      DisplayName: Data Process Notifications

  # SNS Subscription for email notifications
  EmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      TopicArn: !Ref DataProcessNotificationTopic
      Endpoint: !Ref NotificationEmail

  # DynamoDB Table for metadata storage
  MetadataTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub '${AWS::StackName}-file-metadata'
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: fileKey
          AttributeType: S
        - AttributeName: uploadTimestamp
          AttributeType: S
      KeySchema:
        - AttributeName: fileKey
          KeyType: HASH
        - AttributeName: uploadTimestamp
          KeyType: RANGE
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true

  # IAM Role for Lambda function
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: DynamoDBAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                Resource: !GetAtt MetadataTable.Arn
        - PolicyName: S3Access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectMetadata
                Resource: !Sub '${DataUploadBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource: !GetAtt DataUploadBucket.Arn

  # Lambda function for processing metadata
  MetadataProcessorFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-metadata-processor'
      Runtime: python3.11
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 60
      Environment:
        Variables:
          METADATA_TABLE: !Ref MetadataTable
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          from datetime import datetime
          import urllib.parse

          dynamodb = boto3.resource('dynamodb')
          s3 = boto3.client('s3')
          table = dynamodb.Table(os.environ['METADATA_TABLE'])

          def lambda_handler(event, context):
              print(f"Received event: {json.dumps(event)}")
              
              try:
                  # Parse the EventBridge event
                  detail = event['detail']
                  bucket_name = detail['bucket']['name']
                  object_key = urllib.parse.unquote_plus(detail['object']['key'])
                  
                  # Get object metadata from S3
                  response = s3.head_object(Bucket=bucket_name, Key=object_key)
                  
                  # Prepare metadata for DynamoDB
                  metadata = {
                      'fileKey': object_key,
                      'uploadTimestamp': datetime.utcnow().isoformat(),
                      'bucketName': bucket_name,
                      'fileSize': response['ContentLength'],
                      'contentType': response.get('ContentType', 'unknown'),
                      'lastModified': response['LastModified'].isoformat(),
                      'etag': response['ETag'].strip('"'),
                      'processedAt': datetime.utcnow().isoformat()
                  }
                  
                  # Store metadata in DynamoDB
                  table.put_item(Item=metadata)
                  
                  print(f"Successfully stored metadata for {object_key}")
                  
                  return {
                      'statusCode': 200,
                      'body': json.dumps({
                          'message': 'Metadata processed successfully',
                          'fileKey': object_key
                      })
                  }
                  
              except Exception as e:
                  print(f"Error processing metadata: {str(e)}")
                  raise e

  # EventBridge Rule for S3 CSV uploads to SNS
  S3ToSNSRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-csv-to-sns'
      EventBusName: !Ref DataProcessEventBus
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
          object:
            key:
              - suffix: .csv
      State: ENABLED
      Targets:
        - Arn: !Ref DataProcessNotificationTopic
          Id: SNSTarget
          RoleArn: !GetAtt EventBridgeToSNSRole.Arn

  # EventBridge Rule for S3 uploads to Lambda (always)
  S3ToLambdaRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-to-lambda'
      EventBusName: !Ref DataProcessEventBus
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
      State: ENABLED
      Targets:
        - Arn: !GetAtt MetadataProcessorFunction.Arn
          Id: LambdaTarget

  # IAM Role for EventBridge to SNS
  EventBridgeToSNSRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SNSPublishPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref DataProcessNotificationTopic

  # Lambda permission for EventBridge
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref MetadataProcessorFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt S3ToLambdaRule.Arn

  # EventBridge Rule to route S3 events to custom bus
  S3EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-event-router'
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
      State: ENABLED
      Targets:
        - Arn: !GetAtt DataProcessEventBus.Arn
          Id: CustomBusTarget
          RoleArn: !GetAtt EventBridgeRouterRole.Arn

  # IAM Role for EventBridge routing
  EventBridgeRouterRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: EventBridgePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - events:PutEvents
                Resource: !GetAtt DataProcessEventBus.Arn

Outputs:
  S3BucketName:
    Description: Name of the S3 bucket for data uploads
    Value: !Ref DataUploadBucket
    Export:
      Name: !Sub '${AWS::StackName}-S3Bucket'

  SNSTopicArn:
    Description: ARN of the SNS topic for notifications
    Value: !Ref DataProcessNotificationTopic
    Export:
      Name: !Sub '${AWS::StackName}-SNSTopic'

  DynamoDBTableName:
    Description: Name of the DynamoDB table for metadata
    Value: !Ref MetadataTable
    Export:
      Name: !Sub '${AWS::StackName}-MetadataTable'

  LambdaFunctionName:
    Description: Name of the Lambda function for metadata processing
    Value: !Ref MetadataProcessorFunction
    Export:
      Name: !Sub '${AWS::StackName}-LambdaFunction'

  EventBusName:
    Description: Name of the custom EventBridge bus
    Value: !Ref DataProcessEventBus
    Export:
      Name: !Sub '${AWS::StackName}-EventBus'

再度アップロードして今度は無事、デプロイまで完了することができました。SNSから登録したアドレスにメールが来るので、承認をしておきます。

テスト:データの作成もAmazon Qに任せよう

それでは、ちゃんと動くかテストしてみましょう。ダミーデータが必要になりますが、むろん、これもQdevくんに作ってもらいます。

> data-process-flow.yamlをテストしたい。testフォルダを作って、S3にアップロードするファイル(CSV, HTML, TXT)を作って

> data-process-flow.yamlファイルをテストするために、testフォルダを作成してテスト用のファイルを準備しましょう。

(略)

> CloudFormationテンプレートの内容を確認しました。このテンプレートはS3にファイルがアップロードされると、EventBridge経由
でLambda関数とSNS(CSVファイルの場合)に通知する仕組みです。

testフォルダを作成して、CSV、HTML、TXTファイルのサンプルを作成しましょう。
(略) 

> 完璧です!testフォルダに以下のテストファイルを作成しました:

こんな感じのファイルを6種類作ってくれました。

作成されたバケットにこれらのファイルをアップロードすると、程なくして以下のようなメールが2通届きました。確かに、CSVファイルのそれぞれについてアップロード通知を送ってきてくれています。

また、Dynamoを見ると画像のようにすべてのファイルについてメタデータを保存してくれていました。最初の図で期待していた処理がちゃんとなされていることが確認できます。

おまけ

せっかくなのでもう少し遊んでみましょう。

機能の追加

ただ、上のメール通知はJSON形式のメッセージがそのままメールの本文に書かれているだけで、正直可読性は低いです。さらにおねだりをしてフォーマットされたメールが飛ぶようにしてもらいましょう。

> data-process-flow.yamlを更新して、メール送信される通知の本文の可読性を上げてくれる?

> data-process-flow.yamlを更新して、SNS通知の本文をより読みやすくするために、Lambda関数を追加してメール通知をフォーマットしましょう。
(略)
> 完璧です!data-process-flow.yamlを更新して、メール通知の可読性を大幅に向上させました。
機能を追加したdata-process-flow.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Uploaded data process flow infrastructure'

Parameters:
  NotificationEmail:
    Type: String
    Description: Email address for notifications
    AllowedPattern: ^[^\s@]+@[^\s@]+\.[^\s@]+$
    ConstraintDescription: Must be a valid email address

Resources:
  # S3 Bucket for data uploads
  DataUploadBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub '${AWS::StackName}-data-upload-${AWS::AccountId}'
      NotificationConfiguration:
        EventBridgeConfiguration:
          EventBridgeEnabled: true
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      BucketEncryption:
        ServerSideEncryptionConfiguration:
          - ServerSideEncryptionByDefault:
              SSEAlgorithm: AES256
      VersioningConfiguration:
        Status: Enabled

  # EventBridge Custom Bus
  DataProcessEventBus:
    Type: AWS::Events::EventBus
    Properties:
      Name: !Sub '${AWS::StackName}-data-process-bus'

  # SNS Topic for notifications
  DataProcessNotificationTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: !Sub '${AWS::StackName}-data-process-notifications'
      DisplayName: Data Process Notifications

  # SNS Subscription for email notifications
  EmailSubscription:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      TopicArn: !Ref DataProcessNotificationTopic
      Endpoint: !Ref NotificationEmail

  # DynamoDB Table for metadata storage
  MetadataTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: !Sub '${AWS::StackName}-file-metadata'
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: fileKey
          AttributeType: S
        - AttributeName: uploadTimestamp
          AttributeType: S
      KeySchema:
        - AttributeName: fileKey
          KeyType: HASH
        - AttributeName: uploadTimestamp
          KeyType: RANGE
      StreamSpecification:
        StreamViewType: NEW_AND_OLD_IMAGES
      PointInTimeRecoverySpecification:
        PointInTimeRecoveryEnabled: true

  # IAM Role for Lambda function
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
      Policies:
        - PolicyName: DynamoDBAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:PutItem
                  - dynamodb:GetItem
                  - dynamodb:UpdateItem
                Resource: !GetAtt MetadataTable.Arn
        - PolicyName: S3Access
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:GetObjectMetadata
                Resource: !Sub '${DataUploadBucket.Arn}/*'
              - Effect: Allow
                Action:
                  - s3:ListBucket
                Resource: !GetAtt DataUploadBucket.Arn
        - PolicyName: SNSAccess
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref DataProcessNotificationTopic

  # Lambda function for formatting notifications
  NotificationFormatterFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-notification-formatter'
      Runtime: python3.11
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 60
      Environment:
        Variables:
          SNS_TOPIC_ARN: !Ref DataProcessNotificationTopic
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          from datetime import datetime
          import urllib.parse

          sns = boto3.client('sns')
          s3 = boto3.client('s3')

          def lambda_handler(event, context):
              print("Received event: {}".format(json.dumps(event)))
              
              try:
                  # Parse the EventBridge event
                  detail = event['detail']
                  bucket_name = detail['bucket']['name']
                  object_key = urllib.parse.unquote_plus(detail['object']['key'])
                  
                  # Get object metadata from S3
                  response = s3.head_object(Bucket=bucket_name, Key=object_key)
                  
                  # Format file size
                  file_size = response['ContentLength']
                  if file_size < 1024:
                      size_str = "{} bytes".format(file_size)
                  elif file_size < 1024 * 1024:
                      size_str = "{:.1f} KB".format(file_size / 1024)
                  else:
                      size_str = "{:.1f} MB".format(file_size / (1024 * 1024))
                  
                  # Format timestamp
                  upload_time = response['LastModified'].strftime('%Y-%m-%d %H:%M:%S (UTC)')
                  
                  # Create formatted message
                  subject = "CSV File Uploaded - {}".format(object_key)
                  
                  message_parts = [
                      "CSV File Upload Notification",
                      "",
                      "File Information",
                      "================",
                      "File Name - {}".format(object_key),
                      "Bucket - {}".format(bucket_name),
                      "File Size - {}".format(size_str),
                      "Content Type - {}".format(response.get('ContentType', 'unknown')),
                      "Upload Time - {}".format(upload_time),
                      "",
                      "Processing Status",
                      "=================",
                      "File upload completed",
                      "Metadata processing in progress", 
                      "Data analysis preparation",
                      "",
                      "Next Steps",
                      "==========",
                      "1. File content validation",
                      "2. Data quality check",
                      "3. Processing result notification",
                      "",
                      "This is an automated message from the Data Processing System v2.1"
                  ]
                  message = "\n".join(message_parts)
                  
                  # Send formatted notification
                  sns.publish(
                      TopicArn=os.environ['SNS_TOPIC_ARN'],
                      Subject=subject,
                      Message=message
                  )
                  
                  print("Successfully sent formatted notification for {}".format(object_key))
                  
                  return {
                      'statusCode': 200,
                      'body': json.dumps({
                          'message': 'Notification sent successfully',
                          'fileKey': object_key
                      })
                  }
                  
              except Exception as e:
                  print("Error sending notification: {}".format(str(e)))
                  raise e

  MetadataProcessorFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub '${AWS::StackName}-metadata-processor'
      Runtime: python3.11
      Handler: index.lambda_handler
      Role: !GetAtt LambdaExecutionRole.Arn
      Timeout: 60
      Environment:
        Variables:
          METADATA_TABLE: !Ref MetadataTable
      Code:
        ZipFile: |
          import json
          import boto3
          import os
          from datetime import datetime
          import urllib.parse

          dynamodb = boto3.resource('dynamodb')
          s3 = boto3.client('s3')
          table = dynamodb.Table(os.environ['METADATA_TABLE'])

          def lambda_handler(event, context):
              print("Received event: {}".format(json.dumps(event)))
              
              try:
                  # Parse the EventBridge event
                  detail = event['detail']
                  bucket_name = detail['bucket']['name']
                  object_key = urllib.parse.unquote_plus(detail['object']['key'])
                  
                  # Get object metadata from S3
                  response = s3.head_object(Bucket=bucket_name, Key=object_key)
                  
                  # Prepare metadata for DynamoDB
                  metadata = {
                      'fileKey': object_key,
                      'uploadTimestamp': datetime.utcnow().isoformat(),
                      'bucketName': bucket_name,
                      'fileSize': response['ContentLength'],
                      'contentType': response.get('ContentType', 'unknown'),
                      'lastModified': response['LastModified'].isoformat(),
                      'etag': response['ETag'].strip('"'),
                      'processedAt': datetime.utcnow().isoformat()
                  }
                  
                  # Store metadata in DynamoDB
                  table.put_item(Item=metadata)
                  
                  print("Successfully stored metadata for {}".format(object_key))
                  
                  return {
                      'statusCode': 200,
                      'body': json.dumps({
                          'message': 'Metadata processed successfully',
                          'fileKey': object_key
                      })
                  }
                  
              except Exception as e:
                  print("Error processing metadata: {}".format(str(e)))
                  raise e

  # EventBridge Rule for S3 CSV uploads to notification formatter
  S3ToNotificationRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-csv-to-notification'
      EventBusName: !Ref DataProcessEventBus
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
          object:
            key:
              - suffix: .csv
      State: ENABLED
      Targets:
        - Arn: !GetAtt NotificationFormatterFunction.Arn
          Id: NotificationFormatterTarget

  # EventBridge Rule for S3 uploads to Lambda (always)
  S3ToLambdaRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-to-lambda'
      EventBusName: !Ref DataProcessEventBus
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
      State: ENABLED
      Targets:
        - Arn: !GetAtt MetadataProcessorFunction.Arn
          Id: LambdaTarget

  # IAM Role for EventBridge to SNS
  EventBridgeToSNSRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: SNSPublishPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - sns:Publish
                Resource: !Ref DataProcessNotificationTopic

  # Lambda permission for EventBridge (Notification Formatter)
  NotificationFormatterInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref NotificationFormatterFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt S3ToNotificationRule.Arn

  # Lambda permission for EventBridge (Metadata Processor)
  LambdaInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref MetadataProcessorFunction
      Action: lambda:InvokeFunction
      Principal: events.amazonaws.com
      SourceArn: !GetAtt S3ToLambdaRule.Arn

  # EventBridge Rule to route S3 events to custom bus
  S3EventRule:
    Type: AWS::Events::Rule
    Properties:
      Name: !Sub '${AWS::StackName}-s3-event-router'
      EventPattern:
        source:
          - aws.s3
        detail-type:
          - Object Created
        detail:
          bucket:
            name:
              - !Ref DataUploadBucket
      State: ENABLED
      Targets:
        - Arn: !GetAtt DataProcessEventBus.Arn
          Id: CustomBusTarget
          RoleArn: !GetAtt EventBridgeRouterRole.Arn

  # IAM Role for EventBridge routing
  EventBridgeRouterRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: events.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: EventBridgePolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - events:PutEvents
                Resource: !GetAtt DataProcessEventBus.Arn

Outputs:
  S3BucketName:
    Description: Name of the S3 bucket for data uploads
    Value: !Ref DataUploadBucket
    Export:
      Name: !Sub '${AWS::StackName}-S3Bucket'

  SNSTopicArn:
    Description: ARN of the SNS topic for notifications
    Value: !Ref DataProcessNotificationTopic
    Export:
      Name: !Sub '${AWS::StackName}-SNSTopic'

  DynamoDBTableName:
    Description: Name of the DynamoDB table for metadata
    Value: !Ref MetadataTable
    Export:
      Name: !Sub '${AWS::StackName}-MetadataTable'

  LambdaFunctionName:
    Description: Name of the Lambda function for metadata processing
    Value: !Ref MetadataProcessorFunction
    Export:
      Name: !Sub '${AWS::StackName}-LambdaFunction'

  NotificationFormatterFunctionName:
    Description: Name of the Lambda function for notification formatting
    Value: !Ref NotificationFormatterFunction
    Export:
      Name: !Sub '${AWS::StackName}-NotificationFormatterFunction'

  EventBusName:
    Description: Name of the custom EventBridge bus
    Value: !Ref DataProcessEventBus
    Export:
      Name: !Sub '${AWS::StackName}-EventBus'

フォーマット用のLambda関数を追加してくれました。
更新されたテンプレートでスタックを更新し、再度CSVファイルをアップロードすると今度は以下のように読みやすい形で通知が送られてきました。

アーキテクチャ図を清書してもらう

機能の追加によってLambdaが追加されたので、作成された添付ファイルをもとに改めて構成図を作成させてみます。
実際の指示は履歴が消えてしまったのですが、プロンプトを作成するにあたっては下記記事を参考に最新版のアイコンを参照するなどを指示に含めました。​

作成された構成図は以下の通りです。かなり自分のイメージ通りに出力してくれた印象です。

おわりに

いかがでしたか?そこそこ複雑なテンプレートでもちょっとした対話で簡単に作成することができ、今日の生成AIの威力を身にしみて感じる自由研究となりました。
ChatGPTが出始めの頃、コード生成をさせるにも途中で出力が止まり、「続きを書いて」を繰り返して指示して出力されたコードを何度も修正してやっと動くようになる、というのが遠い昔に感じます。

これで夏休みの宿題は終わりです。皆様くれぐれも暑さには気を付けてお過ごしください。

タイトルとURLをコピーしました