Spring Boot アプリケーションをお手軽に本番運用

AWS App Runner と AWS CDK の活用 ~ 第三回

2023-07-03
デベロッパーのためのクラウド活用方法

Author : 林 政利

こんにちは、ソリューションアーキテクトの林です。普段は、コンテナという技術にフォーカスして、さまざまな業種、業態のお客様を支援させていただいています。

本シリーズは、 AWS App Runner と Spring Boot をテーマに、 簡単なアプリケーションをデプロイするところから始めて、データベースやキャッシュサーバーとの接続、秘匿情報の管理、Spring Boot Actuator によるメトリクスの取得とその活用、カナリアリリースなど、本番環境で実施したい諸々の方法を皆様にご紹介していくものです。

前回 は、App Runner サービスを Amazon VPC と接続し、 VPC 内の Amazon ElastiCache に Spring Boot アプリケーションからアクセスするところまでをご紹介しました。また、 App Runner、VPC、ElastiCache と、取り扱う AWS のサービスが多くなってきたことから、そうしたサービスを AWS CDK でコード化して管理する、IaC の具体的な手順を紹介しました。

さて、 AWS には、データベースの接続情報やパスワードなどの、アプリケーションの設定情報や秘匿情報を管理するためのマネージドサービス、AWS Systems Manager (SSM) Parameter Store や AWS Secrets Manager があります。
AWS App Runner は、2023 年 1 月のアップデートで、Parameter Store や Secrets Manager から設定情報、秘匿情報を取得して、App Runner のサービスから参照できるようになりました。

前回、アプリケーションの設定情報を、アプリケーションの環境変数に直接設定していましたが、この記事では、設定情報を SSM Parameter Store に保存し、App Runner のサービスから参照するようにします。これにより、例えばデータベースやキャッシュサーバーのプロビジョニングや管理はインフラを管理するチームが実施し、その接続に必要な情報が格納されたストアの ARN だけをアプリケーションの開発チームに渡して利用してもらう、という役割分担も可能になりますね。

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »

この連載記事のその他の記事はこちら

選択
  • 選択
  • 第一回 ~ Spring Boot アプリケーションをデプロイ
  • 第二回 ~VPC 内の Amazon ElastiCache へアクセス
  • 第三回 ~アプリケーションの設定情報を AWS Systems Manager Parameter Store に保存
  • 最終回 ~ App Runner サービスのモニタリング方法

この記事のデモを無料でお試しいただけます »

毎月提供されるデベロッパー向けアップデート情報とともに、クレジットコードを受け取ることができます。 


実装するアーキテクチャ

以下の図が、前回まで CDK で作成したアーキテクチャを表したものです。

ご覧の通り、CDK で VPC を作成し、そこに ElastiCache for Redis を作成していました。そして、 App Runner の VPC 接続 (VPC Connector) を作成して、App Runner が稼働している、AWS が管理している VPC から CDK で作成された VPC へ接続し、アプリケーションから Redis へ接続できるようにしています。

今回、ここに新しく SSM Parameter Store の値を作成し、 App Runner からこの値を取得して使用するようにします。


CDK から、SSM Parameter Store の値を作成する

SSM Parameter Store は、 CDK からも取り扱うことができます。前回の作成した CDK のコードを修正し、Redis の接続情報を Parameter Store に格納します。
lib/infra-stack.ts を以下のように変更してみてください。

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

/**
* ElastiCache へ接続するための情報
*/
export type CacheConnection = {
  /** Redis に付与された SecurityGroup ID */
  readonly securityGroupId: string;
  /** Redis のホスト名 */
  // readonly host: string;
  // 文字列から、 Parameter Store にデータを変更
  readonly host: ssm.StringParameter;
  /** Redis のポート */
  // 文字列から、 Parameter Store にデータを変更
  // readonly port: string;
  readonly port: ssm.StringParameter;
}

/**
* Redis などのインフラを構築するスタック
*/
export class InfraStack extends cdk.Stack {

  /** 構築した VPC */
  readonly vpc: ec2.IVpc;
  /** 構築した Redis への接続情報 */
  readonly cacheConnection: CacheConnection;

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

    // VPC を構築
    this.vpc = new ec2.Vpc(this, 'Vpc', {
      natGateways: 1
    });

