AWS CDKで、承認フロー付き AWS CodePipeline を構築する

GitHubと連携してアプリケーションを自動デプロイしたいが、 masterブランチにマージされたらすぐにデプロイするのではなく、承認フローを挟みたい、というケースに対応できます。
2020.04.08

AWS CDK で、認証フロー付きの AWS CodePipeline を作ってみます。次のようなユースケースを想定しています。

  • GitHubと連携してアプリケーションを自動デプロイしたいが
  • master ブランチにマージされたらすぐにデプロイするのではなく、承認フローを挟みたい
  • これを CodePipeline でやりたい
  • 一連のリソースを AWS CDK で作成したい

デプロイ可能なコードベースをmasterブランチのみに集約し、その代わりリリースタイミングについては CICD側に任せるといった運用が可能になります。ブランチの数を最小限に押さえられる点がメリットです。AWS CDK を利用してのPipeline構築は、CX事業本部の平内が、すでに 作っています。今回はソースをGitHubにしたうえで、承認フローを加えてみます。

最終形

AWS CodePipeline で、これを作ります。

images/approval_pipeline.png

なお最終段の AWS CodeBuild による作業は任意のものでかまいません。さっそく AWS CDK で構築していきましょう。

実行環境

やること

  1. サンプルリポジトリをクローンしてアプリケーションをデプロイする
  2. AWS CodePipeline の AWS CDK Stack を実装してデプロイする
  3. 承認フローの動作確認

1. サンプルリポジトリ、サンプルアプリ

ここでは、まだPipelineは作りません。サーバーレスアプリケーションのサンプルを取得して、動作を確認します。

> git clone -b hello-deploy-e2e-pipeline git@github.com:cm-wada-yusuke/template-aws-cdk-typescript-serverless-app.git
> yarn install

ディレクトリ構成について軽く説明します。このリポジトリはAWSのサーバーレスアプリケーションを yarn workspace を利用して管理する実験リポジトリです。workspaceとして、

  • app-node: Lambda Function のコード
  • infra-aws: AWS CDK

の2つを用意しています。ここから実行するコマンドは AWS CDK のものですので、ワークスペースとしては主に infra-aws を使うことになります。なお、サーバーレスアプリケーションの管理に yarn workspace が使えるのではないかという提言は CX事業本部 加藤からです。

このあと、コマンドラインから AWS へアクセスできる状況を作ります。私はデプロイしたいアカウントに Assume Role し、なおかつそれを fish shell で行うので次のようにしています(aws_swrole を利用)。

> aws_swrole cm-wada

コマンドラインでの Assume Role については次の記事を参考にしてください。

ツールとしてはこれらが使えます。

その後、はじめて CDK を使う AWS アカウントに対しては、cdk bootstrap を実行します。S3バケットが生成されます。

> yarn workspace infra-aws cdk bootstrap

---
$ /template-aws-cdk-typescript-serverless-app/node_modules/.bin/cdk bootstrap
   Bootstrapping environment aws://1234567890/ap-northeast-1...
   Environment aws://1234567890/ap-northeast-1 bootstrapped.
  Done in 11.96s.
  Done in 12.25s.

これで、AWS CDK を利用してのデプロイ準備が整いました。まずは、Pipelineではなくアプリケーションをデプロイしてみます。

> yarn workspace app-node tsc            # Lambda Function のビルド
> yarn workspace app-node run bundle     # node_modules のバンドル

---
$ shx mkdir -p dist/layer/nodejs &&  shx cp yarn.lock dist/layer/nodejs &&  shx cp package.json dist/layer/nodejs && yarn --cwd dist/layer/nodejs --production install
[1/4]   Resolving packages...
[2/4]   Fetching packages...
[3/4]   Linking dependencies...
[4/4]   Building fresh packages...
success Saved lockfile.
  Done in 1.91s.
  Done in 2.20s.

bundle は、AWS Lambda の LayerVersion へ node_modules をデプロイするための操作です。さすがに長いので package.json にスクリプトとして定義していますが、実行しているコマンドはログに出力されているとおりです:package.json をコピーし、--production のみのフラグをつけてインストールしています。

ビルドされたファイルを使って、AWS CDK からデプロイします。

> yarn workspace infra-aws cdk deploy GreetingServiceStack

