AWS CDKでリソースの構築とテストコードの書き方について

はじめに

最近はAWS CDKでリソースを構築することが多いのですが、AWS CDKは一般的なプログラミング言語AWSのリソースを定義するものなので、テストコードを書くことができます。そのテストコードの書き方についてまとめてみます。

前提条件

本記事は、以下の言語とAWS CDKのバージョンについて記しています。

  • TypeScript
  • AWS CDK 2.118.0

公式ドキュメント

AWS CDKの公式ドキュメントにテストについて記されたページがあるので目を通します。が、機械翻訳での提供なので、英語版を読んだ方が良いかもしれません。

docs.aws.amazon.com

テストのカテゴリ

上記公式ドキュメントに記されていますが、テストには以下2つのカテゴリがあるようです。

  • きめ細やかなアサーション(Fine-grained assertions)
    • AWS CDKが生成するCloudFormationの各種設定値が期待通りに生成されているかテストする
  • スナップショット(Snapshot test)
    • AWS CDKが生成するCloudFormationのテンプレートを以前のものと比較し、差分があるかをテストする

開発中は「きめ細やかなアサーション(Fine-grained assertions)」でテストを書いて、本番運用後は「スナップショット(Snapshot test)」を組み合わせる感じかなと考えました。

今回は「きめ細やかなアサーション(Fine-grained assertions)」の書き方について記します。

実装

cdk initにてプロジェクトを生成すると以下のようなサンプルテストコード込みで生成されるので、それを拡張するようにします。

// import * as cdk from 'aws-cdk-lib';
// import { Template } from 'aws-cdk-lib/assertions';
// import * as Cdk from '../lib/cdk-stack';

// example test. To run these tests, uncomment this file along with the
// example resource in lib/cdk-stack.ts
test('SQS Queue Created', () => {
//   const app = new cdk.App();
//     // WHEN
//   const stack = new Cdk.CdkStack(app, 'MyTestStack');
//     // THEN
//   const template = Template.fromStack(stack);

//   template.hasResourceProperties('AWS::SQS::Queue', {
//     VisibilityTimeout: 300
//   });
});

template変数を生成するまではいわゆるおまじないなので、ある程度コピペで。stack変数を生成するnew Cdk.CdkStack(app, 'MyTestStack');は適宜自分が生成したいStackに合わせて修正するぐらいです。

AWS CDKはCloudFormationを生成するので、CloudFormationの値が想定値と一致しているかどうかをテストするのが基本的な戦略になるかなと思います。AWS CDKのaws-cdk-lib.assertionsモジュールにて、各種Matcherの説明があるのでこれがテストコードの実装の拠り所になるかなと思います。

docs.aws.amazon.com

個人的には、以下のMatcherを使うことが多いです。

  • resourceCountIs
    • 対象のリソースが想定する数作成されるか
  • hasResourceProperties
    • 対象のリソースのプロパティが想定している値であるか
  • hasResource
    • 対象のリソースのプロパティ以外の設定(DeletionPolicyなど)の値が想定しているものか

繰り返しになりますが、AWS CDKはCloudFormationを生成するので、各種検証する値はCloudFormationのドキュメントも合わせて確認することになるかなと思います。

docs.aws.amazon.com

利用するサービスに対してCloudFormationの知識がないとテストも書きにくいと思います。この辺がちょっとハードルが高いかなと思いますが、一回ですべてを網羅するのは難しいので徐々に書き足す形が良いのかなと思っています。

なお、AWSの各種サービスは日々バージョンアップが行われ、AWS CDK自身も頻繁にバージョンアップされています。以下のページを見ると、1ヶ月に4回以上はザラにアップデートされています。

www.npmjs.com

CloudFormationのドキュメントにはあるのにAWS CDKにはない場合にはAWS CDKのバージョンアップを実施するとサクッと解決することが多いかなと思います。

実行

実行はnpm run testで実行できます。実行後、失敗するとどこで失敗したかがわかるように表示されます。ここらへんはJtestの機能です。

% npm run test

