【AWS】CircleCIを使ってコンテナイメージをAWS LambdaへデプロイするCI/CD環境を構築してみた

:::note info
この記事は、AWS Lambda と Serverless Advent Calendar 2021 16日目の投稿です。
:::

https://qiita.com/advent-calendar/2021/lambda

# 1.はじめに
2020年12月にAWS Lambda はコンテナイメージのサポート開始を発表しました。
ECSやEKSといったコンテナ系のサービスだけでなく、Lambdaでもコンテナネイティブな開発環境を作ることができます。

これまでのLambda関数のデプロイ方法は下記の通りです。

- マネージメントコンソールからソースコードを編集する
- ZIPファイルをアップロードする
- Amazon S3に配置したZIPファイルをデプロイする

上記の3つに加えてECRに配置したコンテナイメージをデプロイする方式が利用可能になりましたので、
こちらを利用した開発ワークフローをCI/CD環境をCircleCIで構築してみました。

---

# 2.本記事の対象者
 -  CircleCIを利用したい方
 -  コンテナイメージをLambdaにデプロイしたい方

---

# 3.CircleCIとは
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2295139/97c71d13-8cec-4aae-89ae-f042333bb03d.png)

---

# 4.AWS Lambdaにデプロイするコンテナイメージ
GO言語で作成したAPIとなります。本記事はこちらのコンテナイメージを利用しています。

https://github.com/KEMPER0530/go-lambda-api-demo

- 開発したもの
    - DynamoDBへ接続をサーバレスで実装
- APIエンドポイント
    - https://*************.execute-api.ap-northeast-1.amazonaws.com/dev/V1/<コマンド>
- GETリクエスト
 
| コマンド  | パラメータ        | 説明    |
| --- | ------------------ | --- |
| actuator-health   | なし           | DynamoDBへ接続し値が返却されるかどうかを確認し、<br>接続有無をJSON形式で通知    |


---

# 5.CI/CD構成
![circleci_gaiyou.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/999cb41b-7094-164c-936b-2d2cfce88b23.png)

①GitHubへpush
②指定のリポジトリにpushされたことをトリガーにして、CircleCIを起動します。
③リポジトリ内のconfig.ymlの構成に従い、コンテナイメージを作成後にECRにPush
④Lambdaの更新・作成
⑤③でPushしたイメージを取得する

---

# 6.IAMユーザの作成
CircleCIからAWSのリソースを扱うためには、CircleCI用のIAMユーザーを作成・権限付与をし、アクセスキーを払い出す必要があります。ロールは以下の2つを付与します。

- AmazonEC2ContainerRegistryFullAccess
- AWSLambda_FullAccess

作成したIAMユーザーのアクセスキーとシークレットアクセスキーはCircleCIの環境変数へ後ほど使うので、忘れないようにどこかに控えておきます。

---

# 7.CI/CDのセットアップ
CI/CDに必要なファイルを作成していきます。

``` 
 （プロジェクトルートディレクトリ）/
     └── src
     │    └── ・・・・        # サンプルプログラム
     └── Dockerfile　　　　　 # コンテナイメージ作成用ファイル
     └── .circleci
          └── config.yml　　　# CircleCIの設定ファイル
```

---

## 7-1.Dockerfileの作成
Dockerファイルの設定は下記の通りです。

``` Dockerfile
# FROM でベースのコンテナイメージを指定
FROM golang:1.17.2-alpine
MAINTAINER kemper0530

ENV GOPATH /go
ENV PATH=$PATH:$GOPATH/src

# COPYでコンテナイメージにバンドルするファイルを指定
ENV PATH=$PATH:$GOPATH/src/github.com/kemper0530/go-lambda-api-demo/src
WORKDIR $GOPATH/src/github.com/kemper0530/go-lambda-api-demo
COPY  /src $GOPATH/src/github.com/kemper0530/go-lambda-api-demo/src
# モジュールの依存関係を作成
RUN go mod init go-lambda-api-demo
RUN go mod tidy
# ビルド
RUN GOOS=linux go build -o go-lambda-api-demo ./src
# ENTRYPOINTでLambda Functionのエントリポイント(handler)を指定
ENTRYPOINT ["/go/src/github.com/kemper0530/go-lambda-api-demo/go-lambda-api-demo"]
```

---

## 7-2.config.ymlの作成
config.ymlファイルの設定は下記の通りです。
CircleCIのOrbsを利用するためバージョン2.1を指定しております。

