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と接続させたいのですが、どうもうまく動いてくれず...。もうちょっと調べてみます。

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

はじめに

先日からApp RunnerとかElastiCacheとかを使った検証をしていましたが、マネージメントコンソールで作業をするのは時間がかかり、料金節約のためにリソースを削除するのも大変なので、AWS CDKで実装してみることにしました。

プロジェクト作成

プロジェクトは以下のドキュメントを見ながら作業をすることに。

docs.aws.amazon.com

NodeJS環境にてnpx cdk init --language typescriptを実行すればOK。

ネットワーク関係を作成する

各種リソースを作成するためにAWS CDKのAPIドキュメントに目を通します。

docs.aws.amazon.com

まずはVPCを作るので、aws-ec2のVpcクラスに目を通します。

docs.aws.amazon.com

lib/construct/network.tsというディレクトリ・ファイルを作成し、以下の内容として保存します。

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

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

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

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

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

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

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

あとは、lib/cdk-stack.tsを更新します。

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import { Network } from './construct/network';

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');
  }
}

これで実装は完了。

実行する

まずはcdk bootstrapを実行し、CDKに関するリソースを作成します。必要なロールやcdk-からはじまるS3バケットが作成されました。ちなみにこのS3バケットにはDeletionPolicy: Retainが指定されています。

このため、cdk bootstrapで生成された以下スクリーンショットのCloudFormationのスタックを削除してもこのS3バケットは残ります。

あとはcdk deployを実行することでAWSリソースが作成されます。

ちなみに、今回はCloud9上で実行してみたのですが、t2.microですとTypeScriptのビルドがメモリ不足で異常終了しましたので実行するにはt3.small以上のインスタンスが必要かと思います。

削除する

作成したAWSリソースはcdk destroyを実行すれば削除されます。

他のリソースを作成する

VPC以外のリソースを作成してみます。まずはECR。ドキュメントは以下のもの。

docs.aws.amazon.com

実装例は以下のもの。lib/construct/ecr-repository.tsとして保存します。

import { RemovalPolicy } from 'aws-cdk-lib';
import * as ecr from 'aws-cdk-lib/aws-ecr';
import { Construct } from 'constructs';

export class EcrRepository extends Construct {
    readonly repository: ecr.Repository;

    constructor(scope: Construct, id: string) {
        super(scope, id);
        // リポジトリを作成する
        this.repository = new ecr.Repository(scope, 'MyAppRepository', {
            repositoryName: 'myapp-repository',
            removalPolicy: RemovalPolicy.DESTROY,
            imageScanOnPush: false
        });
    }
}

ElastiCacheの作成。ドキュメントは以下のもの。

docs.aws.amazon.com

実装例は以下のもの。lib/construct/cache.ts`として保存します。

import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as elasticache from 'aws-cdk-lib/aws-elasticache';
import { Construct } from 'constructs';

interface CacheProps {
    vpc: ec2.Vpc
    cacheSecurityGroup: ec2.SecurityGroup
}

export class Cache extends Construct {
    readonly cacheCluster: elasticache.CfnCacheCluster;

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

        const { vpc, cacheSecurityGroup } = props;

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

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

その後、lib/cdk-stack.tsを以下のように更新します。

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

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');

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

あとは、cdk deployを実行すればOK。ElastiCacheの作成には10分ぐらいかかるのでちょっと気長に待ちましょう。