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分ぐらいかかるのでちょっと気長に待ちましょう。

Spring Session Redisの接続先をAmazon ElastiCacheにする

はじめに

Spring Bootアプリの開発でセッション管理をする際にSpring Sessionを使うことが多いかなと思います。今回、Spring Session Data Redisの接続先としてAmazon ElastiCacheを使ってみましたので、その内容を記します。

前提条件

今回、Spring Bootは3.1.5を使用しました。このバージョンが重要で、Spring Boot 2.7までとは設定が異なりましたので、その点も踏まえて記します。

また、EC2上で動かしているSpring BootアプリからAmazon ElastiCacheに対して接続する構成としました。

実装

build.gradleのdependenciesに以下の二つを追記します。

    implementation 'org.springframework.boot:spring-boot-starter-data-redis'
    implementation 'org.springframework.session:spring-session-data-redis'

接続先の設定はapplication.propertiesにて以下のようにします。Spring Boot 3.xから設定項目が微妙に変わっているのが要注意です。

spring.data.redis.host=${REDIS_HOST:localhost}
spring.data.redis.port=${REDIS_PORT:6379}

Spring Boot 2.7までですと、以下の記述がapplication.propertiesに必要でした。

spring.session.redis.configure-action=none

これは、下記ページにあるようにElastiCache RedisにてRedisのconfigコマンドは使用が制限されており、その対応のためです。

docs.aws.amazon.com

Spring Boot 2.7では上記の記述をしていない場合、アプリ起動時(bootRunなど実行時)に以下のメッセージが出て、起動に失敗します。メッセージ内にERR unknown command 'CONFIG'というのがconfigコマンドの使用が制限されていることを示しています。

2023-11-19 06:35:19.826  WARN 27001 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'enableRedisKeyspaceNotificationsInitializer' defined in class path resource [org/springframework/boot/autoconfigure/session/RedisSessionConfiguration$SpringBootRedisHttpSessionConfiguration.class]: Invocation of init method failed; nested exception is org.springframework.data.redis.RedisSystemException: Error in execution; nested exception is io.lettuce.core.RedisCommandExecutionException: ERR unknown command 'CONFIG', with args beginning with: 'GET' 'notify-keyspace-events'

ただ、Spring Boot 3.0からは不要となります。

詳細は以下のブログを参照。

tech.excite.co.jp

アプリの実装は以下のような形で。これもSpring Boot 3.xからHttpSessionのimportがjakartaのものになるのが注意点です。

package com.github.miyohide.springboot_31;

import jakarta.servlet.http.HttpSession;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GreetingController {
  private final HttpSession httpSession;
  private final RedisTemplate<String, String> redisTemplate;
  private static final Logger logger = LoggerFactory.getLogger(GreetingController.class);

  public GreetingController(HttpSession httpSession, RedisTemplate<String, String> redisTemplate) {
    this.httpSession = httpSession;
    this.redisTemplate = redisTemplate;
  }

  @GetMapping("/")
  public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
    logger.info("HTTP Session id = [" + httpSession.getId() + "]");
    if (httpSession.isNew()) {
      httpSession.setAttribute("name", name);
    }
    return new Greeting(
        httpSession.getId(),
        (String) httpSession.getAttribute("name"),
        httpSession.getCreationTime());
  }

  @GetMapping("/view")
  public String viewSession() {
    Set<String> redisKeys = redisTemplate.keys("spring:session:sessions:*");
    List<String> keysList = new ArrayList<>();
    for (String redisKey : redisKeys) {
      keysList.add(redisKey);
    }
    return keysList.toString();
  }

  @GetMapping("/goodbye")
  public String goodbye() {
    String httpSessionId = httpSession.getId();
    String name = httpSession.getAttribute("name").toString();
    httpSession.invalidate();
    return "bye. HTTP Session id = [" + httpSessionId + "], name = [" + name + "]";
  }
}

実行

環境変数REDIS_HOSTREDIS_PORTAmazon ElastiCacheのエンドポイントの値を入れてSpring Bootアプリを実行すると、Amazon ElastiCacheにセッション情報が格納されていることが確認できます。

ソース

以下にサンプルコードを載せています。

github.com

考察

Amazon ElastiCacheの使用例としてSpring Session Redisを使った例を実装してみました。Spring Bootのバージョン間で若干の差がありますが、単純にセッションの置き場としてでしたら設定する項目が少なければ少ないほど良いと考えており、今回の変更は大変嬉しいです。

ただ、Amazon ElastiCacheは非常に高機能ですし、せっかく使うのであればセッションの置き場だけで使うのはちょっともったいない感じもあります。なんらかの使用用途がないか模索しています。