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、SAM、CDK、Terraformなどいくつかの選択肢があります。それぞれのサービスが持つ特徴と、どのような場面で使うのがベストなのかを実践を通じて学んでいきたいと思います!
今回は第1回目ということで、CloudFormationを使用します!後日、SAM、CDK、Terraformを使用して、それぞれのツールの違いについて比較したいと考えています!

構築するシステム

image.png

題材にするのはこちらのシステムです!
ユーザーがアップロードしたファイルを、拡張子に基づいて振り分け、適切なS3バケットに保存する処理を行うものです。

処理フロー

  1. API Gatewayを通じてファイルをアップロード
  2. Lambda関数がファイルを受け取り、拡張子を基にファイル種別を判定
  3. 適切なS3バケットにファイルを保存
  • 画像ファイル(jpg, png, gif)→ images-bucket
  • ドキュメント(pdf, docx, txt)→ documents-bucket
  • ログファイル(log, csv)→ logs-bucket

CloudFormationとは

AWSのリソースをYAMLやJSON形式のテンプレートで定義して一括で作成、変更、削除をするためのサービスです。テンプレートはセクションを分けて作成する必要があり、各セクション毎に決められた役割があります。

各セクションの役割

  • AWSTemplateFormatVersion:テンプレートのバージョンを指定
  • Description:テンプレートの概要や目的を記載
  • Parameters:外部から入力されるパラメータを定義
  • Conditions:条件に基づいてリソースの作成を制御
  • Resources:作成するAWSリソースを定義
  • Outputs:スタック作成後に表示する情報や他スタックから参照する値を定義
  • Metadata:テンプレートに関するメタデータや補足情報を記載(今回は使用せず)
  • Transform:特定のテンプレート変換を指定(今回は使用せず)

CloudFormationのテンプレート

今回作成したテンプレートはこちらです!

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'File Sorter System - Upload files and sort them by type into appropriate S3 buckets'

Parameters:
  Environment:
    Type: String
    Default: dev
    AllowedValues: [dev, staging, prod]
    Description: 'Deployment environment'

Conditions:
  IsProdOrStaging: !Or [!Equals [!Ref Environment, prod], !Equals [!Ref Environment, staging]]

