Amazon Web Services ブログ

CDK Pipelines: AWS CDK アプリケーションの継続的デリバリ

AWS Cloud Development Kit(AWS CDK)は、使い慣れたプログラミング言語でクラウドインフラストラクチャを定義し、AWS CloudFormation を通じてプロビジョニングするためのオープンソースのソフトウェア開発フレームワークです。AWS CDK は、次の 3 つの主要なコンポーネントで構成されています。

  • 再利用可能なインフラストラクチャ・コンポーネントをモデリングするためのコアフレームワーク
  • CDK アプリケーションをデプロイするための CLI
  • AWS Construct Library(クラウドリソースを抽象化し、実績のあるデフォルト値をカプセル化する高レベルのコンポーネントのセット)

CDK を使用すると、cdk deploy を実行するだけで、ワークステーションから AWS クラウドにアプリケーションを簡単にデプロイできます。これは、初期開発およびテストを行う場合に最適ですが、本番ワークロードをデプロイするためには、より信頼性の高い自動化されたパイプラインを使用する必要があります。

CDKアプリケーションを継続的にデプロイするために、お好みのCI/CDシステムを利用することが可能ですが、より簡単で、かつすぐに利用可能な方法をお客様はご要望でした。これはCDKの中核的な理念に適合します。つまりクラウドアプリケーションの開発を可能な限り簡素化して、お客様が関心のある部分に集中することです。

CDK Pipelines の開発者プレビューリリースをお知らせします。CDK Pipelines は、AWS CodePipeline によって CDK アプリケーションの継続的なデプロイパイプラインを簡単にセットアップできる高レベルのコンストラクトライブラリです。この投稿では、CDK Pipelines を使用して、AWS Lambda と連携した Amazon API Gateway エンドポイントを 2 つの異なるアカウントにデプロイする方法について説明します。

前提条件

以下のチュートリアルでは TypeScript を使用し、バージョン 1.51.0 以降の AWS CDK が必要です。

このチュートリアルを実施するには、GitHub アカウントが必要で、ソースコードを保存する GitHub リポジトリを作成しておく必要があります。この記事のOWNERまたはREPOという表記は、フォークしたGitHubリポジトリの所有者と名前に置き換えてください。

AWS CodePipeline からこの GitHub リポジトリ にアクセスするには、GitHub personal access token を、プレーンテキストのシークレット(JSON シークレットではなく)として、AWS Secrets Manager に github-token という名前で保存する必要があります。手順については、「チュートリアル:シークレットの作成と取得」を参照してください。トークンには、repoadmin:repo_hookのスコープが必要です。

CDK パイプラインを使用すると、同じアカウントにデプロイするのと同じくらい簡単に、別のアカウントとリージョンにデプロイできます。このチュートリアルに完全に従うには、少なくとも 1 つ、できれば 2 つの AWS アカウントに対する管理者アクセス権が必要です。これらのアカウントを ACCOUNT1ACCOUNT2 と呼びます。複数の AWS アカウントを作成するには、AWS ControlTower を使用します。手順については、「AWS ControlTower — マルチアカウント AWS 環境のセットアップと管理」を参照してください(訳注: AWS ControlTower 特有の機能は使用しませんので、AWS Organizations を使用してアカウントを作成いただくのでもかまいません)。 このチュートリアルでは、AWS コマンドラインインターフェイス(AWS CLI)認証情報プロファイルで設定した、account1profile および account2profile という名前の認証情報があることを前提としています。

コンセプト

CDK Pipelines は、AWS Construct Library にあるものよりも少し高いレベルで、特定の目的向けのコンストラクトライブラリです。そのため設定の選択肢は限られますが、そのぶん使用するための労力が少なくなっています。

ライブラリ内の最も重要なコンストラクトは CDKPipeline です。CDK アプリケーションをデプロイするための CodePipeline パイプラインを設定します。パイプラインは、デプロイメントの論理フェーズを表すいくつかのステージで構成されます。各ステージには、その特定のステージで何をすべきかを定義するアクションが 1 つ以上含まれています。CDK Pipelines は、デフォルトであらかじめ定義されたいくつかのステージとアクションを持ちますが、アプリケーションのニーズに合わせてステージとアクションを追加できます。

