😊

作って理解するCloudFormation(yaml)

2024/08/31に公開

はじめに

普段、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だけなので、最低限ここだけ理解すれば良さそう。

各セクションの説明や使い方はこちら。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-anatomy.html

作ってみる

下準備

テンプレートを作成する前に、Lambdaのコードをzip化してS3にアップロードしておきましょう。
エラー処理などは考えずに超シンプルに書きます。

index.mjs
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を作成します。

cloudformation.yaml
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関数を定義します。

cloudformation.yaml
(続きから)
  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ロールも必要です。

cloudformation.yaml
  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がこのロールを引き受けるためのアクションです。
  • Policies

    • このロールに付与するポリシーを定義します。
  • PolicyName: LambdaDynamoDBPolicy

    • ポリシーの名前です。任意の名前を付けられます。
  • PolicyDocument

    • ポリシーのドキュメントを定義します。
  • Version: "2012-10-17"

    • ポリシーのバージョンです。
  • Resource

    • ポリシーが適用されるリソースです。ここでは DynamoDBTable リソースのARN(Amazon Resource Name)を参照しています。これにより、このロールは指定されたDynamoDBテーブルに対してのみアクセスを許可します。

API GatewayがLambda関数を呼び出すための権限も必要です。

cloudformation.yaml
  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::Region}:{AWS::AccountId}:${ApiGateway}/*/POST/items"

    • 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

cloudformation.yaml
  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を参照します。
  • 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に置き換えられます。

デプロイ

全てのリソースを定義したら、CloudFormationを使ってデプロイします。今回は1ファイルだけなのでコンソールからぽちぽちしてリソースを作ります。

作成した定義ファイルを選択

スタック名を入力

設定はそのままで次へ

IAMリソースが作成される承認にチェックを入れて送信

リソースが作られました

動作確認

デプロイが成功したら、API Gatewayのエンドポイントを使用して、Lambda関数を呼び出し、DynamoDBにデータが格納されていることを確認します。

さいごに

実際にテンプレートを作ってみて、CloudFormationの理解を深めることができました。ロールまでしっかり作り込まないといけないので少し大変だと思いました(CDKに感謝)。どなたかの理解のお役に立てれば幸いです。

NCDCエンジニアブログ

Discussion