> cdk@0.1.0 test
> jest

 FAIL  test/cdk.test.ts
  ✕ ECRが1つ作成されること (82 ms)

  ● ECRが1つ作成されること

    Template has 1 resources with type AWS::ECR::Repository, but none match as expected.
    The 1 closest matches:
    Repository22E53BBD :: {
      "DeletionPolicy": "Delete",
      "Properties": {
    !!   Expected false but received true
        "EmptyOnDelete": true,
        "RepositoryName": "my-ruby-app"
      },
      "Type": "AWS::ECR::Repository",
      "UpdateReplacePolicy": "Delete"
    }

       9 |
      10 |   template.resourceCountIs('AWS::ECR::Repository', 1);
    > 11 |   template.hasResourceProperties('AWS::ECR::Repository', {
         |            ^
      12 |     RepositoryName: 'my-ruby-app',
      13 |     EmptyOnDelete: false,
      14 |   });

      at Template.hasResourceProperties (node_modules/aws-cdk-lib/assertions/lib/template.js:1:3101)
      at Object.<anonymous> (test/cdk.test.ts:11:12)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.696 s, estimated 2 s

成功すると、以下のように表示されます。

% npm run test            

> cdk@0.1.0 test
> jest

 PASS  test/cdk.test.ts
  ✓ ECRが1つ作成されること (83 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.712 s
Ran all test suites.

考察

AWS CDK自身は1ヶ月に複数回バージョンアップされています。クラウドの更新頻度を踏まえるとそれぐらいのバージョンアップは必然かなと思います。そのバージョンアップに追随するためにはテストコードを書くのは自然な発想と考えています。世の中、AWS CDKを使ってリソースを作ることは書かれていますが、テストコードの書き方は書かれていなかったので今回記事化しました。

実際には、AWS CDK、CloudFormation、Jestの知識がいるのでテストを書くとなるとハードルが高いなと感じます。ですが、一度に書き上げるのではなく徐々に作り上げていくとよいのかなと思いました。

ここで書いたのはあくまで基本的な記述方法で、上記のマニュアルにもいろんな検証方法が掲載されています。それらも実際の利用シーンとあわせて実装してみることが今後の課題です。

また、カバレッジを取得することはできないのかなと思ったりもしました。AWS CDKではそんなに複雑なことをしないケースがほとんどですのであまりニーズはないのかもしれません。

LambdaとRDSの連携をCDKで簡単実装する

はじめに

先日、Lambda上でコンテナイメージを動かしRDSに接続するということをやりました。

miyohide.hatenablog.com

このときは手作業でぽちぽちやりましたが、環境設定が面倒くさかったのでCDKで実装してみました。

VPCを作る

まずはVPCを作成します。こんな感じで。ちょっとでも料金節約のために、natGatewaysの数は1に設定します。

    // VPCの作成
    const vpc = new Vpc(this, "MyVPC", {
      enableDnsHostnames: true,
      enableDnsSupport: true,
      maxAzs: 2,
      natGateways: 1,
      subnetConfiguration: [
        {
          name: "PublicSubnet",
          subnetType: SubnetType.PUBLIC,
          cidrMask: 24,
          mapPublicIpOnLaunch: true,
        },
        {
          name: "PrivateSubnet",
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
          cidrMask: 24,
        },
        {
          name: "DBSubnet",
          subnetType: SubnetType.PRIVATE_ISOLATED,
          cidrMask: 24,
        }
      ]
    });

こんな感じでリソースが作られます。

作ってみて思いましたが、name属性にSubnetはいらなかったかなと思います。自動生成する名前にもSubnetがつくので冗長でした。

Lambdaを作る

コンテナイメージを作るLambdaを作成します。コンテナイメージはECRにあるので、こんな感じで実装します。

    // ECRのリポジトリを指定する
    const repository = Repository.fromRepositoryName(this, "MyRepository", "my-ruby-app");

    // Lambda用のセキュリティグループを作成する
    const lambdaSecurityGroup = new SecurityGroup(this, "LambdaSecurityGroup", {
      vpc: vpc,
      description: "Lambda Security Group",
      allowAllOutbound: true,
    });

    // ECRにあるコンテナイメージを利用してLambda関数を作成する
    const lambda = new Function(this, "Lambda", {
      code: Code.fromEcrImage(repository, {
        tag: "latest",
      }),
      functionName: "my-ruby-app",
      runtime: Runtime.FROM_IMAGE,
      handler: Handler.FROM_IMAGE,
      timeout: cdk.Duration.seconds(30),
      vpc: vpc,
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_WITH_EGRESS,
      },
      securityGroups: [lambdaSecurityGroup],
    });

既存のECRはRepository.fromRepositoryNameで取得できるので、それを使い、Functioncode属性にてCode.fromEcrImageにて対象のイメージを指定します。