CDK Pipelines によって作成されたパイプラインは自己書き換えを行います。つまり、パイプラインを使い始める際、cdk deploy を 1 回実行するだけで済みます。その後は、ソースコードに新しい CDK アプリケーションやパイプラインのステージを追加すると、パイプラインは自動的に自分自身のパイプラインを更新します。

次の図は、CDKパイプラインのステージを示しています。

  • Source — このステージはおそらくよくご存知でしょう。フォークしたGitHubリポジトリからCDKアプリケーションのソースを取得し、新しい commit を push するたびにパイプラインをトリガーします。
  • Build — このステージは (必要な場合) コードをコンパイルし、cdk synth を実行します。このステップの出力はクラウドアセンブリ(Cloud assemblies)です。クラウドアセンブリは、パイプラインのこの後のすべてのアクションで使用されます。
  • UpdatePipeline — このステージでは、必要に応じてパイプラインを変更します。たとえば、コードを更新してパイプラインに新しいデプロイメントステージを追加したり、アプリケーションに新しいアセットを追加したりすると、パイプラインは自動的に更新され、変更内容が反映されます。
  • PublishAssets — このステージでは、アプリで使用しているすべてのファイルアセットを Amazon Simple Storage Service(Amazon S3)に準備し、すべての Docker イメージを Amazon Elastic Container Registry(Amazon ECR)に公開します。これらは後続のデプロイメントステージで使用できるよう、すべてのアカウントとリージョンごとに展開されます。

 

すべての後続のステージで、ソースコードで指定したアカウントとリージョンに CDK アプリケーションをデプロイします。

デプロイする予定のアカウントとリージョンの組み合わせすべてに対して、最初にブートストラップする必要があります。つまり、CDK がアクセスできるように、最小限のインフラストラクチャをアカウントにプロビジョニングします。また、パイプラインを持つアカウントに対して信頼関係を追加する必要があります。その方法は、この記事の後半で説明します。

サンプルアプリケーション

このユースケースでは、単純なアプリケーションをデプロイします。アプリケーションは固定応答を返すLambda関数とAPIゲートウェイから構成され、インターネットからアクセスできます。

次のコマンドを実行して、スターターアプリケーションを作成し、必要な依存関係をインストールします。(フォルダ名を変えると以後のファイル名やクラス名が変わりますのでご注意ください)

mkdir cdkpipelines-demo && cd cdkpipelines-demo
npx cdk init --language=typescript

# Install dependencies for the CDK application
npm install @aws-cdk/aws-apigateway @aws-cdk/aws-lambda \
  @aws-cdk/aws-codepipeline @aws-cdk/aws-codepipeline-actions \
  @types/aws-lambda

# Install CDK pipelines
npm install @aws-cdk/pipelines

アプリケーションのインフラストラクチャを定義する lib/cdkpipelines-demo-stack.ts を次のコードのように変更します。

import * as apigw from '@aws-cdk/aws-apigateway';
import * as lambda from '@aws-cdk/aws-lambda';
import { CfnOutput, Construct, Stack, StackProps } from '@aws-cdk/core';
import * as path from 'path';

/**
 * A stack for our simple Lambda-powered web service
 */
export class CdkpipelinesDemoStack extends Stack {
  /**
   * The URL of the API Gateway endpoint, for use in the integ tests
   */
  public readonly urlOutput: CfnOutput;
 
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // The Lambda function that contains the functionality
    const handler = new lambda.Function(this, 'Lambda', {
      runtime: lambda.Runtime.NODEJS_12_X,
      handler: 'handler.handler',
      code: lambda.Code.fromAsset(path.resolve(__dirname, 'lambda')),
    });

    // An API Gateway to make the Lambda web-accessible
    const gw = new apigw.LambdaRestApi(this, 'Gateway', {
      description: 'Endpoint for a simple Lambda-powered web service',
      handler,
    });

    this.urlOutput = new CfnOutput(this, 'Url', {
      value: gw.url,
    });
  }
}

lib/lambda/handler.ts ファイルを追加し、次のコードを入力します。これはシンプルな AWS Lambda ハンドラを定義します。