---
$ /template-aws-cdk-typescript-serverless-app/node_modules/.bin/cdk deploy GreetingServiceStack
GreetingServiceStack (application): deploying...
[0%] start: Publishing 00c2d9f2aeb88c84e9dfd2d03b5c538d385e24755b01668b6cd2417680be2470:current
[50%] success: Published 00c2d9f2aeb88c84e9dfd2d03b5c538d385e24755b01668b6cd2417680be2470:current
[50%] start: Publishing eeb2864066fb75b9efff7eabaa87684d14a98a563c8508231ee97ecfbb579742:current
[100%] success: Published eeb2864066fb75b9efff7eabaa87684d14a98a563c8508231ee97ecfbb579742:current
application: creating CloudFormation changeset...
 1/5 | 9:58:45 | UPDATE_COMPLETE      | AWS::IAM::Role            | GreetingServiceStack/getGreetingReply/ServiceRole (getGreetingReplyServiceRole5EC32EC0)
 1/5 | 9:58:46 | UPDATE_IN_PROGRESS   | AWS::CDK::Metadata        | CDKMetadata
 2/5 | 9:58:47 | UPDATE_COMPLETE      | AWS::CDK::Metadata        | CDKMetadata
 2/5 | 9:58:48 | UPDATE_IN_PROGRESS   | AWS::Lambda::LayerVersion | GreetingServiceStack/NodeModulesLayer (NodeModulesLayer29E0D577) Requested update requires the creation of a new physical resource; hence creating one.
 2/5 | 9:58:57 | UPDATE_IN_PROGRESS   | AWS::Lambda::LayerVersion | GreetingServiceStack/NodeModulesLayer (NodeModulesLayer29E0D577) Resource creation Initiated
 3/5 | 9:58:57 | UPDATE_COMPLETE      | AWS::Lambda::LayerVersion | GreetingServiceStack/NodeModulesLayer (NodeModulesLayer29E0D577)
 3/5 | 9:59:00 | UPDATE_IN_PROGRESS   | AWS::Lambda::Function     | GreetingServiceStack/getGreetingReply (getGreetingReplyF4DA3046)
 4/5 | 9:59:04 | UPDATE_COMPLETE      | AWS::Lambda::Function     | GreetingServiceStack/getGreetingReply (getGreetingReplyF4DA3046)

  GreetingServiceStack (application)

Lambda Invoke を試すE2Eテストを実行して、デプロイされていることを確認しましょう。

> yarn workspace app-node jest --config=jest.config.e2e.js --runInBand --testRunner='jest-circus/runner'

---
Determining test suites to run...setup
 PASS  tests/e2e/phase1-greeting/step1.get-greeting-reply.test.ts (6.155s)
  lambda invoke test
    success: greeting function invoke
      ✓ stamp api returns reply response (682ms)

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

E2Eテストでは、'How are you?' という入力に対して Lambda Function から 'Fine, and you? > How are you?' が返ってくることを実際に Invoke して確認しています。これで、AWS CDK を使ってデプロイ可能であることがまずは確認できました。

2. AWS CodePipeline の CDK Stack

アプリケーションの CDK Stack がすでにあるので、同じようにPipelineの Stack も作成しましょう。packages/infra-aws/lib/pipeline-deploy-stack.ts を作り、実装します。AWS CDK で AWS CodePipeline を作るときは、おおよそ次の流れになります。

  • Pipelineの中に含まれる実行単位、アクションを複数個定義する
    • GitHub リポジトリの更新をPipeline契機とする SourceAction
    • 承認フローとなる ApproveAction (今回やりたいやつ)
    • デプロイするための CodeBuildAction
  • Pipeline を定義し、あらかじめ作っておいたアクションを任意のステージに設置する

これを踏まえ、コードです(クラスではなく関数スタイルで実装しています)。

packages/infra-aws/lib/pipeline-deploy-stack.ts

import * as cdk from '@aws-cdk/core';
import { Construct, Stack } from '@aws-cdk/core';
import * as codePipeline from '@aws-cdk/aws-codepipeline';
import { Pipeline } from '@aws-cdk/aws-codepipeline';
import * as actions from '@aws-cdk/aws-codepipeline-actions';
import * as codeBuild from '@aws-cdk/aws-codebuild';
import { LinuxBuildImage } from '@aws-cdk/aws-codebuild';
import * as iam from '@aws-cdk/aws-iam';