RDSを作る

RDSを作成します。こんな感じで実装します。

    // RDS用のセキュリティグループの作成
    const rdsSecurityGroup = new SecurityGroup(this, "RDSSecurityGroup", {
      vpc: vpc,
      description: "RDS Security Group",
      allowAllOutbound: true,
    });

    const dbSubnetGroup = new SubnetGroup(this, "MyDBSubnetGroup", {
      vpc: vpc,
      description: "My DB Subnet Group",
      vpcSubnets: {
        subnetType: SubnetType.PRIVATE_ISOLATED,
        onePerAz: true,
      }
    });

    // RDSインスタンスの作成と設定を行う。今回はPostgreSQLを使用しているため、
    // DatabaseInstanceEngine.POSTGRESを指定する。
    const rdsInstance = new DatabaseInstance(this, "MyRDSInstance", {
      engine: DatabaseInstanceEngine.POSTGRES,
      instanceType: InstanceType.of(InstanceClass.T3, InstanceSize.MICRO),
      vpc: vpc,
      databaseName: "mypostgresdb",
      multiAz: false,
      subnetGroup: dbSubnetGroup,
      securityGroups: [rdsSecurityGroup],
    });

上記のCDK内でRDSのパスワードなどは指定していませんが、これらはSecret Managerに自動的に登録されます。

前回でのプログラムでは環境変数経由でユーザー名やパスワードの取得をしていましたが、Secret Managerから読み取らせる必要があります。実装についてはまた後日。

RDSの作成は、そこそこ時間がかかるのでのんびり待ちます。

セキュリティグループの設定

セキュリティグループの指定は以下の実装で適切なものが実装されます。

rdsInstance.connections.allowDefaultPortFrom(lambda, "Lambda to RDS");

おまけ

これでCDKで前回の環境を実装することができました。今回はテスト用だったので、一度動作確認をしたらcdk destroyで削除しようとしたのですが、実際の削除には30分ぐらい時間がかかりました。主な要因は、Lambdaが生成するネットワークインターフェースが20分ぐらい使っているという状況のまま消えなかったため。あまり気にせずにのんびり待ちます。

Lambdaで動くコンテナイメージ(Ruby)を作成し、RDS(PostgreSQL)と接続してみた

はじめに・動機

先日、RDS(PostgreSQL)に接続するLambda関数をRubyで実装しようとしたとき、pg gemの動作がうまく動きませんでした。このため、今回はLambda関数をコンテナイメージで動かすことをやってみます。

コンテナイメージを作る

AWSのドキュメントにLambda関数で動くコンテナイメージの作り方が記載されていたのでそれに従います。

docs.aws.amazon.com

ベースイメージとして、public.ecr.aws/lambda/ruby:3.2を使うのがポイントかと思います。pg gemを使いたいので、関連ライブラリをインストールする必要があります。実際に実装したDockerfileは以下のようなものになりました。

FROM public.ecr.aws/lambda/ruby:3.2

RUN yum install -y amazon-linux-extras && \
    amazon-linux-extras enable postgresql14 && \
    yum group install "Development Tools" -y

RUN yum install -y postgresql-devel

# Copy Gemfile and Gemfile.lock
COPY Gemfile Gemfile.lock ${LAMBDA_TASK_ROOT}/

# Install the specified gems
RUN bundle config set --local path 'vendor/bundle' && \
    bundle install

# Copy function code
COPY lambda_function.rb ${LAMBDA_TASK_ROOT}/

# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "lambda_function.LambdaFunction::Handler.process" ]

必然的にコンテナイメージも大きくなります。マルチステージビルド化で多少小さくなるかもしれませんが、まずは動作確認のためにこのまま進めます。

コンテナイメージを作っておくと、ローカルで動作確認も取れます。docker runで動かして、別のターミナルからcurl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'を叩けばOK。今回は、以下のようなdocker-compose.ymlを作成してdocker compose upで起動して、

version: '3'
services:
  app:
    build: .
    image: my_lambda_app:0.0.1
    ports:
      - "9000:8080"
    environment:
      PG_HOSTNAME: db
      PG_USERNAME: "postgres"
      PG_PASSWORD: "postgres"

  db:
    image: postgres:15
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

先ほどのcurl "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'を叩けば、動作確認も簡単にできました。

CodeBuildでコンテナイメージを作成し、ECRにpushする