```yml
version: 2.1
# Orbsの利用
orbs:
  aws-ecr: circleci/aws-ecr@6.15.3
  aws-cli: circleci/aws-cli@2.0
  go: circleci/go@1.7.0
# ワークフローの定義
workflows:
  version: 2
  test_and_build_and_deploy:
    jobs:
      - test
      - aws-ecr/build-and-push-image:
          region: AWS_REGION
          account-url: AWS_ECR_ACCOUNT_URL
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          create-repo: true
          requires:
            - test
          filters:
            branches:
              only: main
      - aws-lambda-deploy:
          requires:
            - aws-ecr/build-and-push-image
          filters:
            branches:
              only: main
# ジョブの定義
jobs:
  test:
    executor:
      name: go/default
      tag: '1.17'
    steps:
      - checkout
      - go/load-cache
      - go/mod-download
      - go/save-cache
      - go/test:
          packages: ./tests
          covermode: atomic
          failfast: true
          race: true
  aws-lambda-deploy:
    executor: aws-cli/default
    steps:
      - checkout
      - aws-cli/setup
      - run:
          name: Deploy Lambda
          command: |
            DIGEST=$(aws ecr list-images --repository-name ${AWS_RESOURCE_NAME_PREFIX} --out text --query "imageIds[?imageTag=='${AWS_ECR_IMAGE_TAG}'] | [0].imageDigest")
            if ! aws lambda get-function --region ${AWS_REGION} --function-name ${LAMBDA_FUNCTION} > /dev/null 2>&1; then
              aws lambda create-function \
               --function-name ${LAMBDA_FUNCTION} \
               --package-type Image \
               --code ImageUri=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${AWS_RESOURCE_NAME_PREFIX}@${DIGEST} \
               --region ${AWS_REGION} \
               --environment "Variables={GO_ENV="production",PORT=${PORT}}" \
               --role ${AWS_LAMBDA_ROLE}
            else
              aws lambda update-function-code \
               --function-name ${LAMBDA_FUNCTION} \
               --image-uri ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${AWS_RESOURCE_NAME_PREFIX}:${AWS_ECR_IMAGE_TAG} \
               --region ${AWS_REGION}
            fi


```

---

