GitHub ActionsとAWS CDKを使ってAWS Lambdaを自動デプロイしていく

この記事は[ぷりぷりあぷりけーしょんず Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ppap)の４日目の記事です。 

# はじめに
個人開発をする上でAWS Lambdaというのは色んな面で低コストで様々なことができ、アプリエンジニアな私からするとなんとなく「やった」感が得られるものが有りついつい使ってしまうし、そんな人はきっと沢山いるでしょう。知らんけど。

デプロイも楽だし、Node.jsやPythonならなんかミスったらコンソールで直接いじいじ直しちゃえば何とかなるもんね、うん最高。満足満足。

と、満足せずに今日はより開発現場でも使いやすく、Gitで管理しながらプルリクが通ってmasterにマージされたらあら不思議、本番に反映されている！な事をGitHub ActionsとAWS CDKを使って実現して行こうと思います。

# スキルセット
GitHub Actions
AWS CDK 1.18.0(2019.12.03時点最新)
AWS Lambda
AWS APIGateway
Go

## ディレクトリ構造
```
- .github
  |
   - workflows
     | 
      - actions.yml 
- bin
  |
   - main(goのバイナリができる予定のdir)
- cdk
  |
   - cdk initした資材群（省略）
- src
  |
   - main.go
- go.mod
- .gitignore
```

なお、今回は各スキルセットにはあまり深入りせずサクサク実装していこうと思います（~~なんせ自分もまだよくわかってない~~）