次に、Lambda上で動かすことを考えます。CodeBuildでコンテナイメージを作成し、ECRにpushすることを実装します。

AWSのドキュメントにサンプルがあるのでそれをお手本に。

docs.aws.amazon.com

CodeBuildの作成時に、「特権付与」にチェックを入れるのを忘れずに。

また、ポリシーの追加も必要です。こんな感じで。

以下のbuildspec.ymlを作成してビルドを実行します。

version: 0.2

phases:
  pre_build:
    commands:
      - aws ecr get-login-password --region $AWS_DEFAULT_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNTID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com
  build:
    commands:
      - docker build -t $IMAGE_REPO_NAME:$IMAGE_TAG .
      - docker tag $IMAGE_REPO_NAME:$IMAGE_TAG $AWS_ACCOUNTID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG
  post_build:
    commands:
      - docker push $AWS_ACCOUNTID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$IMAGE_TAG

成功すると、作成していたECRにイメージがpushされています。

コンテナイメージをLambdaで動かす

あとはLambdaで動かします。Lambda作成時に「コンテナイメージ」を選択して、画面の指示通りに指定してあげればOK。

今回は、SELECT * FROM pg_stat_activityの結果を出力させるものを動かしました。RDSの接続設定をすれば、無事動くことが確認できました。

考察

今回の検証で、一番時間がかかったのはDockerfileの作成でした。ベースイメージのpublic.ecr.aws/lambda/ruby:3.2Amazon Linux 2ですのでgemのビルドに必要な前提ライブラリであるDevelopment Toolspostgresql-develを特定するのが一番時間がかかりました。特定さえできれば、あとはコピペで良いかなと考えています。

今回はRDS for PostgreSQLに接続することが目的でしたので、postgresql-develが前提ライブラリでしたが、RDS for MySQLなどはそれぞれ別の前提ライブラリが必要となることは注意点となります。

コンテナイメージを作ることでローカルでの動作確認が取れるというのは思わぬメリットでした。AWS上での開発も高速とはいえ、ローカルで動作させる速度には敵わないのでこれは大きなメリットかなと思います。

AWS Application ComposerのIDE拡張機能の試用と生成AIの挑戦

はじめに

AWS re:Invent 2023の内容を見てみると、AWS Application ComposerのIDE拡張機能が出たことを知りました。

aws.amazon.com

Application Composerはサーバーレスアプリケーションのインフラ部分を視覚的に作成できるサービスという説明が目についたのですが、個人的にはCloudFormationのテンプレート作成支援ツールって感じです。

aws.amazon.com

早速試してみました。

なお、このタイトルははてなブログのAI機能を使って生成してもらいました。

AWS Builder IDの作成

本件において必須ではありませんが、生成AIにIaCコードを生成させるためにAmazon CodeWhispererを使うことにします。そのためにはAWS Builder IDを作成します。AWS Builder ID profileに移動してAWS Builder IDを作成しておきます。

AWS Toolkit for Visual Studio Codeをインストールする

今回、Visual Studio CodeIDEとして使うため、その拡張機能であるAWS Toolkit for Visual Studio Codeをインストールします。

aws.amazon.com

Application Composerを操作する

適当なYAMLファイルを作成し、右クリックで「Open with Application Composer」を選択します。

すると以下のスクリーンショットのような画面が現れます。

拡張コンポーネントはかなり色々な設定項目をこの画面上で設定できます。以下はLambda関数の例。

他にも標準IaCリソースというものがあり、ここでは多くのリソースが用意されています。

標準IaCでは詳細な設定項目は用意されていません。リソース構成の部分でCloudFormationの内容を記述する感じです。

ここで「提案を生成」をクリックすると少し待った後に生成されます。

こんな感じでポチポチやれば、いつの間にかYAMLファイルが完成です。以下のものを作ってみました。

YAMLファイルは以下の通り。