### 7-2-1.Orbsの宣言 
そもそも、CircleCIに限らずCI/CDで設定ファイルの中身は結局の所、「コマンドベース」で指定していく形となっていました。
そんな煩わしい定義を「パッケージ」としてまとめたのが[orbs](https://circleci.com/docs/ja/2.0/orb-intro/)です。

例えば、ECRへコンテナイメージをPushする場合は下記の作業が必要となります。

 1. docker buildコマンドでコンテナイメージを作る
 2. docker loginでECRにログインする
 3. docker tagで1で作ったコンテナイメージに、AWS指定の名前・タグ付けを行う
 4. 3のイメージをECRにdocker pushする

上記の作業に対して何のコマンドを実行するのかを定義しなくてはいけませんでした。
Orbsを利用することでconfig.ymlへの記載内容を大幅に簡素化できます。

今回は「[aws-ecr](https://circleci.com/developer/orbs/orb/circleci/aws-ecr)」と「[aws-cli](https://circleci.com/developer/orbs/orb/circleci/aws-cli)」と「[circleci/go](https://circleci.com/developer/ja/orbs/orb/circleci/go)」のOrbsを利用します。

```
orbs:
  aws-ecr: circleci/aws-ecr@6.15.3
  aws-cli: circleci/aws-cli@2.0
  go: circleci/go@1.7.0
```

---

### 7-2-2.テストの実施
Orbsを利用してgoのテストを実施します。

``` config.yml
    executor:
      name: go/default
      tag: '1.17'
    steps:
      - checkout
      - go/mod-download
      - go/test:
          packages: ./tests
```

---

### 7-2-3.ECRへのPUSH
Orbsを利用してコンテナイメージをECRへPUSHします。
『aws-ecr/build-and-push-image』と記載してパラメータを設定するだけで、
パラメータ内容に基づいてコンテナイメージ作成してECRへPUSHまでを実施してくれます。


```
...
      - aws-ecr/build-and-push-image:
          region: AWS_REGION
          account-url: AWS_ECR_ACCOUNT_URL
          repo: "${AWS_RESOURCE_NAME_PREFIX}"
          create-repo: true
...
```

 - reigion リージョン
 - account-url AmazonECRアカウントのURL(例：{アカウントID} .dkr.ecr.{リージョン}.amazonaws.com)
 - repo リポジトリ名
 - create-repo 対象リポジトリ名がなかったら新規作成するか



---

### 7-2-4.Lambdaへのデプロイ
Lambdaへのデプロイについてはaws-cliのOrbsを利用し、AWS-CLIを利用し、
肝心なデプロイ部分はコマンドを書いていきます。(ECR→LambdaへデプロイするOrbsがないため)

```
・・・
  aws-lambda-deploy:
    executor: aws-cli/default
    steps:
      - checkout
      - aws-cli/setup
      - run:
          name: Deploy Lambda
          command: |
            DIGEST=$(aws ecr list-images --repository-name ${AWS_RESOURCE_NAME_PREFIX} --out text --query "imageIds[?imageTag=='${AWS_ECR_IMAGE_TAG}'] | [0].imageDigest")
            if ! aws lambda get-function --region ${AWS_REGION} --function-name ${LAMBDA_FUNCTION} > /dev/null 2>&1; then
              aws lambda create-function \
               --function-name ${LAMBDA_FUNCTION} \
               --package-type Image \
               --code ImageUri=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${AWS_RESOURCE_NAME_PREFIX}@${DIGEST} \
               --region ${AWS_REGION} \
               --environment "Variables={DBMS=${DBMS},DB_NAME=${DB_NAME},DB_PASS=${DB_PASS},DB_PROTOCOL=${DB_PROTOCOL},DB_USER=${DB_USER},PORT=${PORT}}" \
               --role ${AWS_LAMBDA_ROLE}
            else
              aws lambda update-function-code \
               --function-name ${LAMBDA_FUNCTION} \
               --image-uri ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${AWS_RESOURCE_NAME_PREFIX}:${AWS_ECR_IMAGE_TAG} \
               --region ${AWS_REGION}
            fi
・・・
```

---

## 7-3.CircleCIの利用準備
下記へアクセスしログインを実施する
https://circleci.com/ja/vcs-authorize/

![スクリーンショット 2021-11-14 22.52.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/56e2ce5f-0e83-b2c5-868d-3788d04fb7c6.png)

---

対象のリポジトリを選択し「SetupProject」を押下
![スクリーンショット 2021-11-08 19.58.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/441544e7-92af-3397-3862-68c73c5adec8.png)

---

「Let's Go」を押下
 ![スクリーンショット 2021-11-08 20.20.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/69150d8d-b456-f59e-9edc-293559fed85c.png)

---

CircleCIの環境変数に設定します
![スクリーンショット 2021-11-16 21.38.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/0c16284c-39ab-18d7-39d4-933785f04731.png)

 - AWS_REGION:　リージョン(例:ap-northeast-1)
 - AWS_ECR_ACCOUNT_URL:　{アカウントID}.dkr.ecr.{リージョン}.amazonaws.com
 - AWS_RESOURCE_NAME_PREFIX： ECRのリポジトリ名
 - AWS_ACCESS_KEY_ID: アクセスキー
 - AWS_SECRET_ACCESS_KEY: シークレットキー
 - LAMBDA_FUNCTION: Lambdaの関数名
 - AWS_LAMBDA_ROLE: Lambdaで利用するIAMのロール
 - AWS_ACCOUNT_ID: AWSアカウントID

---

# 8.デモ
mainブランチにpushするとCircleCIが動き出す
![スクリーンショット 2021-11-14 17.08.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/bc584e9c-ad38-d43f-e843-4bb49b486ef8.png)
↓
完了
![スクリーンショット 2021-11-14 17.11.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/ad4235a4-a328-1f4d-e038-5f899ed9f697.png)
↓
ECRのマネージメントコンソール確認
 ![スクリーンショット 2021-11-14 17.14.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/17cb5782-da6b-a6d2-b344-6757f60345e7.png)
↓
Lambdaの関数を確認
![スクリーンショット 2021-11-14 17.12.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/7538f478-0d52-a095-e415-cc53050c5b97.png)
![スクリーンショット 2021-11-14 17.13.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/292397/7219c80c-5c65-fa67-e23f-4fa3348821a6.png)

---

# 9.まとめ
 - コンテナイメージをサポートしたことで、ECS、EKSと同じようなデプロイフローに乗せることができるようになる。(ECRに集めてそこからデプロイ)
 - 個人開発程度であれば、コスト面でもLambdaのコンテナイメージは非常に有効。
    - ECS,EKSは起動している分課金が発生するが、Lambdaはリクエスト100万件あたり0.20USD(東京リージョン)
 - CircleCI Orbsを利用することで設定ファイルの記述が簡素化できたので、設定ファイルの属人化が排除できると感じた。
 - 基本的にLambdaを利用する場合、Lambda以外のリソースも必要になるのでそれらを含めたデプロイを考える必要がある。
    - HTTPリクエストを受けたい場合はAPIGatewayが必要だし、Cronみたいに定期実行させたい場合にはEventBridgeが必要である。純粋にLambdaだけで済むケースはない。

---

# 参考
https://aws.amazon.com/jp/builders-flash/202103/new-lambda-container-development/?awsf.filter-name=*all