export async function greetingDeployPipelineStack(
    scope: Construct,
    id: string,
): Promise<Stack> {
    const stack = new cdk.Stack(scope, id, {
        stackName: 'DeployStack',
    });

    /**
     * GitHub リポジトリの更新をPipeline契機とする SourceAction
    **/
    const appOutput = new codePipeline.Artifact();
    const gitHubToken = cdk.SecretValue.secretsManager('GitHubToken');
    const sourceAction = new actions.GitHubSourceAction({
        actionName: 'GitHubSourceAction',
        owner: 'cm-wada-yusuke',
        oauthToken: gitHubToken,
        repo: 'template-aws-cdk-typescript-serverless-app',
        branch: 'master',
        output: appOutput,
        runOrder: 1,
    });

    /**
     * 承認フローとなる ApproveAction(今回やりたいやつ)
    **/
    const approvalAction = new actions.ManualApprovalAction({
        actionName: 'DeployApprovalAction',
        runOrder: 2,
        externalEntityLink: sourceAction.variables.commitUrl,
    });


    /**
     * デプロイするための CodeBuildAction 
    **/
    const deployRole = new iam.Role(stack, 'CodeBuildDeployRole', {
        assumedBy: new iam.ServicePrincipal('codebuild.amazonaws.com'),
        managedPolicies: [
            {
                managedPolicyArn: 'arn:aws:iam::aws:policy/AdministratorAccess',
            },
        ],
    });
    const applicationBuild = new codeBuild.PipelineProject(
        stack,
        'GreetingApplicationDeploy-project',
        {
            projectName: 'GreetinApplicationDeploy-project',
            buildSpec: codeBuild.BuildSpec.fromSourceFilename(
                './buildspec/buildspec-deploy.yml',
            ),
            role: deployRole,
            environment: {
                buildImage: LinuxBuildImage.STANDARD_3_0,
                environmentVariables: {
                    AWS_DEFAULT_REGION: {
                        type: codeBuild.BuildEnvironmentVariableType.PLAINTEXT,
                        value: stack.region,
                    },
                },
            },
        },
    );
    const applicationDeployAction = new actions.CodeBuildAction({
        actionName: 'GreetingApplicationDeployAction',
        project: applicationBuild,
        input: appOutput,
        runOrder: 3,
    });

    /**
     * Pipeline を定義し、あらかじめ作っておいたアクションを任意のステージに設置する
    **/
    const pipeline = new Pipeline(stack, 'GreetingApplicationDeploy-pipeline', {
        pipelineName: 'GreetingApplicationDeploy-pipeline',
    });

    pipeline.addStage({
        stageName: 'GitHubSourceAction-stage',
        actions: [sourceAction],
    });

    pipeline.addStage({
        stageName: 'GreetingApplicationDeploy-stage',
        actions: [approvalAction, applicationDeployAction],
    });

    return stack;
}

packages/infra-aws/bin/infra-aws.ts で、作成した Pipelineの Stack が CDK のデプロイ対象となるよう修正します。

packages/infra-aws/bin/infra-aws.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { greetingServiceApplicationStack } from '../lib/greeting-service-stack';
import { greetingDeployPipelineStack } from '../lib/pipeline-deploy-stack';

async function buildApp(): Promise<void> {
    const app = new cdk.App();

    // Application stack
    await greetingServiceApplicationStack(app, 'GreetingServiceStack');

    // Deploy stack
    await greetingDeployPipelineStack(app, 'DeployStack');
}

buildApp();

さらに、AWS CodeBuild が動作するために、buildspec.yml も必要です。

buildspec/buildspec-deploy.yml

version: 0.2
phases:
  install:
    commands:
      - yarn install
  build:
    commands:
      - yarn deploy

Pipelineをデプロイします。GitHubのアクセストークンをあらかじめ Secrets Manager に格納しておく必要があります。

> aws secretsmanager create-secret --name GitHubToken --secret-string XXXXXXYYYYYYYYYZZZZZZ
> yarn workspace infra-aws cdk deploy DeployStack

---
DeployStack: deploying...
DeployStack: creating CloudFormation changeset...
 0/5 | 9:36:04 | UPDATE_IN_PROGRESS   | AWS::CodeBuild::Project     | GreetingApplicationDeploy-project (GreetingApplicationDeployproject1BE433F6)
 1/5 | 9:36:06 | UPDATE_COMPLETE      | AWS::CodeBuild::Project     | GreetingApplicationDeploy-project (GreetingApplicationDeployproject1BE433F6)

 DeployStack

AWSコンソールにアクセスして、冒頭のようなPipelineが作成されていれば成功です。

3. 承認フローの動作確認

images/approval_pipeline.gif

GitHubSouceAction で更新が検知されてもすぐにデプロイを行うのではなく、人の手で承認するまで待つようなPipelineが構築できました。

まとめ

CICDにおいて、ブランチ管理とデプロイフローは常に議論になります。ベストなフローは現場によってさまざまですが、AWS CodePipline を使えば今回のようなユースケースに対応できます。また、AWS CDK を使うことで、なかなかメンテナンスが難しいCICDの設定についてもアプリケーションと同様コードで管理できます。

これらを組み合わせて、開発を加速していきましょう。