Transform: AWS::Serverless-2016-10-31
Resources:
  Function:
    Type: AWS::Serverless::Function
    Properties:
      Description: !Sub
        - Stack ${AWS::StackName} Function ${ResourceName}
        - ResourceName: Function
      MemorySize: 1024
      Timeout: 30
      Tracing: Active
      PackageType: Image
      ImageUri: myrubyapp:0.0.1
  FunctionLogGroup:
    Type: AWS::Logs::LogGroup
    DeletionPolicy: Retain
    Properties:
      LogGroupName: !Sub /aws/lambda/${Function}
  Repository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: myrubyapp
      RepositoryPolicyText:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowPushPull
            Effect: Allow
            Principal: '*'
            Action:
              - ecr:GetDownloadUrlForLayer
              - ecr:BatchGetImage
              - ecr:BatchCheckLayerAvailability
              - ecr:PutImage
              - ecr:InitiateLayerUpload
              - ecr:UploadLayerPart
              - ecr:CompleteLayerUpload
  VPC:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.0.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      InstanceTenancy: default
      Tags:
        - Key: Name
          Value: Primary_VPC
  PublicSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.0.0/24
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region
  PrivateSubnet1:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref VPC
      CidrBlock: 10.0.1.0/24
      AvailabilityZone: !Select
        - 0
        - !GetAZs
          Ref: AWS::Region

生成AIのお約束ですが、完璧に正しいものがでてくるわけではないので過度な期待は危険かなと思います。とりあえず、よく使われるテンプレートが生成される感じです。このため、全くの初心者が生成AIでプロ並みにというわけではなく、ある程度わかっている人向けなのかなと感じました。

AWS LambdaからRDSへの接続設定が簡単になっていた

AWS LambdaからRDSへの接続設定をするのは地味に面倒くさいのですが、RDSのアクションメニューから「Lambda接続のセットアップ」から簡単に設定できるようになっていたので、試してみました。振り返ると2023年8月4日の更新が該当するようです。

aws.amazon.com

公式ドキュメントは以下のもの。

docs.aws.amazon.com

RDSのアクションから「Lambda接続のセットアップ」を選択して、画面の指示通りに入力したら作業は終わります。

が、私は以下の点でハマりました。

  • 既存のLambda関数はRDSと同じVPCに存在する必要があります。

Lambda関数をVPCに存在させるためには、以下のドキュメントを見ながら。

docs.aws.amazon.com

  • 新規にLambda関数を作成することもできるみたいですが、ランタイムはNode固定でした。
  • 最初はRubyPostgreSQLの構成で実行しようと思いましたが、実行時にcannot load such file -- pgが出力されて動かなかったので、諦めてJavaで書き直しました。
    • ちょっと検索してみると、以下のStackoverflowの記事に該当しそうなのですが、検証はできませんでした。

stackoverflow.com

設定画面に以下のような図が出て、これからやる内容が理解できます。

AWS CDKでAWSリソースを作成する(3)

はじめに

先日からAWS CDKを使ってAWSリソースを作成してみることをしています。前回までは以下の記事を参照。

今日はAppRunnerからElastiCacheに接続するアプリの環境を作成してみます。アプリケーションは以下の記事で実装したものを使います。

miyohide.hatenablog.com

ネットワークを作成

VPCやセキュリティグループを作成します。最初の実装を変更して、addEgressRuleを設定します。addEgressRuleを設定するにはSecurityGroupのコンストラクタにおいてallowAllOutboundfalseに設定する必要があります。

import { Construct } from 'constructs';
import {
  IpAddresses,
  Port,
  SecurityGroup,
  SubnetType,
  Vpc,
} from 'aws-cdk-lib/aws-ec2';

export class Network extends Construct {
  readonly vpc: Vpc;
  readonly appRunnerSecurityGroup: SecurityGroup;
  readonly cacheSecurityGroup: SecurityGroup;

  // VPCとサブネットを作る
  constructor(scope: Construct, id: string) {
    super(scope, id);

    this.vpc = new Vpc(scope, 'VPC', {
      vpcName: 'myapp-vpc',
      ipAddresses: IpAddresses.cidr('10.0.0.0/16'),
      maxAzs: 2,
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: 'myapp-Public',
          subnetType: SubnetType.PUBLIC,
        },
        // Redis用のサブネット
        {
          cidrMask: 24,
          name: 'myapp-cache',
          subnetType: SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
      natGateways: 0,
    });

    // App Runnerに設定するセキュリティグループ
    this.appRunnerSecurityGroup = new SecurityGroup(
      scope,
      'AppRunnerSecurityGroup',
      {
        vpc: this.vpc,
        allowAllOutbound: false,
        description: 'for myapp-app-runner',
        securityGroupName: 'myapp-app-runner-sg',
      },
    );

    //Cacheに設定するセキュリティグループ
    this.cacheSecurityGroup = new SecurityGroup(scope, 'CacheSecurityGroup', {
      vpc: this.vpc,
      description: 'for myapp-cache',
      securityGroupName: 'myapp-cache-sg',
    });

    // AppRunnerセキュリティグループからCacheセキュリティグループへのポート6379を許可
    this.appRunnerSecurityGroup.addEgressRule(
      this.cacheSecurityGroup,
      Port.tcp(6379),
    );

    // CacheセキュリティグループにてAppRunnerセキュリティグループからポート6379の通信を許可
    this.cacheSecurityGroup.addIngressRule(
      this.appRunnerSecurityGroup,
      Port.tcp(6379),
    );
  }
}