今回の成果物は[こちら](https://github.com/ohanamisan/auto-deploy-sample-01)になります。

## Go

GoのLambdaを作っていきます。
今回は`GitHub ActionsとAWS CDKを使って自動デプロイする`がメインなお題なのでここはシンプルにHello WorldなLambdaで勘弁カツオです。
ギリギリ補足するとしたらGoでLambda書くときは`lambda.Start(func)`しましょうというあたりでしょうか。

```go:勘弁カツオなLambda
package main
 
import (   
  "fmt"
  "github.com/aws/aws-lambda-go/lambda"
)
 
func hello() (events.APIGatewayProxyResponse, error) {
	return events.APIGatewayProxyResponse{
		StatusCode: 200,
		Body:       "Hello Katsuo",
	}, nil
}
 
func main() {
  lambda.Start(hello)
}
```

# CDK

今回は`AWS APIGateway`から`AWS Lambda`を発火するあるあるCDKを作成していきます。
CDKの環境構築な記事は沢山あるので今回は用意されている前提で進めます。

ディレクトリ構造にならってcdkディレクトリを作成しその中でinitします。
今回はtypescriptで実装していきます（~~というかこれしかやった事ない~~）

```bash
$ mkdir cdk
$ cd cdk
$ cdk init app --language=typescript
```

続いて必要なライブラリをインストールしておきます。
今回使うのは`@aws-cdk/aws-lambda` `@aws-cdk/aws-apigateway`この二つを使っていきます。

```bash
$ npm install --save @aws-cdk/aws-lambda @aws-cdk/aws-apigateway
```

さて実装していく中身は主に`lib`パッケージの中なのですがその他の`bin`, `test`パッケージを見ていただいてわかるように.tsファイルが`cdk.ts`というようになっているかと思います。
initしたディレクトリ名で名前解決されているような感じです。
このままだとカッコわりいという方はいい感じの名前に変更してください（今回はこのままいきます）

## cdk-stack.tsの実装

では実装です。`lib`ディレクトリの直下にある`***-stack.ts`(今回は`cdk-stack.ts`)にリソースを定義していきます。

```typescript:cdk-stack.ts
import cdk = require('@aws-cdk/core');
import lambda = require("@aws-cdk/aws-lambda")
import apigateway = require("@aws-cdk/aws-apigateway")
import {Duration} from "@aws-cdk/core";

export class CdkStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Lambda
    const lambdaHandler = new lambda.Function(this, "api", {
      code: lambda.Code.fromAsset("../bin"),
      handler: "main",
      runtime: lambda.Runtime.GO_1_X,
      timeout: Duration.seconds(10),
      environment: {
        AUTO_DEPLOY_TEST: "success"
      }
    });

    // API Gateway
    const api = new apigateway.RestApi(this, "auto-deploy-sample-api", {
      restApiName: "auto-deploy-sample-api"
    });

    const lambdaHandlerIntegration = new apigateway.LambdaIntegration(lambdaHandler, {proxy: true});
    api.root.addResource("auto-deploy-sample-api")
    api.root.addMethod("GET", lambdaHandlerIntegration)
    
  }
}

```

なんと不思議なことにインフラちんぷんかんぷんなアプリエンジニアな私でもソースコードになると途端に何がされているかが一目瞭然。
一応リソースごとに見ていきます。

### Lambda

| key              | value                 |
|:-----------------|----------------------:|
| code             |      デプロイするソースコード|
| handler          |  エントリーポイントとなる関数名|
| runtime          |                 使用言語|
| timeout          |     実行時のタイムアウト設定 |
| environment      |                環境変数 |

`code`は今回プロジェクト直下の`bin/main`にビルドされるものを参照する想定です。
ここでのソースは`lib`からではなく`cdk`から参照します。
大体これくらいが必須なものになってくるかと思います。用途によってはVPCの設定などもあるかと思いますがもちろんできますし、ドキュメントを見れば基本大丈夫な上、ソースコードの参照元を見れば使い方は大体わかるので積極的にIDEで参照ジャンプしましょう。

```typescript:VPCのところ
    readonly vpc?: ec2.IVpc;
    /**
     * Where to place the network interfaces within the VPC.
     *
     * Only used if 'vpc' is supplied. Note: internet access for Lambdas
     * requires a NAT gateway, so picking Public subnets is not allowed.
     *
     * @default - Private subnets.
     */
    readonly vpcSubnets?: ec2.SubnetSelection;
    /**
     * What security group to associate with the Lambda's network interfaces.
     *
     * Only used if 'vpc' is supplied.
     *
     * @default - If the function is placed within a VPC and a security group is
     * not specified, a dedicated security group will be created for this
     * function.
     */

```

### API Gateway
もはや解説するまでもないと思います。
lambdaのインテグレーションを定義し作成したapigateのResourceとMethodにaddするだけで紐づけられます。簡単便利。


## cdk.tsの実装

ここにはAWSのアカウント情報とリージョンを定義していきたいのですがソースコードに直接書き込んでしまうのはいささかナンセンスです。
そこで`cdk.json`を使います。`cdk init`した際に作成されているファイルで内部に`context`というキーと更にキーバリューを定義しておくことで`.ts`ファイルから`app.node.tryGetContext("キー名")`でバリューを取り出すことがきます。
実際には以下のようになります。

```json:cdk.json
{
  "context": {
    "account": "YOUR_AWS_ACCOUNT",
    "region": "YOUR_AWS_REGION"
  },
  "app": "npx ts-node bin/cdk.ts"
}
```

```typescript:cdk.ts
#!/usr/bin/env node
import 'source-map-support/register';
import {CdkStack} from '../lib/cdk-stack';
import cdk = require('@aws-cdk/core');

const app = new cdk.App();
new CdkStack(app, 'CdkStack',
    {
        env: {
            account: app.node.tryGetContext("account"),
            region: app.node.tryGetContext("region")
        }
    });
```

ここまで実装したら一旦`cdk bootstrap`しておきます。
これをやり忘れると自動デプロイの際にどこにデプロイすれば良いのかわからなくなり怒られます。

```bash
$ cdk bootstrap 　　　// --profile *** <- 必要ならつけましょう
```

# GitHub Actions

あと一歩で自動デプロイです。
最後にGitHub Actionsにタスクを登録していきます。


## AWS access情報周りを登録
 
タスクの登録の前に予めSettingsタブのSecretsにAWSのアクセスキー情報などを登録しておきます。プロジェクトなんかでやるならマシンユーザー的なものでやるのが良いのでしょうかね。

<img width="1091" alt="accesskey.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227898/2e8e34c0-c997-6c78-6199-c5badabcb890.png">

下記のように`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`みたく登録できればおけーです。

<img width="1103" alt="addkeys.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227898/7c26d8fa-92d7-3183-9c3d-29cb79364db7.png">

## actions本題

ではここからactionsを作成していきます。
まずはリポジトリの真ん中あたりの`Actions`タブを選択します。

<img width="1046" alt="repository.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227898/0bc5f931-748b-6780-8fa9-f7fb540b6cb1.png">

actionsタブをクリックするとあらかじめ用意されたactionが沢山出てきますが残念ながらCDKは用意されていません。

<img width="1394" alt="actions-list.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/227898/02ac5489-ea05-d86f-01fb-0bd0531781da.png">


[こんなようなもの](https://github.com/marketplace/actions/aws-cdk-action)もありましたがタスクの途中でこけてしまい解決もできず...
なのでactionのフロー上で自分でCDKをUbunts環境に用意しデプロイをしてみました。
`aws-cli`が必要になるのは`python`, `cdk`は`node`, そして今回の`go`のビルド環境をあらかじめ用意されているactionからコピペしながら作成したものが以下の通りです。
自分で追加したのはpipのupgradeと`aws-cli`, `cdk`, `typescript`のインストール、goのビルドコマンドくらいです。

```yaml:actions.yml
name: CI

on:
  push:
    branches:
      - master

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    
    # python
    - name: Set up Python 3.7
      uses: actions/setup-python@v1
      with:
        python-version: 3.7
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
    
    # node.js
    - name: Use Node.js 10.x
      uses: actions/setup-node@v1
      with:
        node-version: 10.x

    # go
    - name: Set up Go 1.13
      uses: actions/setup-go@v1
      with:
        go-version: 1.13
      id: go
      
    # aws cli & cdk
    - name: aws cli install
      run: |
        pip install awscli --upgrade --user    
        npm install -g aws-cdk
    
    - name: build and deploy
      env:
        AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
        AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      run: |
        GOOS=linux GOARCH=amd64 go build -o ./bin/main ./src
        cd cdk
        npm install --save typescript
        npx cdk deploy --require-approval "never"

```

`cdk`のデプロイ時に`--require-approval "never"`をつけているのはCloudFormationの変更内容に問題ないかのチェックが入らず、問答無用でデプロイするためです。

# 終わりに

これでめでたく`master`リポジトリにマージされると自動でAWSにCDKで定義されたスタックがデプロイされるようになりました。
いちいち実装が終わったらローカルでCDKコマンドを打つ必要もなければ開発ユーザーの権限ではAWSのリソースをいじれないようにしマシンユーザーからだけLambdaの更新ができるようになればプロジェクトはよりセキュアでGitでソースは管理され健康的になります。

果たしてGitHub Actionsの使い方としてこれはあってるのか疑問はありますが何となく「こんなことができる」感はわかったのでこれからまたキャッチアップしていきます。

本当はCodePipeLineとか使うとより良いのでしょうが今日はここまで。

明日は@Black-Spiderさんの記事です！