import { APIGatewayProxyEvent, APIGatewayProxyResult, Context } from 'aws-lambda';

export async function handler(event: APIGatewayProxyEvent, context: Context): Promise<APIGatewayProxyResult> {
  return {
    body: 'Hello from a Lambda Function',
    statusCode: 200,
  };
}

空のパイプラインの定義

アプリケーションのスタックを定義したら、パイプラインを介してデプロイできます。

最初のステップは、Stage の独自のサブクラスを定義することです。このサブクラスは、アプリケーションの 論理的でまとまりのあるデプロイ可能な単位を定義します。これは、CloudFormation スタックを定義する際に Stack クラスのカスタムサブクラスを作る方法と似ています。違いは、Stage に 1 つ以上の Stack を含めることができるため、パイプライン経由で複雑なアプリケーションのコピーを複数作成できる柔軟性がある点です。このユースケースでは、ステージは 1 つのスタックのみで構成されます。

lib/cdkpipelines-demo-stage.tsを作成し、その中に次のコードを入力します。

import { CfnOutput, Construct, Stage, StageProps } from '@aws-cdk/core';
import { CdkpipelinesDemoStack } from './cdkpipelines-demo-stack';

/**
 * Deployable unit of web service app
 */
export class CdkpipelinesDemoStage extends Stage {
  public readonly urlOutput: CfnOutput;
  
  constructor(scope: Construct, id: string, props?: StageProps) {
    super(scope, id, props);

    const service = new CdkpipelinesDemoStack(this, 'WebService');
    
    // Expose CdkpipelinesDemoStack's output one level higher
    this.urlOutput = service.urlOutput;
  }
}

コードベースをきれいに整理するため、パイプライン定義を独自のスタックファイル lib/cdkpipelines-demo-pipeline-stack.tsに入れるとよいでしょう(以下のコードでOWNERREPOを置き換えることを忘れないでください)。

import * as codepipeline from '@aws-cdk/aws-codepipeline';
import * as codepipeline_actions from '@aws-cdk/aws-codepipeline-actions';
import { Construct, SecretValue, Stack, StackProps } from '@aws-cdk/core';
import { CdkPipeline, SimpleSynthAction } from "@aws-cdk/pipelines";

/**
 * The stack that defines the application pipeline
 */
export class CdkpipelinesDemoPipelineStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const sourceArtifact = new codepipeline.Artifact();
    const cloudAssemblyArtifact = new codepipeline.Artifact();
 
    const pipeline = new CdkPipeline(this, 'Pipeline', {
      // The pipeline name
      pipelineName: 'MyServicePipeline',
      cloudAssemblyArtifact,

      // Where the source can be found
      sourceAction: new codepipeline_actions.GitHubSourceAction({
        actionName: 'GitHub',
        output: sourceArtifact,
        oauthToken: SecretValue.secretsManager('github-token'),
        owner: 'OWNER',
        repo: 'REPO',
      }),

       // How it will be built and synthesized
       synthAction: SimpleSynthAction.standardNpmSynth({
         sourceArtifact,
         cloudAssemblyArtifact,
         
         // We need a build step to compile the TypeScript Lambda
         buildCommand: 'npm run build'
       }),
    });

    // This is where we add the application stages
    // ...
  }
}

前述のコードは、パイプラインの次の基本プロパティを定義します。

  • 名前
  • ソースの場所。自分のGitHubリポジトリの名前と一致するように変更してください
  • ビルドと合成(Synthesis)を行う方法。このユースケースでは、標準のNPMビルドを使用します(このタイプのビルドは npm run build を実行し、その後に npx cdk synth を実行します)。

また、パイプラインをデプロイするアカウントとリージョンの組み合わせに対して、CDKPipelinesDemoPipelinesStack をインスタンス化する必要があります。bin/cdkpipelines-demo.tsに次のコードを入れてください(必要に応じてACCOUNT1とリージョン名を置き換えてください)。

#!/usr/bin/env node
import { App } from '@aws-cdk/core';
import { CdkpipelinesDemoPipelineStack } from '../lib/cdkpipelines-demo-pipeline-stack';

const app = new App();

