作って理解するCloudFormation(yaml)
はじめに
普段、AWS CDKやAmplifyを使ってアプリを作っていますが、その裏ではCloudFormationが動いています。素のCloudFormationを書いたことがなかったので、勉強がてらAPI Gateway、Lambda、DynamoDBを組み合わせたサーバーレスアーキテクチャを構築してみました。
そもそもCloudFormationとは?
CloudFormationは、AWSリソースをコード(YAMLまたはJSON)で定義し、そのコードをもとに自動的にインフラを構築・管理するサービスです。CloudFormationテンプレートのYAML形式は、以下のような構造を持っています。公式ドキュメントをそのまま載せます。
AWSTemplateFormatVersion: 'version date'
Description:
String
Metadata:
template metadata
Parameters:
set of parameters
Rules:
set of rules
Mappings:
set of mappings
Conditions:
set of conditions
Transform:
set of transforms
Resources:
set of resources
Outputs:
set of outputs
量が多いですね。必須なのはResources
だけなので、最低限ここだけ理解すれば良さそう。
各セクションの説明や使い方はこちら。
作ってみる
下準備
テンプレートを作成する前に、Lambdaのコードをzip化してS3にアップロードしておきましょう。
エラー処理などは考えずに超シンプルに書きます。
import { DynamoDBClient, PutItemCommand } from "@aws-sdk/client-dynamodb";
const client = new DynamoDBClient({ region: "ap-northeast-1" });
export const handler = async (event) => {
const data = JSON.parse(event.body);
const command = new PutItemCommand({
TableName: "CloudFormationTable",
Item: {
id: { S: data.id },
value: { S: data.value },
},
});
await client.send(command);
return {
statusCode: 200,
body: JSON.stringify({ message: "Data saved successfully" }),
};
};
zip化
$ zip -r lambda-code.zip ./
S3バケットを作成してzipファイルをアップロード
次から実際にCloudFormationでリソースの定義を書いてみます。今回は1ファイルに全リソースを定義します。
DynamoDB
初めに、DynamoDBを作成します。
Resources:
DynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: CloudFormationTable
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
-
Resources
- CloudFormationテンプレートのリソースセクションです。この中で作成するリソースを定義します。
-
DynamoDBTable
- 作成するDynamoDBテーブルの名前です。任意の名前を付けられます。
-
Type: AWS::DynamoDB::Table
- このリソースがDynamoDBテーブルであることを指定します。
-
Properties
- テーブルのプロパティを設定します。
-
TableName
- テーブルの名前を指定します。
-
AttributeDefinitions
- テーブルで使用する属性を定義します。この場合、idという属性を定義しており、そのタイプは文字列 (S) です。
-
KeySchema
- テーブルのキーを定義します。ここでは、idをハッシュキーとして指定しています(KeyType: HASH)。これは主キーの役割を果たします。
-
ProvisionedThroughput
- テーブルのスループット設定です。読み取りと書き込みのキャパシティユニットをそれぞれ指定します。
- ReadCapacityUnits: 1時間あたりに行える読み取り操作の単位数。
- WriteCapacityUnits: 1時間あたりに行える書き込み操作の単位数。
Lambda
次に、DynamoDBにデータを書き込むLambda関数を定義します。
(続きから)
LambdaFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: CloudFormationFunction
Handler: index.handler
Runtime: nodejs20.x
Role: !GetAtt LambdaExecutionRole.Arn
Code:
S3Bucket: learning-cloudformation-bucket
S3Key: lambda-code.zip
-
LambdaFunction
- 作成するLambda関数の名前です。任意の名前を付けられます。
-
Type: AWS::Lambda::Function
- このリソースがLambda関数であることを指定します。
-
Properties
- Lambda関数のプロパティを設定します。
-
FunctionName
- 関数の名前を指定します。この名前はLambda関数の呼び出しや管理に使用されます。
-
Handler: index.handler
- Lambda関数のエントリーポイントを指定します。ここでは、index というファイルの handler という関数がエントリーポイントです。
-
Runtime: nodejs20.x
- 関数が実行されるランタイム環境を指定します。ここでは、Node.js 20.x を指定しています。
-
Role: !GetAtt LambdaExecutionRole.Arn
- Lambda関数に付与するIAMロールのARNを指定します。!GetAttはCloudFormationの関数で、LambdaExecutionRole というリソースのARNを取得します。
-
Code
- Lambda関数のコードが格納されているS3バケットとオブジェクトキーを指定します。
-
S3Bucket
- Lambda関数のコードが保存されているS3バケットの名前です。
-
S3Key
- S3バケット内のLambda関数のコードファイルのキーです。ここでは、lambda-code.zip という名前のZIPファイルです。
Lambda関数がDynamoDBにアクセスするための権限を定義するIAMロールも必要です。
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: LambdaDynamoDBPolicy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource: !GetAtt DynamoDBTable.Arn
-
LambdaExecutionRole:
- 作成するIAMロールの名前です。このロールはLambda関数に付与されます。
-
Type: AWS::IAM::Role
- このリソースがIAMロールであることを指定します。
-
Properties
- IAMロールのプロパティを設定します。
-
AssumeRolePolicyDocument
- ロールを引き受けることができるエンティティを定義します。
-
Version: "2012-10-17"
- ポリシーのバージョンです。これはAWSのポリシー仕様のバージョンです。
-
Statement
- Effect: Allow
- 許可するアクションの効果を指定します。この場合は許可です。
- Principal
- ロールを引き受けるエンティティを指定します。ここでは、Lambdaサービスがこのロールを引き受けることができるようにしています。
- Service: lambda.amazonaws.com
- Lambdaサービスがこのロールを引き受けるための指定です。
- Action: sts:AssumeRole
- Lambdaがこのロールを引き受けるためのアクションです。
- Effect: Allow
-
Policies
- このロールに付与するポリシーを定義します。
-
PolicyName: LambdaDynamoDBPolicy
- ポリシーの名前です。任意の名前を付けられます。
-
PolicyDocument
- ポリシーのドキュメントを定義します。
-
Version: "2012-10-17"
- ポリシーのバージョンです。
-
Resource
- ポリシーが適用されるリソースです。ここでは DynamoDBTable リソースのARN(Amazon Resource Name)を参照しています。これにより、このロールは指定されたDynamoDBテーブルに対してのみアクセスを許可します。
API GatewayがLambda関数を呼び出すための権限も必要です。
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: "lambda:InvokeFunction"
FunctionName: !Ref LambdaFunction
Principal: "apigateway.amazonaws.com"
SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/items"
-
LambdaInvokePermission:
- このリソースは、API GatewayがLambda関数を呼び出すための権限を設定するためのものです。
-
Type: AWS::Lambda::Permission
- このリソースがLambda関数へのアクセス許可を設定することを示します。
-
Properties:
- Lambda関数に関する権限のプロパティを設定します。
-
Action: "lambda:InvokeFunction"
- 許可するアクションです。この場合、lambda:InvokeFunction はLambda関数を呼び出すための権限を指定します。
-
FunctionName: !Ref LambdaFunction
- 権限を設定するLambda関数の名前です。!Ref LambdaFunction は、テンプレート内で定義された LambdaFunction リソースの名前を参照します。
-
Principal: "apigateway.amazonaws.com"
- このロールにアクセスを許可するエンティティです。ここでは、API GatewayがLambda関数を呼び出すことを許可しています。
-
SourceArn: !Sub "arn:aws:execute-api:
{AWS::AccountId}:${ApiGateway}/*/POST/items"{AWS::Region}: - Lambda関数にアクセスを許可するAPI GatewayのARN(Amazon Resource Name)です。!Sub 関数は、テンプレート内の変数を置換します。
- ${AWS::Region} は現在のAWSリージョンを、
- ${AWS::AccountId} はAWSアカウントIDを、
- ${ApiGateway} はAPI GatewayのIDを表します。
- このARN形式は、特定のAPI Gatewayステージ、メソッド(POST)、リソースパス(/items)からのLambda関数の呼び出しを許可します。
ちょっと疲れてきましたが、あともう少しです。頑張りましょう。
API Gateway
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: CloudFormationApi
ApiResource:
Type: AWS::ApiGateway::Resource
Properties:
ParentId: !GetAtt ApiGateway.RootResourceId
PathPart: items
RestApiId: !Ref ApiGateway
ApiMethod:
Type: AWS::ApiGateway::Method
Properties:
AuthorizationType: NONE
HttpMethod: POST
ResourceId: !Ref ApiResource
RestApiId: !Ref ApiGateway
Integration:
IntegrationHttpMethod: POST
Type: AWS_PROXY
Uri: !Sub
- arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaArn}/invocations
- LambdaArn: !GetAtt LambdaFunction.Arn
- Type: AWS::ApiGateway::RestApi
- API GatewayのREST APIを作成します。
- Name
- API Gatewayの名前を CloudFormationApi に設定しています。
- ApiResource
- Type: AWS::ApiGateway::Resource
- API Gatewayにリソース(エンドポイント)を追加します。
- ParentId: !GetAtt ApiGateway.RootResourceId
- リソースがAPIのルートリソース(/)の子であることを示します。
- PathPart: /items
- パス部分を設定します。これがエンドポイントのパスです。
- RestApiId: !Ref ApiGateway
- 作成したAPI GatewayのIDを参照します。
- Type: AWS::ApiGateway::Resource
- ApiMethod
- Type: AWS::ApiGateway::Method
- API GatewayにHTTPメソッドを追加します。
- AuthorizationType: NONE
- 認証なしでアクセスできることを示します。
- HttpMethod: POST
- POSTリクエストを受け入れる設定です。
- ResourceId: !Ref ApiResource
- 設定するリソースのID(ここでは /items パス)を参照します。
- RestApiId: !Ref ApiGateway
- API GatewayのIDを参照します。
- Integration:
- IntegrationHttpMethod: POST
- Lambda関数へのリクエストメソッドとしてPOSTを指定します。
- Type: AWS_PROXY
- AWS Lambdaとの統合タイプで、Lambda関数を直接呼び出す設定です。
- Uri
- Lambda関数のARNを指定します。${AWS::Region} と ${LambdaArn} は、リージョンとLambda関数のARNに置き換えられます。
- IntegrationHttpMethod: POST
- Type: AWS::ApiGateway::Method
デプロイ
全てのリソースを定義したら、CloudFormationを使ってデプロイします。今回は1ファイルだけなのでコンソールからぽちぽちしてリソースを作ります。
作成した定義ファイルを選択
スタック名を入力
設定はそのままで次へ
IAMリソースが作成される承認にチェックを入れて送信
リソースが作られました
動作確認
デプロイが成功したら、API Gatewayのエンドポイントを使用して、Lambda関数を呼び出し、DynamoDBにデータが格納されていることを確認します。
さいごに
実際にテンプレートを作ってみて、CloudFormationの理解を深めることができました。ロールまでしっかり作り込まないといけないので少し大変だと思いました(CDKに感謝)。どなたかの理解のお役に立てれば幸いです。
NCDC株式会社( ncdc.co.jp/ )のエンジニアチームです。 募集中のエンジニアのポジションや、採用している技術スタックの紹介などはこちら( github.com/ncdcdev/recruitment )をご覧ください! ※エンジニア以外も記事を投稿することがあります
Discussion