この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
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 を定義し、あらかじめ作っておいたアクションを任意のステージに設置する
これを踏まえ、コードです(クラスではなく関数スタイルで実装しています)。
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. 承認フローの動作確認
GitHubSouceAction
で更新が検知されてもすぐにデプロイを行うのではなく、人の手で承認するまで待つようなPipelineが構築できました。
まとめ
CICDにおいて、ブランチ管理とデプロイフローは常に議論になります。ベストなフローは現場によってさまざまですが、AWS CodePipline を使えば今回のようなユースケースに対応できます。また、AWS CDK を使うことで、なかなかメンテナンスが難しいCICDの設定についてもアプリケーションと同様コードで管理できます。
これらを組み合わせて、開発を加速していきましょう。