CDKとGithub ActionsによるCI/CDパイプライン
その昔、初めてのサーバーレスアプリケーション開発というブログを書きました。 このシリーズを通して出来上がるものは、AWSのコードシリーズを用いて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とは?という方はこちらの記事を先に見ていただければと思います。
次の環境で検証します。
$ node -v v10.18.1
それでは早速いってみましょう!!
サーバーレスアプリケーションを作成
初期処理
まずは、TypescriptのAWS CDKテンプレートを作成します。
mkdir cdk-github-actions && cd cdk-github-actions npx cdk init app --language=typescript
Lambdaファンクション作成
次にLambdaファンクションを作成します。
ハンドラーが存在するsrc/lambda/hello.ts
と、その関数から呼び出されるモジュールsrc/model/message.ts
の2つのファイルを作成します。まずはモジュールから作成します。
次にハンドラーを作成します。
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) }; }
モジュールに対するテストコードの作成
上記のモジュールに対するテストコードを作成します。
package.json
にtest:app
を追記し、
"scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest", "test:app": "jest --testMatch **/message.test.ts", "cdk": "cdk" },
テストを実行してみましょう。
$ 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 total Tests: 1 passed, 1 total Snapshots: 0 total Time: 3.041s Ran all test suites. ~/s/8/2/cdk-github-actions *…
問題なさそうです。
CDKスタック作成
次にAWSリソースを作成するためのコードを記述していきます。
ライブラリをインストール
LambdaとAPI Gatewayを作成するためのライブラリをインストールします。
npm install @aws-cdk/aws-lambda npm install @aws-cdk/aws-apigateway
CDKスタック作成
API GatewayとLambdaをデプロイするためのスタックを作成します。cdk-github-actions-stack.ts
を以下のように修正します。
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 ], }); } }
差分確認
アプリケーションをビルドし差分を確認してみましょう。
$ 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します。
git init git add . git commit -m "first commit" git remote add origin https://github.com/jogannaoki/cdk-github-actions.git git push -u origin master Counting objects: 100% (22/22), done.
Github Actions ワークフロー作成
ブランチをfeature/add-github-actions
に切り替え、Github Actionsによるワークフローを定義していきます。
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を実行
name: cdk on: 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
を追記します。
"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を投げます。
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のエンドポイントにアクセスしてみましょう。
curl https://XXXXX.execute-api.ap-northeast-1.amazonaws.com/prod/ Hello, CDK! You've hit /
AWSリソースが正しくデプロイされていることが確認できます。
API Gatewayの設定値変更
せっかくパイプラインを作成したので、AWSリソースの設定値を変更した際の挙動も確認してみます。
ブランチを切り替えます。
git checkout -b feature/change-endpointtypes-regional
API GatewayのendpointTypesをREGIONALに変更します。
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を投げます。
git add . git commit -m 'change endpointtypes regional' git push origin feature/change-endpointtypes-regional
GithubActionsの結果をみてみましょう。
想定どおりの差分ですね。問題ないのでmasterにmargeします。
Lambdaファンクション変更
Lambdaファンクションを変更した場合はどのような挙動になるのでしょうか??モジュールを変更し動作を確認してみましょう。
ブランチを切り替えます。
git checkout -b feature/change-lambda-message
src/model/message.ts
の戻り値を変更します。
先ほどと同じようにブランチをGithubにPushした後、masterブランチに対してPullRequestを投げます。
git add . git commit -m 'change lambda message' git push origin feature/change-lambda-message
するとどうでしょう。
なんと、テストで失敗しました。
よかったよかった。CIでテストを回すことで想定外のコードがリリースされるのを防げました。気を取り直して、テストコードを修正します。
GithubにPushします。
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のエンドポイントにアクセスしてみましょう。
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への通知やトリガーによってデプロイ先の環境を変えるなども簡単に実現できそうでした。
皆さんこれでゴリゴリデプロイしてくれると嬉しいです。
それではまた!!