CDKとGithub ActionsによるCI/CDパイプライン
その昔、初めてのサーバーレスアプリケーション開発というブログを書きました。
このシリーズを通して出来上がるものは、以下のようなAWSリソースとそれをデプロイするためのパイプラインです。
時は流れ、2020年。同じような仕組みを作るのであればCDKとGithub Actions使いたいという思いに駆られたので、こんな感じのパイプラインを作成してみました。
今回作成したコードは以下のリポジトリにあげています。
目次
CDKとGithub Actions
今回構築するアプリケーションの全体構成はこちら。
CDKで「クライアントからリクエストを受けて文字列を返却する」簡単なアプリケーションを作成します。
AWSにデプロイされるまでの流れは以下のようになります。
- ローカルでCDKを使ったアプリケーションを作成
- featureブランチを作成しmasterブランチ対しPull Request
- Github ActionsがPull Requestをトリガーにアプリケーションコードに対するテスト、およびcdk diffを実行
- 差分を確認し問題なければmasterにmarge
- Github Actionsがmasterへのmargeをトリガーにcdk deployを実行
CDKとは?Github Actionsとは?という方はこちらの記事を先に見ていただければと思います。
次の環境で検証します。
1 2 | $ node -vv10.18.1 |
それでは早速いってみましょう!!
サーバーレスアプリケーションを作成
初期処理
まずは、TypescriptのAWS CDKテンプレートを作成します。
1 2 | mkdir cdk-github-actions && cd cdk-github-actionsnpx cdk init app --language=typescript |
Lambdaファンクション作成
次にLambdaファンクションを作成します。
ハンドラーが存在するsrc/lambda/hello.tsと、その関数から呼び出されるモジュールsrc/model/message.tsの2つのファイルを作成します。まずはモジュールから作成します。
1 2 3 | export function message(path: string) { return `Hello, CDK! You've hit ${path}\n`} |
次にハンドラーを作成します。
1 2 3 4 5 6 7 8 9 | import { message } from "../model/message";export const handler = async (event: any = {}): Promise<any> => { return { statusCode: 200, headers: { "Content-Type": "text/plain" }, body: message(event.path) };} |
モジュールに対するテストコードの作成
上記のモジュールに対するテストコードを作成します。
1 2 3 4 5 | import { message } from "../../src/model/message";test('正しいメッセージが返却される', () => { expect(message('hoge')).toEqual(`Hello, CDK! You've hit hoge\n`)}) |
package.jsonにtest:appを追記し、
1 2 3 4 5 6 7 | "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "test:app": "jest --testMatch **/message.test.ts", "cdk": "cdk"}, |
テストを実行してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $ npm run test:app> cdk-github-actions@0.1.0 test:app /XXX> jest --testMatch **/message.test.ts PASS test/model/message.test.ts ✓ 正しいメッセージが返却される (3ms)Test Suites: 1 passed, 1 totalTests: 1 passed, 1 totalSnapshots: 0 totalTime: 3.041sRan all test suites. ~/s/8/2/cdk-github-actions *… |
問題なさそうです。
CDKスタック作成
次にAWSリソースを作成するためのコードを記述していきます。
ライブラリをインストール
LambdaとAPI Gatewayを作成するためのライブラリをインストールします。
1 2 | npm install @aws-cdk/aws-lambdanpm install @aws-cdk/aws-apigateway |
CDKスタック作成
API GatewayとLambdaをデプロイするためのスタックを作成します。cdk-github-actions-stack.tsを以下のように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import cdk = require('@aws-cdk/core');import lambda = require('@aws-cdk/aws-lambda');import apigw = require('@aws-cdk/aws-apigateway');export class CdkGithubActionsStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // defines an AWS Lambda resource const hello = new lambda.Function(this, 'HelloHandler', { runtime: lambda.Runtime.NODEJS_10_X, // execution environment code: lambda.Code.asset('src/'), // code loaded from the "lambda" directory handler: 'lambda/hello.handler' // file is "hello", function is "handler" }); // defines an API Gateway REST API resource backed by our "hello" function. new apigw.LambdaRestApi(this, 'Endpoint', { handler: hello, endpointTypes: [ apigw.EndpointType.EDGE ], }); }} |
差分確認
アプリケーションをビルドし差分を確認してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | $ npm run build $ npm run cdk diff==== 一部抜粋 =====Resources[+] AWS::IAM::Role HelloHandler/ServiceRole HelloHandlerServiceRole11EF7C63 [+] AWS::Lambda::Function HelloHandler HelloHandler2E4FBA4D [+] AWS::ApiGateway::RestApi Endpoint EndpointEEF1FD8F [+] AWS::ApiGateway::Deployment Endpoint/Deployment EndpointDeployment318525DA37c0e38727e25b4317827bf43e918fbf [+] AWS::ApiGateway::Stage Endpoint/DeploymentStage.prod EndpointDeploymentStageprodB78BEEA0 [+] AWS::IAM::Role Endpoint/CloudWatchRole EndpointCloudWatchRoleC3C64E0F [+] AWS::ApiGateway::Account Endpoint/Account EndpointAccountB8304247 [+] AWS::ApiGateway::Resource Endpoint/Default/{proxy+} Endpointproxy39E2174E [+] AWS::Lambda::Permission Endpoint/Default/{proxy+}/ANY/ApiPermission.CdkGithubActionsStackEndpoint73E73CAA.ANY..{proxy+} EndpointproxyANYApiPermissionCdkGithubActionsStackEndpoint73E73CAAANYproxy4ED8430A [+] AWS::Lambda::Permission Endpoint/Default/{proxy+}/ANY/ApiPermission.Test.CdkGithubActionsStackEndpoint73E73CAA.ANY..{proxy+} EndpointproxyANYApiPermissionTestCdkGithubActionsStackEndpoint73E73CAAANYproxyE1463D1E [+] AWS::ApiGateway::Method Endpoint/Default/{proxy+}/ANY EndpointproxyANYC09721C5 [+] AWS::Lambda::Permission Endpoint/Default/ANY/ApiPermission.CdkGithubActionsStackEndpoint73E73CAA.ANY.. EndpointANYApiPermissionCdkGithubActionsStackEndpoint73E73CAAANYD11C262F [+] AWS::Lambda::Permission Endpoint/Default/ANY/ApiPermission.Test.CdkGithubActionsStackEndpoint73E73CAA.ANY.. EndpointANYApiPermissionTestCdkGithubActionsStackEndpoint73E73CAAANYDB8B9B1B [+] AWS::ApiGateway::Method Endpoint/Default/ANY EndpointANY485C938B Outputs[+] Output Endpoint/Endpoint Endpoint8024A810: {"Value":{"Fn::Join":["",["https://",{"Ref":"EndpointEEF1FD8F"},".execute-api.",{"Ref":"AWS::Region"},".",{"Ref":"AWS::URLSuffix"},"/",{"Ref":"EndpointDeploymentStageprodB78BEEA0"},"/"]]}} |
cdk deploy実行時に作成されるリソースが確認できます。
Github Actions ワークフロー作成
Github Actionsのワークフローを作成します。
リポジトリ作成
まずはGithubにリポジトリを作成し、先程までのコードをPushします。
1 2 3 4 5 6 | git initgit add .git commit -m "first commit"git remote add origin https://github.com/jogannaoki/cdk-github-actions.gitgit push -u origin masterCounting objects: 100% (22/22), done. |
Github Actions ワークフロー作成
ブランチをfeature/add-github-actionsに切り替え、Github Actionsによるワークフローを定義していきます。
1 | git checkout -b feature/add-github-actions |
Github Actionsは、CodePipelineとは違いリポジトリ内のコンフィグファイルとしてワークフローを定義することができます。.github/workflows/配下にcdk.ymlを作成します。
以下のような挙動となるようにワークフローを定義します。
- pull_requestおよびmasterブランチへのpushによりワークフローをトリガー
- pull_requestの場合はtestとcdk diffを実行
- masterブランチへのpushの場合はcdk deployを実行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | name: cdkon: push: branches: - master pull_request:jobs: aws_cdk: runs-on: ubuntu-18.04 steps: - name: Checkout uses: actions/checkout@v1 - name: Setup Node uses: actions/setup-node@v1 with: node-version: '10.x' - name: Setup dependencies run: npm ci - name: Build run: npm run build - name: Unit tests if: contains(github.event_name, 'pull_request') run: npm run test:app - name: CDK Diff Check if: contains(github.event_name, 'pull_request') run: npm run cdk:diff env: AWS_DEFAULT_REGION: 'ap-northeast-1' AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - name: CDK Deploy if: contains(github.event_name, 'push') run: npm run cdk:deploy env: AWS_DEFAULT_REGION: 'ap-northeast-1' AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} |
package.jsonにGithubActionsから実行するためのスクリプトcdk:diffとdeployを追記します。
1 2 3 4 5 6 7 8 9 | "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "test:app": "jest --testMatch **/message.test.ts", "cdk": "cdk", "cdk:diff": "cdk diff || true", "cdk:deploy": "cdk deploy --require-approval never"}, |
AWSアクセスキーの登録
以下を参考にAdmin権限を持つユーザーのAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEYをGitHubに登録します。
Github ActionsによるCI
完成したCIを確認してみましょう!
ブランチをGithubにPushした後、masterブランチに対してPullRequestを投げます。
1 2 3 | git add .git commit -m 'add github actions!!'git push origin feature/add-github-actions |
すると、GithubActionsがトリガーされます。
それぞれの詳細も確認することができます。
テスト結果および作成されるAWSリソースも確認することができます。
期待値通りです。
Github ActionsによるCD
PRが問題なさそうだったので、masterにmargeしてみましょう!
margeをトリガーにGithubActionsが実行され、AWSリソースがデプロイされます。
API Gatewayのエンドポイントにアクセスしてみましょう。
1 2 | curl https://XXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/Hello, CDK! You've hit / |
AWSリソースが正しくデプロイされていることが確認できます。
API Gatewayの設定値変更
せっかくパイプラインを作成したので、AWSリソースの設定値を変更した際の挙動も確認してみます。
ブランチを切り替えます。
1 | git checkout -b feature/change-endpointtypes-regional |
API GatewayのendpointTypesをREGIONALに変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import cdk = require('@aws-cdk/core');import lambda = require('@aws-cdk/aws-lambda');import apigw = require('@aws-cdk/aws-apigateway');export class CdkGithubActionsStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { super(scope, id, props); // defines an AWS Lambda resource const hello = new lambda.Function(this, 'HelloHandler', { runtime: lambda.Runtime.NODEJS_10_X, // execution environment code: lambda.Code.asset('src/'), // code loaded from the "lambda" directory handler: 'lambda/hello.handler' // file is "hello", function is "handler" }); // defines an API Gateway REST API resource backed by our "hello" function. new apigw.LambdaRestApi(this, 'Endpoint', { handler: hello, endpointTypes: [ apigw.EndpointType.REGIONAL ], }); }} |
先ほどと同じようにブランチをGithubにPushした後、masterブランチに対してPullRequestを投げます。
1 2 3 | git add .git commit -m 'change endpointtypes regional'git push origin feature/change-endpointtypes-regional |
GithubActionsの結果をみてみましょう。
想定どおりの差分ですね。問題ないのでmasterにmargeします。
Lambdaファンクション変更
Lambdaファンクションを変更した場合はどのような挙動になるのでしょうか??モジュールを変更し動作を確認してみましょう。
ブランチを切り替えます。
1 | git checkout -b feature/change-lambda-message |
src/model/message.tsの戻り値を変更します。
1 2 3 | export function message(path: string) { return `Hello, CDK! You've hit ${path} v2\n`} |
先ほどと同じようにブランチをGithubにPushした後、masterブランチに対してPullRequestを投げます。
1 2 3 | git add .git commit -m 'change lambda message'git push origin feature/change-lambda-message |
するとどうでしょう。
なんと、テストで失敗しました。
よかったよかった。CIでテストを回すことで想定外のコードがリリースされるのを防げました。気を取り直して、テストコードを修正します。
1 2 3 4 5 | import { message } from "../../src/model/message";test('正しいメッセージが返却される', () => { expect(message('hoge')).toEqual(`Hello, CDK! You've hit hoge v2\n`)}) |
GithubにPushします。
1 2 3 | git add .git commit -m 'change test code'git push origin feature/change-lambda-message |
GithubActionsの結果をみてみましょう。
asset.pathに差分が出ています。これはS3にアップロードされるLambdaファンクションを元にしたハッシュ値です。Lambdaファンクションが変更されると、差分として出力されます。このassetはcdk diffを実行した環境のcdk.outに出力されます。
今回はLambdaファンクションを変更しているため想定内の差分です。PRをmargeし、デプロイ完了した後API Gatewayのエンドポイントにアクセスしてみましょう。
1 2 | curl https://XXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/Hello, CDK! You've hit / v2 |
Good!!
さいごに
CDKとGithub ActionsでCI/CDパイプラインを作成してみました。Github Actionsは初めて触りましたが、リポジトリ内のコンフィグでワークフローを定義できるのでCodePipelineよりは使い勝手が良いように感じました。また、今回は実施していませんが、Slackへの通知やトリガーによってデプロイ先の環境を変えるなども簡単に実現できそうでした。
皆さんこれでゴリゴリデプロイしてくれると嬉しいです。
それではまた!!