Resources:
  # ===== S3 Buckets =====

  # 画像ファイル用のバケット
  ImagesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'file-sorter-images-${Environment}-${AWS::AccountId}'

  # ドキュメントファイル用のバケット
  DocumentsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'file-sorter-documents-${Environment}-${AWS::AccountId}'

  # ログファイル用のバケット
  LogsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'file-sorter-logs-${Environment}-${AWS::AccountId}'

  # ===== Lambda Function =====

  # Lambda実行用のIAMロール
  FileSorterLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub 'file-sorter-lambda-role-${Environment}'
      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: S3AccessPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:PutObject
                Resource:
                  - !Sub 'arn:aws:s3:::${ImagesBucket}/*'
                  - !Sub 'arn:aws:s3:::${DocumentsBucket}/*'
                  - !Sub 'arn:aws:s3:::${LogsBucket}/*'

  # Lambda関数
  FileSorterFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName: !Sub 'file-sorter-${Environment}'
      Runtime: python3.9
      Handler: index.lambda_handler
      Role: !GetAtt FileSorterLambdaRole.Arn
      Timeout: !If [IsProdOrStaging, 15, 10]
      MemorySize: !If [IsProdOrStaging, 256, 128]
      Environment:
        Variables:
          IMAGES_BUCKET: !Ref ImagesBucket
          DOCUMENTS_BUCKET: !Ref DocumentsBucket
          LOGS_BUCKET: !Ref LogsBucket
      Code:
        ZipFile: |
          import json
          import boto3
          import base64
          import os

          def lambda_handler(event, context):
              try:
                  # ファイルデータの取得
                  if event.get('isBase64Encoded', False):
                      file_content = base64.b64decode(event['body'])
                  else:
                      file_content = event['body'].encode()

                  # ファイル名の取得
                  file_name = event['headers'].get('x-file-name', 'unknown.txt')

                  # 拡張子で振り分け
                  extension = file_name.lower().split('.')[-1]

                  if extension in ['jpg', 'png', 'gif']:
                      bucket = os.environ['IMAGES_BUCKET']
                  elif extension in ['pdf', 'docx', 'txt']:
                      bucket = os.environ['DOCUMENTS_BUCKET']
                  else:
                      bucket = os.environ['LOGS_BUCKET']

                  # S3に保存
                  s3 = boto3.client('s3')
                  s3.put_object(Bucket=bucket, Key=file_name, Body=file_content)

                  return {
                      'statusCode': 200,
                      'body': json.dumps(f'File saved to {bucket}')
                  }

              except Exception as e:
                  return {
                      'statusCode': 500,
                      'body': json.dumps(f'Error: {str(e)}')
                  }

  # ===== API Gateway =====

  # REST API
  FileSorterAPI:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub 'file-sorter-api-${Environment}'
      Description: 'API for file sorting system'
      BinaryMediaTypes:
        - 'application/octet-stream'
        - 'image/*'
        - 'application/pdf'
  
  # API Gateway Resources
  UploadResource:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId:  !Ref FileSorterAPI
      ParentId: !GetAtt FileSorterAPI.RootResourceId
      PathPart: 'upload'

  # POST METHOD
  UploadMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref FileSorterAPI
      ResourceId: !Ref UploadResource
      HttpMethod: POST
      AuthorizationType: NONE
      Integration:
        Type: AWS_PROXY
        IntegrationHttpMethod: POST
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${FileSorterFunction.Arn}/invocations'
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
  
  # OPTIONS Method (CORS)
  UploadOptionsMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref FileSorterAPI
      ResourceId: !Ref UploadResource
      HttpMethod: OPTIONS
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        IntegrationResponses:
          - StatusCode: 200
            ResponseParameters:
              method.response.header.Access-Control-Allow-Origin: "'*'"
              method.response.header.Access-Control-Allow-Methods: "'POST,OPTIONS'"
              method.response.header.Access-Control-Allow-Headers: "'Content-Type,x-file-name'"
      MethodResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Access-Control-Allow-Origin: true
            method.response.header.Access-Control-Allow-Methods: true
            method.response.header.Access-Control-Allow-Headers: true

  # Lambda Permission for API Gateway
  ApiGatewayInvokePermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref FileSorterFunction
      Action: lambda:InvokeFunction
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${FileSorterAPI}/*/*'

  # API Deployment
  ApiDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn: [UploadMethod, UploadOptionsMethod]
    Properties:
      RestApiId: !Ref FileSorterAPI
      StageName: !Ref Environment
  
Outputs:
  # API Gateway URL
  ApiGatewayURL:
    Description: 'URL of the API Gateway'
    Value: !Sub 'https://${FileSorterAPI}.execute-api.${AWS::Region}.amazonaws.com/${Environment}'

CloudFormationの特徴と制約

実際にシステムを構築してみて感じた特徴や制約について紹介します!

1.同じリソースを大量に作りづらい

CloudFormationではループ処理ができないので、同様のリソースでも個別に定義する必要がありました。

Resources:
  # 画像ファイル用のバケット
  ImagesBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'file-sorter-images-${Environment}-${AWS::AccountId}'
  
  # ドキュメントファイル用のバケット(同じ構造)
  DocumentsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'file-sorter-documents-${Environment}-${AWS::AccountId}'
  
  # ログファイル用のバケット(また同じ構造)
  LogsBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub 'file-sorter-logs-${Environment}-${AWS::AccountId}'

このように、S3バケットを3つ作成するために同じ構造を繰り返し書いています。
同じリソースを大量に必要とする場合、かなり冗長な記述になってしまいそうです。

2.依存関係の手動管理が必要

リソースが作成される順番を手動で制御しなければならないケースがあります。このシステムでは、API Gatewayのデプロイに関して、メソッドの作成完了後にデプロイするといった順序性を定義する必要がありました。

ApiDeployment:
  Type: AWS::ApiGateway::Deployment
  DependsOn: [UploadMethod, UploadOptionsMethod]  # 依存関係を指定
  Properties:
    RestApiId: !Ref FileSorterAPI
    StageName: !Ref Environment

CloudFormationは依存関係を自動で管理してくれるのですが、自動管理がうまく機能しないケースではDependsOn:を使用する必要があります。これを使用することで、メソッドの作成が完了してからAPI Gatewayをデプロイするという順序性を強制するようにしています。仮にこの記述がないと
エラーが発生したり、メソッドが含まれない空のAPIが作成されてしまう可能性があるので注意が必要です。

3.条件分岐の制限

今回作成したテンプレートでは、dev,staging,prodの3環境でLambda性能を段階的に設定しています。

Conditions:
  IsProdOrStaging: !Or [
    !Equals [!Ref Environment, prod], 
    !Equals [!Ref Environment, staging]
  ]

# 使用例
FileSorterFunction:
  Properties:
    Timeout: !If [IsProdOrStaging, 15, 10]      # staging/prod=15秒, dev=10秒
    MemorySize: !If [IsProdOrStaging, 256, 128] # staging/prod=256MB, dev=128MB

この程度の条件分岐では問題ありませんでしたが、CloudFormationではシンプルな論理演算しか使用できないため、複雑な条件式が必要なケースには向いていないかもしれません。

デプロイ手順と実行時間

time aws cloudformation deploy \
  --template-file template.yaml \
  --stack-name file-sorter-dev \
  --parameter-overrides Environment=dev \
  --capabilities CAPABILITY_NAMED_IAM \
  --region ap-northeast-1

デプロイ完了までの実行時間を計測するため、timeコマンドを利用しています。

Successfully created/updated stack - file-sorter-dev
aws cloudformation deploy --template-file template.yaml --stack-name  0.45s user 0.21s system 0% cpu 1:07.44 total

計測した結果、1分7.4秒でデプロイが完了しました。

エンドエンドの試験

簡単に試験手順にも触れておきます。

API_URL="https://<api-id>.execute-api.ap-northeast-1.amazonaws.com/dev"
curl -X POST $API_URL/upload \
  -H "x-file-name: test.jpg" \
  -H "Content-Type: application/octet-stream" \
  --data-binary @test.jpg

aws s3 ls s3://file-sorter-images-dev-<account-id>/

試しにjpgファイルの振り分けが成功するか確認しました。
test.jpgが保存されているのが確認できたらOKです!

まとめ

今回はCloudFormationでシステムを構築してみました!冗長な記述、依存関係の考慮、複雑な条件式の記述が難しそうであることは気になりましたが、YAMLで全てを明示的に記述するスタイルは分かりやすさという点ではいいのかもしれないですね。
次回はSAMを使用して同じ構成を試してみたいと思います!

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?