AWS SAMをCodeBuildでCICDする

AWS SAMで開発したコードをGitHubのmainにpushしたタイミングでCodeBuildを使いCICDすると言う事をやってみたので、備忘録を兼ねて手順を残したいと思います。

題材としては`sam init`で作れるNode.jsの**Hello World Example**を使います。

ざっくりしたイメージです↓
![architect.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/3479b008-b653-fe29-b1d7-a220d7cb95d6.jpeg)

**環境**

OS: macOS Catalina(10.15.7)
SAM CLI: 1.15.0


# やる事

1. hello worldプロジェクトを作成
2. buildspec.ymlを作成
3. githubへpushする
4. CloudFormation用のS3バケットを作成
5. CodeBuild用のIAMロールを作成
6. CodeBuildの設定 
7. ビルドする

**SAMの話なのになぜCloudFormationの話が出るのか？**

CodeBuildの環境にSAM CLIが入っていないので、SAMテンプレートをCodeBuild上でCloudFormationのテンプレートに変換してリソースの作成・デプロイをする事をここでは考えているからです。

SAM CLIをインストールする事も出来ますが、SAMプロジェクトのビルド・デプロイは、CodeBuildの環境にデフォルトで入っているAWS CLIのコマンドで代替出来るので、代替する方法で進めいたいと思います。

# 1. hello worldプロジェクトを作成

1-1. `sam init`コマンドでプロジェクト作成
1-2. template.yamlがパラメーターを受け取れるように改造

1-2.については、普段`sam deploy`する際にパラメーターを渡す事があると思います。CodeBuildでCICDする場合、どう渡せば良いのかを試してみます。

ここでは、prd・stg・devなど環境名をパラメーターで受け取り、その環境名をLambda関数の名前に接尾辞として付けると言う事をしようと思います（例：hello-world-**prd**、hello-world-**stg**）。

## 1-1. `sam init`コマンドでプロジェクト作成

nodejsのプロジェクトでtemplateでは「1 - Hello World Example」を選びます。

```
[~/Desktop]$ sam init
Which template source would you like to use?
	1 - AWS Quick Start Templates
	2 - Custom Template Location
Choice: 1
What package type would you like to use?
	1 - Zip (artifact is a zip uploaded to S3)	
	2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 1

Which runtime would you like to use?
	1 - nodejs12.x
	2 - python3.8
	3 - ruby2.7
	4 - go1.x
	5 - java11
	6 - dotnetcore3.1
	7 - nodejs10.x
	8 - python3.7
	9 - python3.6
	10 - python2.7
	11 - ruby2.5
	12 - java8.al2
	13 - java8
	14 - dotnetcore2.1
Runtime: 1

Project name [sam-app]: hello-world-app     

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

AWS quick start application templates:
	1 - Hello World Example
	2 - Step Functions Sample App (Stock Trader)
	3 - Quick Start: From Scratch
	4 - Quick Start: Scheduled Events
	5 - Quick Start: S3
	6 - Quick Start: SNS
	7 - Quick Start: SQS
	8 - Quick Start: App Backend using TypeScript
	9 - Quick Start: Web Backend
Template selection: 1

    -----------------------
    Generating application:
    -----------------------
    Name: hello-world-app
    Runtime: nodejs12.x
    Dependency Manager: npm
    Application Template: hello-world
    Output Directory: .
    
    Next steps can be found in the README file at ./hello-world-app/README.md
```

## 1-2. template.yamlがパラメーターを受け取れるように改造

繰り返しになりますが、prd・stg・devなど環境名をパラメーターで受け取り、その環境名をLambda関数の名前に接尾辞として付けると言う事をしようと思います（例：hello-world-**prd**、hello-world-**stg**）。

**template.yaml**を下記と置き換えます。

**（↓デフォルトのテンプレートからの変更点）**

・Globalsの項目の次にParametersを追加
・Parametersで受け取った値を関数名にくっつける

```yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  hello-world-app

  Sample SAM Template for hello-world-app
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
Parameters:
  Env: # prd: 本番、stg: ステージング、dev: 開発
    Type: String
    AllowedValues:
      - prd
      - stg
      - dev
    Default: dev

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: 
        !Join
          - ''
          - - 'hello-world'
            - '-'
            - !Ref Env
      CodeUri: hello-world/
      Handler: app.lambdaHandler
      Runtime: nodejs12.x
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
```

# 2. buildspec.ymlを作成

CodeBuildに実行して欲しいコマンドをbuildspec.ymlに記述します。