new CdkpipelinesDemoPipelineStack(app, 'CdkpipelinesDemoPipelineStack', {
  env: { account: 'ACCOUNT1', region: 'us-east-2' },
});

app.synth();

CDK Pipelines は CDK フレームワークの新しい機能を使用するため、明示的に有効化する必要があります。cdk.json ファイルに以下のコードを追加してください。

{
  ...
  "context": {
    "@aws-cdk/core:newStyleStackSynthesis": true
  }
}

ブートストラップ

パイプラインをデプロイする準備はほぼ完了です。まず、パイプラインをデプロイする環境が、最新のブートストラッピングスタックでブートストラップされていることを確認する必要があります。これは、新しい CDK Pipelines の機能に対応するため、CDK ブートストラッピングリソースがバージョン 1.46.0 から変更されたためです。

CDK アプリケーションをデプロイする予定のすべての環境をブートストラップする必要があります。このチュートリアルでは、次の場所(アカウントとリージョンの組み合わせ)が対象です。

  • パイプラインをプロビジョニングする場所
  • パイプラインを使用してアプリケーションをデプロイする予定の場所

これは、CDK アプリケーションをデプロイする環境ごとに 1 回だけ行う必要があります。環境がすでにブートストラップされているかどうかが不明な場合は、いつでもコマンドを再度実行できます。

account1-profile という名前のプロファイルに ACCOUNT1 の認証情報があることを確認します。詳細については、 AWS CLI の 「名前付きプロファイル」を参照してください。cdk.json が存在するディレクトリで次のコマンドを実行します。

npx cdk bootstrap \
  --profile account1-profile \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
  aws://ACCOUNT1/us-east-2

このコマンドでは、次の 2 点が重要です。

  • cdk.json"@aws-CDK/core:NewStyleStackSynthesis" コンテキストキーを定義し、cdk.jsonファイルが置かれたディレクトリでこのCDK CLI コマンドを実行することによって、ブートストラップリソースは CDK Pipelines が動作するために必要な、新しい(1.46.0 以降の)ものに切り替えられます。この新しいブートストラップスタックは、バケットといくつかのロールをアカウントに作成します。これらは、CDK CLI および CDK Pipelines が対象アカウントにデプロイを行う際に使用されます。この新しいブートストラップリソースは将来的にデフォルトになりますが、この執筆時点では、まだオプトインされています。
  • --cloudformation-execution-policies は、デプロイメントロールがアカウントに対して持つ権限を制御します。これまで、CDK CLI には、ツールを実行するユーザーと同じ権限がありました。新しいブートストラップリソースを使用すると、アカウントをブートストラップする担当者は、CDK がデプロイ対象アカウントに持つデプロイメント権限を制御できます。アカウントのフルコントロール権限を持つには、CDK を明示的にオプトインする必要があります。必要に応じて、これを別のポリシー(またはポリシー群)に変更できます。

環境をブートストラップしたら、次はパイプラインのプロビジョニングに進みます。

パイプラインのプロビジョニング

パイプラインが作成されるとすぐに、パイプラインが実行され、GitHubからコードをチェックアウトし、そこに見つかったコードに基づいて更新されます。したがって、GitHubに配置するコードは、あなたが書いたコードであることが非常に重要です。あなたが最初にすべきことは、すべての変更を commit して push することです。次のコマンドを実行します。

git add -A .
git commit -m 'Initial deployment of our empty pipeline'
git push

1回だけの操作として、パイプラインスタックをデプロイします。

npx cdk deploy \
--profile account1-profile \
CdkpipelinesDemoPipelineStack

この完了には数分かかります。最終的にCodePipeline コンソールで次のスクリーンショットのようなパイプラインを確認できます。

トラブルシューティングのヒント:パイプラインの作成中にこのステップで内部エラーが表示される場合は、Secrets Manager のシークレット名と、その値である GitHub token がそれぞれ正しく設定されているか確認してください。

最初のステージの追加

これまでのところ、パイプラインをプロビジョニングしましたが、パイプラインはまだアプリケーションをデプロイしていません。これを行うには、MyServiceStage のインスタンスをパイプラインに追加します。

