はじめに
AWSでリソースをコードとして管理する方法には、CloudFormation、SAM、CDK、Terraformなどいくつかの選択肢があります。それぞれのサービスが持つ特徴と、どのような場面で使うのがベストなのかを実践を通じて学んでいきたいと思います!
今回は第1回目ということで、CloudFormationを使用します!後日、SAM、CDK、Terraformを使用して、それぞれのツールの違いについて比較したいと考えています!
構築するシステム
題材にするのはこちらのシステムです!
ユーザーがアップロードしたファイルを、拡張子に基づいて振り分け、適切なS3バケットに保存する処理を行うものです。
処理フロー:
- API Gatewayを通じてファイルをアップロード
- Lambda関数がファイルを受け取り、拡張子を基にファイル種別を判定
- 適切な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のテンプレート
今回作成したテンプレートはこちらです!
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を使用して同じ構成を試してみたいと思います!