CodeBuild上で`sam build`、`sam deplly`コマンドを実行させたいところですが、CodeBuildの環境にはSAM CLIがインストールされていません。

勿論、SAM CLIをダウンロード・インストールしてSAMコマンドを使えるようにすると言うことも出来ますが、今回はより手軽な手段を取りたいと思います。

CodeBuildのビルド環境にはデフォルトでAWS CLIが入っているので、これで代替します。

具体的には、SAMのテンプレートをCloudFormationのテンプレートに変換、そしてデプロイします。

それらをbuildspec.ymlに記述します。

プロジェクト直下に`buildspec.yml`を作成して下記を貼り付けます。

```yaml
version: 0.2

phases:
  build:
    commands:
      - echo 'Building...'
      - aws cloudformation package --template-file template.yaml --s3-bucket $S3_BUCKET --output-template-file output.yml
      - aws cloudformation deploy --template-file output.yml --s3-bucket $S3_BUCKET --stack-name $STACKNAME --capabilities CAPABILITY_IAM --region $REGION --parameter-overrides Env=$ENV
      - aws cloudformation describe-stacks --stack-name $STACKNAME
```

ディレクトリ構成は↓な感じです。

![スクリーンショット 2021-05-26 19.55.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/39fe4d0d-fb21-4b5d-fa67-0b92284853db.png)

### ※補足

#### 環境変数

後ほど、CodeBuildの管理画面上で設定します。

#### aws cloudformation packageは何してるの？

- SAMテンプレートからCloudFormationのテンプレートを作成する
    - `--template-file`
        - SAMテンプレートを指定。これを元にCFテンプレートを作成します。
    - `--output-template-file`
        - ここではファイル名をoutput.ymlとして、CFのテンプレートを作成します。
    - `--s3-bucket`
        - hello-world配下のLambdaを固めてこのバケットにアップロードします。CloudFormationでLambdaをデプロイする際にLambdaのコードをzipで固めてS3にアップロードする作業と同等です。

#### aws cloudformation deployは何してるの？

- CloudFormationスタックを作成します
    - `--template-file`
        - output.ymlをCFテンプレートとして使用します。
    - `--s3-bucket`
        - テンプレートをアップロードする場所。
    - `-stack-name`
        - CFのスタック名
    - `--parameter-overrides`
        - SAMテンプレートに渡すパラメーター。Envとして環境変数\$Envを渡しています。環境変数は後ほど設定します。

#### aws cloudformation describe-stacksは何している？

CloudFormationで作成したリソースを出力します。出力はCodeBuildの画面上で確認出来ます。このコマンドは無くても大丈夫なのですが、テンプレートから作成されるAPIGatewayのURLを確認するのに便利なので記述していいます。

# 3. githubへpushする

GitHubにリポジトリを作りpushします。**masterブランチをmainブランチにリネームした上で**、mainブランチにpushします。

リネームする理由は、GitHub上でリポジトリを作った際にリネームするように言われるので従っているだけです（ご存知の方が多いと思いますが、masterが奴隷制度を連想させる言葉なのでmainに置き換える流れが主流なようです）。

![スクリーンショット 2021-05-26 19.59.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/33aee251-1a72-9879-6ac0-4f368a5b2a58.png)

# 4. CloudFormation用のS3バケットを作成

CodeBuildでCloudFormationのスタックを作りますが、その際に、S3にCloudFormationのテンプレートをアップロードして置く必要があります。そのためのバケットを作成します。

buildspec.ymlのコマンド中で`--s3-bucket $S3_BUCKET`と言う部分で使います。後ほど環境変数に、ここで作成したS3バケット名を設定します。

バケット名は適宜置き換えてください（世界中で一意なので）。バケット名の設定以外はすべてデフォルトでOKです。

![スクリーンショット 2021-05-26 20.02.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/c4c7bfd8-7756-f47e-bafd-cac8cd632c47.png)

# 5. CodeBuild用のIAMロールを作成

CodeBuildにさせようと思っている事を許可するロールを作成します。

IAMのページからIAMロールを作成します。

ユースケースとして「CodeBuild」を選択。必要なポリシーをアタッチして行きます。

![スクリーンショット 2021-05-26 20.05.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/af1a824c-c0f8-f1e2-764a-a3f79cf53880.png)

今回は下記ポリシーをくっつけました。

ポリシーの割当が苦手なので、結構強い感じにしてしまっています。適宜いい感じにして貰えたらと。。。

- AmazonS3FullAccess
    - CodeBuildからS3にテンプレートをアップするなど操作するため