lib/cdkpipelines-demo-pipelines-stack.ts の先頭に新しい import を追加し、次のコメントの下にコードを追加します。// This is where we add the application stages ACCOUNT1 を実際のアカウントIDに置き換え、us-east-2 をお好みのリージョンに置き換えてください。

import { CdkpipelinesDemoStage } from './cdkpipelines-demo-stage';

// ...

// This is where we add the application stages
pipeline.addApplicationStage(new CdkpipelinesDemoStage(this, 'PreProd', {
  env: { account: 'ACCOUNT1', region: 'us-east-2' }
}));

あとはこれを commit して push するだけです。パイプラインは自動的に自分自身を再構成し、新しいステージが追加され、デプロイされます。これを行うには、次のコマンドを実行します。

# 'npm run build' first to make sure there are no typos 
npm run build
git commit -am 'Add PreProd stage'
git push

パイプラインの完了後、サービスが稼働中であることを確認できます。CloudFormation コンソールに移動し、Preprod-WebService スタックを選択し、出力 タブに移動して、GatewayEndpoint のURL をクリックします。新しいページが開き ‘Hello from a Lambda Function’ と表示されます。

UpdatePipeline ステージが失敗と表示されることがあります。これは予期される事象であり、特に気にする必要はありません。パイプラインは自動的に自分自身を再起動し、新しいステージのデプロイに進みます。

新しいスタックを CDKPipelinesDemoStage に追加した場合も同じことが起こります。パイプラインは自動的に更新され、この新しいスタックがすべてのステージにデプロイされます。スタック間に依存関係がある場合は、自動的に正しい順序でデプロイされます。このチュートリアルではご紹介しませんが、気軽に試してみてください!

別のアカウントとリージョンへのデプロイ

別のアカウントにデプロイするのは、追加のステージをデプロイするのと同じくらい簡単です。デプロイ先のアカウントとリージョンをブートストラップする必要があり、パイプラインアカウントに信頼関係を追加する必要があります。

account2-profile という名前のプロファイルで ACCOUNT2 の認証情報を使用できる状態にして、次のコマンドを実行します。

npx cdk bootstrap \
  --profile account2-profile \
  --trust ACCOUNT1 \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
  aws://ACCOUNT2/us-west-2

このコードでは、--trust ACCOUNT1 が追加され、これによって ACCOUNT1ACCOUNT2 にデプロイできるようになります。これでパイプラインが ACCOUNT2 にデプロイできますが、同じメカニズムを使用して、開発者アカウントの認証情報しかない場合でも CDK CLI を使用して別のアカウントにデプロイできます。

ACCOUNT2 とリージョン us-west-2 をブートストラップした後、新しいステージを追加できます。この記事では Prod と呼びます。lib/cdkpipelines-demo-pipelines-stack.ts に次のコードを追加します。

    pipeline.addApplicationStage(new CdkpipelinesDemoStage(this, 'Prod', {
      env: { account: 'ACCOUNT2', region: 'us-west-2' }
    }));

再度、これを commit して push することで、パイプラインは別のリージョンとアカウントにデプロイします。次のコマンドを実行します。

# 'npm run build' first to make sure there are no typos
npm run build
git commit -am 'Add Prod stage'
git push

検証の追加

今作ったパイプラインでは、新しいコードが自動的に本番環境にプッシュされます。明らかに、このパイプラインには何かが欠けています!実際の継続的デリバリーパイプラインは、新しいコードが動作することを確認するためにテストを実行する必要があります。

このチュートリアルでは、curl を使用して、デプロイしたばかりのエンドポイントに対してウェブリクエストを実行するテストを追加します。このテストは AWS CodeBuild で実行します。実際のシナリオでは、このステップにより優れた統合テストスイートをご自身で追加してください。