    // Redis を構築し、そこに接続するための情報を保存する
    this.cacheConnection = this.cache();
  }

  /**
   * Redis を構築
   * @returns 構築した Redis へ接続するための情報
   */
  private cache(): CacheConnection {
    // Redis に付与されるセキュリティグループ
    const cacheSecurityGroup = new ec2.SecurityGroup(this, "CacheSecurityGroup", { vpc: this.vpc });

    // Redis を構築する VPC のサブネット
    const subnetGroup = new elasticache.CfnSubnetGroup(this, "CacheSubnetGroup", {
      subnetIds: this.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds,
      description: "Group of subnets to place Cache into",
    });

    // Redis 構築
    const cacheCluster = new elasticache.CfnCacheCluster(this, "CacheCluster", {
      engine: "redis",
      cacheNodeType: "cache.t3.micro",
      numCacheNodes: 1,
      cacheSubnetGroupName: subnetGroup.ref,
      vpcSecurityGroupIds: [cacheSecurityGroup.securityGroupId],
    });
    
    // Redis へ接続するための情報を SSM Parameter Store に登録
    const cacheHostParameter = new ssm.StringParameter(this, "CacheHostParameter", {
      stringValue: cacheCluster.attrRedisEndpointAddress,
    });
    const cachePortParameter = new ssm.StringParameter(this, "CachePortParameter", {
      stringValue: cacheCluster.attrRedisEndpointPort,
    });
    
    // Redis へ接続するための情報を返り値にする
    return {
      securityGroupId: cacheSecurityGroup.securityGroupId,
      // host: cacheCluster.attrRedisEndpointAddress,
      // 接続情報を文字列から Paramete Store に変更
      host: cacheHostParameter,
      // port: cacheCluster.attrRedisEndpointPort,
      // 接続情報を文字列から Paramete Store に変更
      port: cachePortParameter,
    }
  }
}

Parameter Store への接続情報の登録も、CDK であれば、直観的に書くことができます。


SSM Parameter Store の値を App Runner から参照する

この Parameter Store の値を App Runner のサービスから利用する設定も追加してみましょう。
lib/infra-stack.ts を以下のように変更してみてください。

import * as cdk from 'aws-cdk-lib';
import { StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as apprunner from 'aws-cdk-lib/aws-apprunner';
import { CacheConnection } from './infra-stack';

export class ServiceStack extends cdk.Stack {
  constructor(scope: Construct, id: string, vpc: ec2.IVpc, cacheConnection: CacheConnection, props?: StackProps) {
    super(scope, id, props);

    // VPC での App Runner サービスのセキュリティグループ
    const vpcConnectionSecurityGroup = new ec2.SecurityGroup(this, "SecurityGroup", { vpc });

    // App Runner サービスが、ElastiCache へ接続できるようセキュリティグループを設定
    const cacheSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(
      this, "CacheSecurityGroup", cacheConnection.securityGroupId, {
    });
    cacheSecurityGroup.addIngressRule(
      vpcConnectionSecurityGroup, ec2.Port.tcp(6379), "Ingress to the cache");

    // App Runner サービスと VPC の接続 (VPC Connector) を作成
    const connector = new apprunner.CfnVpcConnector(this, 'VpcConnector', {
      subnets: vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds,
      securityGroups: [ vpcConnectionSecurityGroup.securityGroupId ],
    });
    // SSM Parameter Store に保存された Redis の接続情報を取得できるよう、IAM Role を App Runner サービスに追加
    const instanceRole = new iam.Role(this, 'InstanceRole', {
      // App Runner サービスにより使用されるという設定
      assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
      
      inlinePolicies: {
        ssmParameter: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              // Parameter Store からのパラメーター取得を許可する設定
              effect: iam.Effect.ALLOW,
              actions: [ "ssm:GetParameters" ],
              resources: [
                cacheConnection.host.parameterArn,
                cacheConnection.port.parameterArn,
              ],
            }),
          ],
        }),
      },
    });
    // App Runner サービスを作成
    const service = new apprunner.CfnService(this, 'Service', {
      sourceConfiguration: {
        authenticationConfiguration: {
          // ソースコード取得用の CodeStar Connection ARN を設定
          connectionArn: this.node.tryGetContext('connectionArn'),
        },
        codeRepository: {
          // ソースコードのリポジトリ URL やブランチを設定
          repositoryUrl: this.node.tryGetContext('repositoryUrl'),
          sourceCodeVersion: {
            type: "BRANCH",
            value: "main",
          },
          codeConfiguration: {
            configurationSource: "API",
            codeConfigurationValues: {
              // Java のマネージドランタイムを設定
              runtime: "CORRETTO_11",
              // アプリケーションのビルドコマンド
              buildCommand: "./gradlew bootJar && cp build/libs/*.jar ./",
              // アプリケーションの実行コマンド
              startCommand: "java -jar ./expt-apprunner-springboot.jar",
              // アプリケーションに設定する環境変数
              // runtimeEnvironmentVariables: [
              //  {
              //     name: "CACHE_HOST",
              //     // InfraStack から渡された ElastiCache のホスト名
              //     value: cacheConnection.host,
              //   },
              //   {
              //     name: "CACHE_PORT",
              //     // InfraStack から渡された ElastiCache のポート番号
              //     value: cacheConnection.port,
              //   },
              // ],
              
              // 環境変数を Parameter Store, Secretes Manager から取得する場合は
              // runtimeEnvironmentSecretsを使用して、取得する値のARNを設定
              runtimeEnvironmentSecrets: [
                {
                  name: "CACHE_HOST",
                  value: cacheConnection.host.parameterArn,
                },
                {
                  name: "CACHE_PORT",
                  value: cacheConnection.port.parameterArn,
                },
              ],
            }
          },
        },
      },
      
      // この App Runner サービスに IAM Role をセットする
      instanceConfiguration: {
        instanceRoleArn: instanceRole.roleArn,
      },
      networkConfiguration: {
        egressConfiguration: {
          egressType: "VPC",
          vpcConnectorArn: connector.attrVpcConnectorArn,
        },
      },
    });
    new cdk.CfnOutput(this, 'AppRunnerServiceURL', {
      value: service.attrServiceUrl,
    });
  }
}