- AmazonCloudFormationFullAccess
    - CloudFormationスタックを作成するため
- AWSLambda_FullAccess
    - CloudFormationでLambdaを作成するため
- AmazonAPIGatewayAdministrator
   - CloudFormationでAPIGatewayを作成するため
- CloudWatchFullAccess
    - CodeBuildのビルドの状況などCloudWatchにログを残すため
- IAMFullAccess
    - LambdaにAPIGatewayからのキックを許可するのロールを作成するため


![ロール.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/beaa723e-3147-e591-f2b3-d0278d08ae47.jpeg)


# 6. CodeBuildの設定

マネコンからCoceBuildに移動して、画面右上の「ビルドプロジェクトを作成する」をクリックします。

![ビルド1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/0e8c4fc8-0f63-0efa-0464-ff538422982e.jpeg)

### プロジェクトの設定・ソース

CodeBuildとGitHubを連携させる場合、初回はアカウントの紐付け作業があります。画面に従えば問題なく出来ると思います。

![ビルド2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/582c7c1c-9003-eafb-c678-e58da06ce5fd.jpeg)


### プライマリソースのウェブフックイベント

チェックを入れると画面が展開されます。

GitHubのmainにpushされるとCodeBuildが走るように設定します。

![ビルド3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/6e8d0162-d3bf-30ad-f274-81e0b798728c.png)


### 環境

**オペレーティングシステム**  …  Amazon Linux 2
**ランタイム**  …  Standard
**イメージ**  …  セレクトボックスの中から一番新しいバージョンを選択します（一番下）。
**イメージのバージョン**  …  セレクトボックスの中から一番新しいバージョンを選択します（一番下）。
**サービスロール**  …　既存のサービスロール
**ロールのARN**  …  先ほど作成したロールを選択

![環境.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/41a82928-9e4a-ba11-168d-2afe936ccebd.jpeg)




↑の「▶︎追加設定」をクリックして画面を展開します。

![環境変数.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/d223fc19-d07e-c05d-b651-b5795d9a08e9.jpeg)

↑の画面で環境変数を設定します。

**S3_BUCKET**  … 先ほど作成したS3バケット名
**STACKNAME** … hello-world-app
**REGION** … ap-northeast-1
**ENV** … prd

### Buildspec・バッチ設定・アーティファクト・ログ

デフォルトのままで大丈夫です。

ページを一番下までスクロールして「ビルドプロジェクトを作成する」をクリックします。

# 7. ビルドする

画面右上の「ビルドを開始」をクリックします。

![ビルド作成.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/46412916-fda4-68b6-7dc3-6da93dc3c94a.jpeg)


ステータスが進行中に変わります。

![進行中.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/d63424a1-4c0d-bc9b-6244-ac25ecea2453.jpeg)


ステータスが成功に変わったらビルドが終了です。

![ビルド成功.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/aacf77d5-ffd3-e004-887f-ea94769bab11.jpeg)

↑の図で赤枠で囲んだ箇所に記載されたAPIGatewayのURLにアクセスします。
"hello world"と表示されたらビルド成功です。

![スクリーンショット 2021-05-28 20.10.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/56de89bc-0e86-099b-8e15-e298d56ef6e6.png)

GitHubとの連携も確認します。

`hello-world/app.js`を下記と置き換えます。

```js
let response;
exports.lambdaHandler = async (event, context) => {
    try {
        // const ret = await axios(url);
        response = {
            'statusCode': 200,
            'body': JSON.stringify({
                message: 'hello codebuild',
                // location: ret.data.trim()
            })
        }
    } catch (err) {
        console.log(err);
        return err;
    }

    return response
};
```

GitHubにpushします。

CodeBuildが自動で走ります。

![更新.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/66cb247b-3418-8967-07b8-19b465f64d3a.jpeg)

「進行中」が「成功」と変わるのを待ちAPIGatewayを叩くと、、、

![スクリーンショット 2021-05-28 20.17.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349281/6c90b2ab-17f3-2e5e-cf26-e078a0623c54.png)

Lambdaが更新された事を確認出来ました。

# 終わりに

モチベーションとしては、複数環境（prd、stg、devなど）を切り替えながら使う事を想定した場合で、環境ごとに違うパラメーターを渡すなどしたい時に、都度、手動で渡すと言う煩わしさを回避出来る点が大きいかなー、と思いました。

tomlを複数用意する事でも運用は楽になりますが、個人的にはCodeBuildの環境変数に任せる方が楽だと感じました。

以上です。どなたかのお役に少しでも立つ事が出来たならば幸いです。