AWS LambdaのCI・CD環境を構築してみた

この記事は [レコチョク Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku) の18日目の記事となります。
## はじめに
こんにちは、株式会社レコチョク新卒1年目エンジニアの笹野です。
サーバーサイドエンジニアとして、主に[dヒッツ](https://dhits.docomo.ne.jp/ft/sys00373)というサービスに携わっています。

好きな音楽はK-POPとヴィジュアル系全般で、おすすめアーティストはENHYPENとΛrlequiΩ(アルルカン)です！
好きなAWSのサービスはLambdaで、プライベートで作成したAPIはよくLambdaに置いて使っています。

今回は、そんな大好きなLambdaの開発体験を向上させるべくCI・CD環境を構築してみました。

## CI・CDとは？
- CI(Continuous Integration)
	- CIとは継続的インテグレーションのことで、ビルドとテストを自動化します。
- CD(Continuous DeliveryもしくはContinuous Deploy)
	- CDとは継続的デリバリーのことで、リリース作業を自動化します。

CI・CD環境を構築すると、生産性、開発速度、コード品質の向上といったたくさんの嬉しいことがあります。

## 今回使用するAWSについて
- AWS Lambda
	- Lambdaは関数単位でコードを実行できるサービスであり、サーバレス環境で動きます。ちなみに内部的にはAWS側のサーバに実行用のコンテナを作成→そのコンテナで関数を実行→実行が終了してしばらく経った後に実行用のコンテナを削除という流れでサーバレスを実現しているそうです。
- AWS CodeCommit
	- ソース管理サービスです。今回はLambda関数のGitリポジトリとして使います。
- AWS CodeBuild
	- ソースコードのコンパイル、テスト、デプロイを自動化するサービスです。
- AWS CodePipeline
	- デプロイまでの作業を可視化と自動化するサービスです。CodeCommitとCodeBuildを一連のプロセスとして管理してくれます。
- Amazon Simple Storage Service (Amazon S3)
	- ストレージサービスです。今回はビルドしたパッケージファイルを一時保存する場所として使います。
- AWS CloudFormation
	- AWSリソースを自動で構築するサービスです。Lambda関数の作成と更新をするために使います。

## わかりやすい図
![Pasted image 20231204014912.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/83bc3ec5-c133-5896-b384-be23d7af27cb.png)
1. CodeCommitにPushされたコードの変更をCodePipelineで検知
2. CodeBuildでテストとビルド
3. CloudFormationでLambdaにデプロイ

という流れになるよう構築していきます。

## 構築手順
1. CodeCommitでリポジトリを作る
2. CodePipelineを作成する
	- CodeBuildでCI環境を作る
	- CloudFormationでCD環境を作る

## CodeCommitでリポジトリを作る
### リポジトリを作る
まずはAWSコンソールで「CodeCommit」と検索し、リポジトリを作成します。
適宜リポジトリ名を入力し、作成ボタンを押下でリポジトリの作成は完了です！
![Pasted image 20231204052846.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/d8b16ee5-d4bb-a976-8603-a42035b99ded.png)

### リポジトリをクローンする
クローンするには、Git認証情報が必要なのでIAMで設定していきます。
AWSコンソールで「IAM」と検索し、現在ログインしているユーザを選択します。
セキュリティ認証情報を開き、認証情報を生成ボタンを押下するとクローンに必要なGit認証情報が作成されます。
![Pasted image 20231128122950.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/856aebbf-4721-5f4d-893a-f2bd96849192.png)
表示されたユーザ名とパスワードは忘れずダウンロードするかメモっておきましょう。

そして、接続のステップに表示されているクローンコマンドを実行します。
![Pasted image 20231205112750.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/0cff5486-9dfb-bad1-1c7b-3fc3834f85b8.png)

```
git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/test-sasano-lambda-cicd-rep
```
適宜Git認証情報を入力し、リポジトリのクローンは完了です！

## CodePipelineを作る
作成したリポジトリを絡めて、CodePipelineを作成していきます。
AWSコンソールで「CodePipeline」と検索し、パイプラインを作成します。
パイプライン名を入力し、パイプラインタイプは一旦V1を選択してください(あとから変更可)。そして、今回は新しいサービスロールを作成します。
![Pasted image 20231204054834.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/5c3c0c2b-c8a3-a3f4-8fe4-8e3a53cb9058.png)

次に、ソースプロバイダーはCodeCommitを選択し、リポジトリ名には作成したリポジトリ名を入力してください。
![Pasted image 20231204054910.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/84990fff-3727-4aba-8de6-b8f86aeae481.png)

## CodeBuildでCI環境を作る
先程の画面で次へを押下するとビルドステージの追加ステップに移ります。
プロバイダーはCodeBuildを選択します。そして、プロジェクトを作成するを押下し、CodeBuildを作っていきます。
![Pasted image 20231204032000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/bf3b32c2-5f12-5d38-404f-7d6a43ad82ad.png)

### CodeBuildを作る
適宜、好きなプロジェクト名を入力してください。
![Pasted image 20231204055008.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/863ea196-6695-3075-f6ab-faf7798c820f.png)

次に環境はLambdaを選択し、作成するLambdaの環境を選択してください。
今回は`Python3.11`、`x86_64`のLambdaを作成します。ここでも新しいサービスロールを作成します。
![Pasted image 20231204055106.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/1b155f73-79f5-892c-a73d-206ba6ab6f4c.png)

buildspecファイルを使用するを選択して、CodeBuildの作成は完了です。
![Pasted image 20231204055130.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/416fc786-1a62-811f-9246-e6445b8313a7.png)

CodePipelineの作成に戻りますが、一旦デプロイの設定はスキップし、CodePipelineを作っちゃいます。

CodePipelineが動きますが、一度もリポジトリにPushしていないのでSourceは失敗でOKです。
![Pasted image 20231204055345.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/21b61429-9476-2efa-5a02-2132f9470b60.png)

### Lambda関数を作る
メインとなるソースコードを準備します。
今回は渡された2つの数値の合計を返す関数とそのテストをPythonで簡単に作りました。
```python:lambda_function.py
def lambda_handler(event, context):

	return {
		"result": (event["first_num"] + event["second_num"])
	}
```

```python:test_lambda.py
import pytest
from lambda_function import lambda_handler

def test_lambda_handler():
	event = {
		"first_num": 3,
		"second_num": 2
	}

	result = lambda_handler(event, {})
	assert (result["result"] == 5)
```

### buildspec.ymlを作る
CodeBuildでビルドするために、buildspecを作成します。
```:buildspec.yml
version: 0.2

phases:
	install:
		runtime-versions:
			python: 3.11
		commands:
			- pip install pytest

	pre_build:
		commands:
			- python -m pytest test_lambda.py

	build:
		commands:
			- sam package --template-file template.yml --s3-bucket codepipeline-ap-northeast-1-621537184721 --output-template-file package.yml
	
	artifacts:
		files:
			- package.yml
```
`install`では使用するライブラリをインストールします。もちろん`requirements.txt`を使ったインストールもできます。
`pre_build`ではテストを実行します。テストが通らなかった場合、ちゃんとビルド作業が止まります。
`build`では`sam package`コマンドと`template.yml`を使ってパッケージングしたものをS3に保存します。指定するS3はCodePipelineの作成と同時に作られたバケットでも良いです。その場合、バケットポリシーで暗号化されていないオブジェクトのアップロードを拒否する権限を削除する必要があることに気をつけてください。
```:template.yml
AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Resources:
	Function:
		Type: "AWS::Serverless::Function"
		Properties:
			FunctionName: calcSum
			Handler: lambda_function.lambda_handler
			Runtime: python3.11
			CodeUri: .
```
このテンプレートは、`calcSum`というLambdaを作成(すでに存在していれば更新)するテンプレートになっています。Python3.11をランタイムとし、ルートディレクトリにある`lambda_function`ファイルの`lambda_handler`関数のLambdaを作るというものです。

これらファイルたちをPushしてみましょう。
![Pasted image 20231204060443.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/5cb25148-ace1-cf2d-77ee-78caf144fbc0.png)

無事にビルドフェーズまで成功しました！

## CloudFormationでCD環境を作る
作成したパイプラインを編集してデプロイフェーズを追加していきます。
まず、編集するボタンを押下して編集画面を開きます。
![Pasted image 20231204060630.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/cb158d4f-74a4-4871-c987-e10999967fed.png)

次にBuildの下のステージを追加するボタンを押下します。
![Pasted image 20231204060836.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/8cd92b3b-9fe1-60ce-17f8-d836b050d86b.png)

ステージ名はDeployにしましょう。
![Pasted image 20231204060907.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/420b238c-7219-6371-c34e-f5ec90f2abdc.png)

無事にDeployステージが追加されました。
次に、アクショングループを追加するボタンを押下…と行きたいところですが、
その前にCloudFormation用のロールを新たに作成しておきましょう。
IAMからロールを作成します。サービスはCloudFormationを選択します。
![Pasted image 20231204061406.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/75f75f02-4abb-e610-2c33-3742bf11d372.png)

取り急ぎ、CloudFormationのFullAccess権限をアタッチします。
![Pasted image 20231204061548.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/19cd8749-de3c-5e20-e28b-e856ec342a47.png)

それに加え、下記の操作も許可してください。
```
iam:CreateRole
iam:DetachRolePolicy
iam:DeleteRole
iam:AttachRolePolicy
iam:GetRole
iam:PassRole
lambda:GetFunction
lambda:CreateFunction
lambda:DeleteFunction
lambda:TagResource
lambda:UpdateFunctionCode
lambda:ListTags
s3:GetObject
```
これらはLambdaのデプロイに必要な操作になるため許可が必要です。

適宜ロール名を入力し、CloudFormation用のポリシーは完成です！
![Pasted image 20231204061519.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/14a26cf9-ce7e-0fb6-66b0-27b8d8f62ab8.png)

CloudFormation用のポリシーができたところで、アクショングループを追加していきます。
![Pasted image 20231204061011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/f9d3e3d2-f039-84a0-a3b1-4031d331474d.png)

まずは変更セットを作成または更新するアクションを追加します。
アクション名をCreateUpdateChangeSetとし、アクションプロバイダーはCloudFormationを選択、入力アーティファクトはBuildArtifactを選択します。
![Pasted image 20231204062135.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/de249704-7491-22a0-11f1-4faabb5f8730.png)

アクションモードは変更セットを選択または交換するを選択し、適宜スタック名とセット名を入力します。テンプレートはBuildArtifactの`package.yml`を選択します。そして、このアクションはIAMリソースの作成があり得るのでCAPABILITY_IAMを追加します。ロールは先程作成したCloudFormation用のロールを選択します。
![Pasted image 20231204062124.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/0bdfa19d-60bf-8c08-b90b-9ec96869e519.png)

できた変更セットを実行するため、もう一つアクションを追加します。
アクション名はExecuteChangeSetとし、アクションプロバイダーはCloudFormationを選択します。入力アーティファクトはBuildArtifactを選択し、アクションモードは変更セットを実行するを選択します。
![Pasted image 20231204062240.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/3b6002e3-12ec-ac58-8d56-513b3ac5aa57.png)

これで一連の流れが出来上がりました！パイプラインの変更をリリースするボタンを押下して動きを確かめてみましょう。
![Pasted image 20231204062312.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/4df0add4-90ec-69e9-98aa-8479a7742eed.png)

無事、デプロイまで成功しました！
![Pasted image 20231204062543.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/e30dee4e-7dda-7aae-0f89-eae73b68976d.png)

Lambdaを見てみると新たにLambda関数が作成されていることが確認できます。

では、試しにPythonのコードを下記に変更してPushしてみましょう。(ステータスコードを追加しています)
```python:lambda_function.py
def lambda_handler(event, context):

    return {
        "statusCode": 200,
        "result": (event["first_num"] + event["second_num"])
    }
```

Push後、無事にPipelineの流れはすべて成功し、Lambdaのコードが更新されていることまで確認できました！
![Pasted image 20231204063610.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3620200/1ff7f1df-3b45-21d9-50c3-5cec36821ce5.png)

**これでLambdaのCI・CD環境は完成です！**
## おわりに
本記事ではLambdaのCI・CD環境を構築しました。
サーバレスのCI・CD環境といっても、特別な工程が必要になったりせず、シンプルな構成でCI・CDを実現できました。今後はLambda関数のデプロイを検証環境と本番環境とで分けたり、API Gatewayと絡めたりしてみたいと思いました。これらについては`template.yml`の追記でなんとかなりそうな気がしたので、これから試しつつ作ってみよう思います。

最後まで読んでいただきありがとうございました！

明日のレコチョク [Advent Calendar 2023](https://qiita.com/advent-calendar/2023/recochoku)は19日目 「バイナリを読みながらJPEG画像が壊れた原因を探る」となります。お楽しみに！

## 参考文献
以下の情報を参考にさせていただきました！
- [【aws】CodeCommitにpushするだけでlambda関数をデプロイするパイプラインを作る【CodePipeline】](https://ishidalog.com/?p=475)
- [CodePipelineでLambdaのデプロイをしようとして躓いた話](https://speakerdeck.com/ysd_marrrr/codepipelinedelambdafalsedepuroiwosiyoutositezhi-itahua)
- [AWS の SAM テンプレートを作成します - AWS CodeDeploy](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/tutorial-lambda-sam-template.html)

--- 

この記事はレコチョクのエンジニアブログの記事を転載したものとなります。

https://techblog.recochoku.jp/9984