テストでは、サービスの HTTP エンドポイントの URL を知っておく必要がありますが、そのエンドポイントはランダムに生成された名前を持つ API ゲートウェイです。このデモではすでに AWS CloudFormation の出力に、サービスのアドレスを出力しています。これをテストに渡せばよいのです。
cdkpipelines-demo-pipeline-stack.ts を編集し、Preprod ステージを追加するコードを次のように変更します。

  import { ShellScriptAction } from '@aws-cdk/pipelines';

  // ...

  const preprod = new CdkpipelinesDemoStage(this, 'PreProd', {
    env: { account: 'ACCOUNT1', region: 'us-east-2' }
  });
  const preprodStage = pipeline.addApplicationStage(preprod);
  preprodStage.addActions(new ShellScriptAction({
    actionName: 'TestService',
    useOutputs: {
      // Get the stack Output from the Stage and make it available in
      // the shell script as $ENDPOINT_URL.
      ENDPOINT_URL: pipeline.stackOutput(preprod.urlOutput),
    },
    commands: [
      // Use 'curl' to GET the given URL and fail if it returns an error
      'curl -Ssf $ENDPOINT_URL',
    ],
  }));

次のコマンドで commit し、pushします。

# 'npm run build' first to make sure there are no typos
npm run build
git commit -am 'Add tests to PreProd stage'
git push

次のスクリーンショットのように、PreProd ステージの最後にテストが追加されたことがわかります。

開発スタック

このパイプラインはテストスタックと本番スタックをデプロイしますが、開発者にとっては、自身の開発者アカウントにアプリケーションスタックのプライベートコピーを持ち、作業中に繰り返し使えると非常に便利です。

cdk deploy は、このユースケースに最適です。必要なのは、CDKPipelinesDemoStage の別のインスタンスを追加し、CLI の現在の設定を示す環境変数を使用するように env を定義することだけです。詳細については、「Environments」を参照してください。

bin/cdkpipelines-demo.tsに次のコードを追加します。

import { CdkpipelinesDemoStage } from '../lib/cdkpipelines-demo-stage';

// ...

new CdkpipelinesDemoStage(app, 'Dev', {
  env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
});

チームのすべての開発者は、次のコマンドを実行して、開発者のアカウントとリージョンをブートストラップする必要があります。

npx cdk bootstrap \
  --cloudformation-execution-policies arn:aws:iam::aws:policy/AdministratorAccess \
  aws://DEVELOPER_ACCOUNT/us-east-1

これで、アプリケーションの個人用コピーを自分のアカウント(CLI の現在の認証情報によって決定される)にデプロイできるようになりました。次のコマンドを実行します。

npx cdk synth
npx cdk -a cdk.out/assembly-Dev deploy

開発者プレビューの制限事項

CDK Pipelinesは現在、開発者プレビュー中です。AWS はいくつかの制限を解消するよう取り組んでいますが、次の点に留意してください。

  • コンテキストクエリ — コンテキストクエリはサポートされていません。つまり、Vpc.fromLookup() や類似の関数は利用できません。

クリーンアップ

チュートリアルの内容をクリーンアップするには、使用したそれぞれのアカウントの AWS コンソールにログインし、デプロイしたリージョンの AWS CloudFormation コンソールに移動し、CdkPipelinesDemopipelinesStackWebServiceCDKToolkitのそれぞれのスタックを選択して削除 をクリックします。

パイプラインスタック(CDKPipelinesDemoPipelinesStack)とブートストラップスタック(CDKToolkit)には、それぞれに AWS Key Management Service キーが含まれています。これらのキーを削除しないと、1 か月あたり 1USD が課金されます。

まとめ

CDK を使用して AWS アプリケーションの開発およびデプロイをより迅速かつ容易に行えるようになったことを大変嬉しく思います。CDK Pipelines とその使用方法の詳細については、CDK Pipelines のリファレンスドキュメントを参照してください。Amazon 開発チームがコードでアプリケーションインフラストラクチャを定義し、複数の AWS アカウントとリージョンに段階的にデプロイする方法の詳細については、Amazon Builders’ Library の記事「安全なハンズオフデプロイの自動化」を参照してください。実際のライブラリのデモを見たい場合は、Tech Talk 「Enhanced CI/CD with AWS CDK」を参照してください。

私たちは、プロセスをできるだけ簡単で楽しいものにしたいと願っています。あなたがこのソリューションを試してみて、私たちが何かを見逃したと思われる場合、または私たちがカバーしていないユースケースがある場合は、ぜひお教えください! GitHubプロジェクトページで進捗状況をお伝えします。

原文はこちら。翻訳はSA 大村が担当しました。