CDKとGithub ActionsによるCI/CDパイプライン

2020.01.30

その昔、初めてのサーバーレスアプリケーション開発というブログを書きました。
このシリーズを通して出来上がるものは、以下のような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つのファイルを作成します。まずはモジュールから作成します。

src/model/message.ts

export function message(path: string) {
  return `Hello, CDK! You've hit ${path}\n`
}

次にハンドラーを作成します。

src/lambda/hello.ts

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)
  };
}

モジュールに対するテストコードの作成

上記のモジュールに対するテストコードを作成します。

test/model/message.test.ts

import { message } from "../../src/model/message";

test('正しいメッセージが返却される', () => {
    expect(message('hoge')).toEqual(`Hello, CDK! You've hit hoge\n`)
})

package.jsontest: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の場合はtestcdk diffを実行
  • masterブランチへのpushの場合はcdk deployを実行

.github/workflows/cdk.yml

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:diffdeployを追記します。

  "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の戻り値を変更します。

src/model/message.ts

export function message(path: string) {
  return `Hello, CDK! You've hit ${path} v2\n`
}

先ほどと同じようにブランチをGithubにPushした後、masterブランチに対してPullRequestを投げます。

git add .
git commit -m 'change lambda message'
git push origin feature/change-lambda-message

するとどうでしょう。

なんと、テストで失敗しました。

よかったよかった。CIでテストを回すことで想定外のコードがリリースされるのを防げました。気を取り直して、テストコードを修正します。

test/model/message.test.ts

import { message } from "../../src/model/message";

test('正しいメッセージが返却される', () => {
    expect(message('hoge')).toEqual(`Hello, CDK! You've hit hoge v2\n`)
})

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への通知トリガーによってデプロイ先の環境を変えるなども簡単に実現できそうでした。

皆さんこれでゴリゴリデプロイしてくれると嬉しいです。

それではまた!!