allowAllOutboundのデフォルト値はtrueでこのまま作成するとセキュリティグループのアウトバウンドは以下のような設定で作られます。

allowAllOutboundに明確にfalseを指定することでaddEgressRuleの設定が有効になり、以下のようなアウトバウンドが設定されたセキュリティグループがつくられます。

ElastiCacheを作成

CDKを使ってElastiCacheを作成します。検証用なので、ノードタイプをcache.t3.microに、ノード数は1とします。

import { SecurityGroup, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { CfnCacheCluster, CfnSubnetGroup } from 'aws-cdk-lib/aws-elasticache';
import { Construct } from 'constructs';

interface CacheProps {
  vpc: Vpc;
  cacheSecurityGroup: SecurityGroup;
}

export class Cache extends Construct {
  readonly cacheCluster: CfnCacheCluster;

  constructor(scope: Construct, id: string, props: CacheProps) {
    super(scope, id);

    const { vpc, cacheSecurityGroup } = props;

    const cacheSubnetGroup = new CfnSubnetGroup(this, 'CacheSubnetGroup', {
      subnetIds: vpc.selectSubnets({
        subnetType: SubnetType.PRIVATE_WITH_EGRESS,
      }).subnetIds,
      description: 'Group of subnets to place Cache into',
    });

    this.cacheCluster = new CfnCacheCluster(this, 'CacheCluster', {
      engine: 'redis',
      cacheNodeType: 'cache.t3.micro',
      numCacheNodes: 1,
      cacheSubnetGroupName: cacheSubnetGroup.ref,
      vpcSecurityGroupIds: [cacheSecurityGroup.securityGroupId],
    });
  }
}

App Runnerを作成

App RunnerからElastiCacheに接続するにはVPC Connectorの作成が必要です。また、ElastiCacheの接続先の設定も必要です。今回、接続先は環境変数で設定することとします。具体的なCDKの実装コードは以下の通り。

import { Construct } from 'constructs';
import { SecurityGroup, SubnetType, Vpc } from 'aws-cdk-lib/aws-ec2';
import { Repository } from 'aws-cdk-lib/aws-ecr';
import { CfnCacheCluster } from 'aws-cdk-lib/aws-elasticache';
import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
import { CfnService, CfnVpcConnector } from 'aws-cdk-lib/aws-apprunner';

interface AppRunnerProps {
  vpc: Vpc;
  repository: Repository;
  appRunnerSecurityGroup: SecurityGroup;
  cacheCluster: CfnCacheCluster;
}

export class AppRunner extends Construct {
  constructor(scope: Construct, id: string, props: AppRunnerProps) {
    super(scope, id);

    const { vpc, repository, appRunnerSecurityGroup, cacheCluster } = props;

    // Roleの作成(ECRに接続するため)
    const accessRole = new Role(scope, 'AppRunnerAccessRole', {
      roleName: 'myapp-AppRunnerAccessRole',
      assumedBy: new ServicePrincipal('build.apprunner.amazonaws.com'),
    });

    accessRole.addManagedPolicy(
      ManagedPolicy.fromAwsManagedPolicyName(
        'service-role/AWSAppRunnerServicePolicyForECRAccess',
      ),
    );

    // VPC Connectorの作成
    const vpcConnector = new CfnVpcConnector(scope, 'MyAppVPCConnector', {
      subnets: vpc.selectSubnets({
        subnetType: SubnetType.PRIVATE_WITH_EGRESS,
      }).subnetIds,
      securityGroups: [appRunnerSecurityGroup.securityGroupId],
    });

    const service = new CfnService(this, 'AppRunnerService', {
      sourceConfiguration: {
        authenticationConfiguration: {
          accessRoleArn: accessRole.roleArn,
        },
        autoDeploymentsEnabled: true,
        imageRepository: {
          imageIdentifier: `${repository.repositoryUri}:latest`,
          imageRepositoryType: 'ECR',
          imageConfiguration: {
            port: '8080',
            runtimeEnvironmentVariables: [
              {
                name: 'REDIS_HOST',
                value: cacheCluster.attrRedisEndpointAddress,
              },
              {
                name: 'REDIS_PORT',
                value: cacheCluster.attrRedisEndpointPort,
              },
            ],
          },
        },
      },
      networkConfiguration: {
        egressConfiguration: {
          egressType: 'VPC',
          vpcConnectorArn: vpcConnector.attrVpcConnectorArn,
        },
      },
    });
  }
}

Stackを作成

これまで作成したものを組み合わせてAWSリソースを作成します。

import { Construct } from 'constructs';
import { Network } from './construct/network';
import { EcrRepository } from './construct/ecr-repository';
import { Cache } from './construct/cache';
import { AppRunner } from './construct/app-runner';
import { Stack, StackProps } from 'aws-cdk-lib';

export class CdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // VPCを作成する
    const { vpc, appRunnerSecurityGroup, cacheSecurityGroup } = new Network(
      this,
      'Network',
    );

    // ECRを作成する
    const { repository } = new EcrRepository(this, 'Ecr');

    // Cacheを作成する
    const { cacheCluster } = new Cache(this, 'ElastiCache', {
      vpc,
      cacheSecurityGroup,
    });

    // App Runnerを作成する
    new AppRunner(this, 'AppRunner', {
      vpc,
      repository,
      appRunnerSecurityGroup,
      cacheCluster,
    });
  }
}

