AWS CDKで、承認フロー付き AWS CodePipeline を構築する
AWS CDK で、認証フロー付きの AWS CodePipeline を作ってみます。次のようなユースケースを想定しています。
- GitHubと連携してアプリケーションを自動デプロイしたいが
- master ブランチにマージされたらすぐにデプロイするのではなく、承認フローを挟みたい
- これを CodePipeline でやりたい
- 一連のリソースを AWS CDK で作成したい
デプロイ可能なコードベースをmasterブランチのみに集約し、その代わりリリースタイミングについては CICD側に任せるといった運用が可能になります。ブランチの数を最小限に押さえられる点がメリットです。AWS CDK を利用してのPipeline構築は、CX事業本部の平内が、すでに 作っています。今回はソースをGitHubにしたうえで、承認フローを加えてみます。
最終形
AWS CodePipeline で、これを作ります。
なお最終段の AWS CodeBuild による作業は任意のものでかまいません。さっそく AWS CDK で構築していきましょう。
実行環境
- aws-cli@2.0.1
- yarn@1.22.4
- aws-cdk@1.31.0
- Node.js 12.x
- 利用リポジトリ:簡単なサーバーレスサンプルを利用
やること
- サンプルリポジトリをクローンしてアプリケーションをデプロイする
- AWS CodePipeline の AWS CDK Stack を実装してデプロイする
- 承認フローの動作確認
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 を定義し、あらかじめ作っておいたアクションを任意のステージに設置する
これを踏まえ、コードです(クラスではなく関数スタイルで実装しています)。
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 のデプロイ対象となるよう修正します。
#!/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
も必要です。
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. 承認フローの動作確認
GitHubSouceAction
で更新が検知されてもすぐにデプロイを行うのではなく、人の手で承認するまで待つようなPipelineが構築できました。
まとめ
CICDにおいて、ブランチ管理とデプロイフローは常に議論になります。ベストなフローは現場によってさまざまですが、AWS CodePipline を使えば今回のようなユースケースに対応できます。また、AWS CDK を使うことで、なかなかメンテナンスが難しいCICDの設定についてもアプリケーションと同様コードで管理できます。
これらを組み合わせて、開発を加速していきましょう。