いかがでしょうか。変更点も、かなり理解しやすいものになっています。

まず、以下で App Runner から SSM Parameter Store にアクセスできるよう、 IAM Role を設定しています。具体的には、ドキュメントの Instance role を設定しています。

    // SSM Parameter Store に保存された Redis の接続情報を取得できるよう、IAM Role を App Runner サービスに追加
    const instanceRole = new iam.Role(this, 'InstanceRole', {
      // App Runner サービスにより使用されるという設定
      assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
      
      inlinePolicies: {
        ssmParameter: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              // Parameter Store からのパラメーター取得を許可する設定
              effect: iam.Effect.ALLOW,
              actions: [ "ssm:GetParameters" ],
              resources: [
                cacheConnection.host.parameterArn,
                cacheConnection.port.parameterArn,
              ],
            }),
          ],
        }),
      },
    });
    
...
    // App Runner サービスを作成
    const service = new apprunner.CfnService(this, 'Service', {
    ...
      // この App Runner サービスに IAM Role をセットする
      instanceConfiguration: {
        instanceRoleArn: instanceRole.roleArn,
      },

そして、以下のように、Parameter Store の値を参照するよう、 App Runner を設定しているだけですね。

    // App Runner サービスを作成
    const service = new apprunner.CfnService(this, 'Service', {
      sourceConfiguration: {
      ...
        codeRepository: {
        ...
          codeConfiguration: {
          ...
            codeConfigurationValues: {
              ...              
              // 環境変数を Parameter Store, Secretes Manager から取得する場合は
              // runtimeEnvironmentSecretsを使用して、取得する値のARNを設定
              runtimeEnvironmentSecrets: [
                {
                  name: "CACHE_HOST",
                  value: cacheConnection.host.parameterArn,
                },
                {
                  name: "CACHE_PORT",
                  value: cacheConnection.port.parameterArn,
                },
              ],

さて、では、この変更をデプロイしてみましょう。

# infra-stack.ts で定義している設定 (cacheConnection.host, cacheConnection.port) を使用しているため、
# 一度 ServiceStack を削除しないと infra-stack.ts の設定を反映できない
npx cdk destroy ServiceStack
npx cdk deploy ServiceStack

App Runner コンソール からデプロイされたサービスを選択し、「Configuration」タブから「Service settings」を確認すると、SSM Parameter Store から設定を参照している様子が確認できます。

クリックすると拡大します

これで、Redis の接続情報を Parameter Store に格納し、Parameter Store の値を App Runner から参照させ、Spring Boot アプリケーションで使用することができました!


まとめ

この第三回では、Redis の接続情報を Parameter Store に設定し、App Runner で使用すること、そしてその一連の設定を CDK で IaC 化するところを見てきました。

接続情報やクレデンシャルなどの情報は、App Runner の環境変数に直接書き込むのではなく、Secrets Manager や Parameter Store に格納したいところです。例えば、今回の infra-stack.ts に相当するスタックをインフラ管理者が管理し、service-stack.ts のスタックを開発者が管理する、という分割も考えられますね。そうすることで、開発者は接続情報の具体的な値を知る必要がなくなり、より安全に開発にフォーカスすることができます。

さて、次回のテーマはモニタリングとデプロイです。App Runner と Spring Boot の組み合わせでは、さまざまな方法でアプリケーションをモニタリングすることができます。次回は、モニタリングとその活用方法について、具体的な実装を見ながらご紹介いたします。

この連載記事のその他の記事はこちら

選択
  • 選択
  • 第一回 ~ Spring Boot アプリケーションをデプロイ
  • 第二回 ~VPC 内の Amazon ElastiCache へアクセス
  • 第三回 ~アプリケーションの設定情報を AWS Systems Manager Parameter Store に保存
  • 最終回 ~ App Runner サービスのモニタリング方法

builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます


筆者プロフィール

林 政利 (@literalice)
アマゾン ウェブ サービス ジャパン合同会社
コンテナスペシャリスト ソリューションアーキテクト

フリーランスや Web 系企業で業務システムや Web サービスの開発、インフラ運用に従事。近年はベンダーでコンテナ技術の普及に努めており、現在、AWS Japan で Amazon ECS や Amazon EKS でのコンテナ運用や開発プロセス構築を中心にソリューションアーキテクトとして活動中。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する