デプロイする

cdk deployを実行するとApp Runnerも作成でき、ElastiCacheにも接続することができました。

AWS CDKでAWSリソースを作成する(2)

はじめに

先日からAWS CDKを使ってAWSリソースを作成してみることをしています。前回は以下の記事を参照。

今日はAppRunnerを作成してみます。

AppRunnerを作成する

今回はコンテナイメージをAppRunnerで動かすことにします。CDKのコードは以下の通り。

import * as apprunner from 'aws-cdk-lib/aws-apprunner';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from "constructs";

interface AppRunnerProps {
    vpc: ec2.Vpc,
    repository: ecr.Repository,
    appRunnerSecurityGroup: ec2.SecurityGroup,
}

export class AppRunner extends Construct {
    constructor(scope: Construct, id: string, props: AppRunnerProps) {
        super(scope, id);

        const { vpc, repository, appRunnerSecurityGroup } = props;

        // Roleの作成(ECRに接続するため)
        const accessRole = new iam.Role(scope, 'AppRunnerAccessRole', {
            roleName: 'myapp-AppRunnerAccessRole',
            assumedBy: new iam.ServicePrincipal('build.apprunner.amazonaws.com'),
        });

        accessRole.addManagedPolicy(
            iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppRunnerServicePolicyForECRAccess'),
        );

        const service = new apprunner.CfnService(this, 'AppRunnerService', {
            sourceConfiguration: {
                authenticationConfiguration: {
                    accessRoleArn: accessRole.roleArn,
                },
                autoDeploymentsEnabled: true,
                imageRepository: {
                    imageIdentifier: `${repository.repositoryUri}:latest`,
                    imageRepositoryType: 'ECR',
                    imageConfiguration: {
                        port: '8080',
                    },
                },
            },
        });
    }
}

ECRからコンテナイメージが取得できずにハマったのですが、accessRole.addManagedPolicyにてservice-role/AWSAppRunnerServicePolicyForECRAccessポリシーを付与してあげればOKでした。

上記をapp-runnner.tsとして保存し、stack側からは以下のように呼び出してあげたらOKでした。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Network } from './construct/network';
import { EcrRepository } from './construct/ecr-repository';
import { AppRunner } from './construct/app-runner';

export class CdkStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // VPCを作成する
    const { vpc, appRunnerSecurityGroup, cacheSecurityGroup } = new Network(this, 'Network');

    // ECRを作成する
    const { repository } = new EcrRepository(this, 'Ecr');

    // App Runnerを作成する
    new AppRunner(this, 'AppRunner', { vpc, repository, appRunnerSecurityGroup });
  }
}

cdk deployを実行すると無事CloudFormation上でデプロイが成功しました。

AppRunnerも想定通りに動いてくれました。

本当はElastiCacheと接続させたいのですが、どうもうまく動いてくれず...。もうちょっと調べてみます。