Go(gin)とLambda(serverless framework)でCICDする
はじめに
仕事でGoを利用する機会ができました
GoをAPIとして用いるならLambda
を利用するのが1つの手段になります
そこで、検証がてらGO(gin)を利用してAPIを作成して、LambdaにCICDデプロイをできるようにしました。
Go(gin)でLambdaにデプロイしたい人のチュートリアルになればと思います。
開発環境
- VSCode
- WSL2 (Ubuntu 20.04)
- Docker version 20.10.1
- docker-compose version v2.2.3
- git version 2.25.1
ディレクトリ構成
完成後のディレクトリ構成はこのようになっています
go_lambda_cicd
┣ Dockerfile
┣ docker-compose.yml
┣ docker-compose.production.yml
┣ .env
┣ package.json
┣ package.lock.json
┣ buildspec.yml
┣ .gitignore
┣ src
┣ .serverless
┣ bin
┣ main
┣ handler
┣ main.go
┣ .air.toml
┣ go.mod
┣ go.sum
┣ main_test.go
┣ Makefile
┣ serverless.yml
コンテナ環境の構築
まずはディレクトリを作成します
$ mkdir go_lambda_cicd
次にコンテナに必要なファイルを作成します
$ cd go_lambda_cicd
$ touch Dockerfile
$ touch docker-compose.yml
$ touch docker-compose.production.yml
$ touch .env
$ touch buildspec.yml
$ touch package.json
以下の内容をそれぞれ記載します
FROM golang:1.18.4-alpine3.16 as base
RUN apk update && apk add git
FROM base as develop
# for Vscode extension
RUN go install github.com/ramya-rao-a/go-outline@latest
RUN go install golang.org/x/tools/gopls@latest
# hotreload
RUN go install github.com/cosmtrek/air@latest
FROM base as production
# serverless framework
RUN apk add make
RUN apk add --no-cache nodejs npm
COPY package.json .
RUN npm install -g serverless
ここではマルチステージビルドで開発と本番でイメージを分けています
開発ではVSCodeで利用するライブラリやホットリロードのためのairを入れています。本番では今回利用するserverless framework
と実行に必要なライブラリ、Makefileを実行するためのmake
をいれました
version: '3'
services:
app:
build:
context: .
target: develop
tty: true
container_name: go
working_dir: /go/src
volumes:
- ./src:/go/src
ports:
- "3000:3000"
depends_on:
- db
db:
image: mysql:8.0.27
container_name: db
env_file:
- .env
ports:
- "3306:3306"
volumes:
- db:/var/lib/mysql
volumes:
db:
開発用のdocker-composeファイルです。
GoコンテナとDBコンテナを立てています。target: developとすることで開発のイメージを利用しています
version: '3'
services:
app:
build:
context: .
target: production
tty: true
container_name: go_prod
working_dir: /go/src
volumes:
- .:/go
- exclued:/go/pkg
ports:
- "3000:3000"
volumes:
exclued:
本番用のdocker-compseファイルです
volumesで全てのファイルをマウントしています。(ただしpkgはローカルにはマウントしない)
buildspec.ymlやserverless.ymlをCodeBuildで実行するためです
MYSQL_DATABASE=myapp
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=password
MySQLの設定ファイルです
version: 0.2
phases:
pre_build:
commands:
- echo Logging in to Test...
- docker-compose -f docker-compose.production.yml run --rm app go test -v
build:
commands:
- echo build go...
- docker-compose -f docker-compose.production.yml run --rm app make build
- echo deploy go...
- docker-compose -f docker-compose.production.yml run --env AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} --env AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} --rm app make deploy
CodeBuildで読み込む設定ファイルです。内容についてはこのあとそれぞれのコマンドで実行するのでわかるかと思います。
Goの実行ファイルの準備
今回はWebフレームワークであるgin
を利用してlambda関数
を実装していきます。
いかの記事を参考に作成していきます
必要なファイルを作成します
$ mkdir src
$ mkdir src/handler
$ touch src/.air.toml
$ touch src/main_test.go
$ touch src/go.mod
$ touch src/Makefile
$ touch src/serverless.yml
$ touch src/handler/main.go
内容を記載します
# Config file for [Air](https://github.com/cosmtrek/air) in TOML format
# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"
[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary, can setup environment variables when run your app.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude specific regular expressions.
exclude_regex = ["_test.go"]
# Exclude unchanged files.
exclude_unchanged = true
# Follow symlink for directories
follow_symlink = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
[log]
# Show log time
time = false
[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"
[misc]
# Delete tmp directory on exit
clean_on_exit = true
ホットリロード用の設定ファイルです。
package sample
import "testing"
func add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{
name: "normal",
args: args{a: 1, b: 2},
want: 3,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := add(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("add() = %v, want %v", got, tt.want)
}
})
}
}
簡単なテストを以下の記事からもってきました
module sample-go
go 1.18
ライブラリ管理のためのファイルです
build:
go mod download
env CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -ldflags="-s -w" -o bin/main handler/main.go
deploy:
serverless deploy
makeコマンドを利用することでコマンドを実行してくれます。例えばbuildの内容を動かすならmake build
で動かせます。あとで利用します
service: aws-lambda-go-api-proxy-gin
provider:
name: aws
runtime: go1.x
stage: ${opt:stage, self:custom.defaultStage}
region: ap-northeast-1
iamRoleStatements:
- Effect: "Allow"
Action:
- "logs:*"
Resource: "*"
package:
exclude:
- ./**
include:
- ./bin/**
custom:
defaultStage: dev
functions:
api:
handler: bin/main
timeout: 900
events:
- http:
path: ping
method: get
- http:
path: hello
method: get
serverless frameworkの設定ファイルです
package:
exclude:
- ./**
include:
- ./bin/**
でsrc/bin
をデプロイ
functions:
api:
handler: bin/main
timeout: 900
events:
- http:
path: ping
method: get
- http:
path: hello
method: get
でbin/mainを実行、エンドポイントはGET /ping
とGET /hello
と設定しています
このエンドポイントがginのエンドポイントに対応します
package main
import (
"log"
"context"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/awslabs/aws-lambda-go-api-proxy/gin"
"github.com/gin-gonic/gin"
)
var ginLambda *ginadapter.GinLambda
func init() {
// stdout and stderr are sent to AWS CloudWatch Logs
log.Printf("Gin cold start")
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "CICD Success!",
})
})
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello World!",
})
})
ginLambda = ginadapter.New(r)
}
func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// If no name is provided in the HTTP request body, throw an error
return ginLambda.ProxyWithContext(ctx, req)
}
func main() {
lambda.Start(Handler)
}
ここで一度開発用コンテナを起動してみます
$ docker-compose up
# 別のターミナルを開いて
$ docker exec -it go sh
# 必要なライブラリをgo.modで管理する
$ go mod tidy
ここまでできればGo関連の準備は終わりです
IAMの用意
Serverless Framework用のIAMユーザーを作成します
ユーザーを追加
をクリック
ユーザー名 : go_serverless
アクセスキー・プログラムによるアクセス : ☑
次のステップ:アクセス権限
クリック
既存のポリシーを直接アタッチをクリック
AdministratorAccess
にチェックを入れて
「次のステップ:タグ」→「次のステップ:確認」→「ユーザーの作成」を順にクリック
アクセスキー
とシークレットアクセスキー
をメモしておきます
手動でデプロイ
まずは手動でのデプロイで動作確認を行います
いま立ち上がっているコンテナは落とします
$ docker-compose down
まずはテストが実行できるか確認します
$ docker-compose -f docker-compose.production.yml run --rm app go test -v
以下になればテストが実行できています
次にserverlessでデプロイを実行します
$ docker-compose -f docker-compose.production.yml build
# Goのビルド(go.sumとbin/mainが作成される)
$ docker-compose -f docker-compose.production.yml run --rm app make build
# Lambdaへのデプロイ(.srverlessが作成される)
$ docker-compose -f docker-compose.production.yml run --env AWS_ACCESS_KEY_ID=[作成したアクセスキー] --env AWS_SECRET_ACCESS_KEY=[作成したシークレットアクセスキー] --rm app make deploy
Makefileに書かれているbuildとdeployをそれぞれ実行しています
また、deployではIAMユーザーのアクセスキーが必要なので環境変数を用いてコンテナに渡しています
うまくいくと以下のようなURLが表示されますのでアクセスします
✔ Service deployed to stack aws-lambda-go-api-proxy-gin-dev (158s)
endpoints:
GET - https://n04acp53bj.execute-api.ap-northeast-1.amazonaws.com/dev/ping
GET - https://n04acp53bj.execute-api.ap-northeast-1.amazonaws.com/dev/hello
functions:
api: aws-lambda-go-api-proxy-gin-dev-api (4.5 MB)
GET /ping
GET /hello
Lambdaのデプロイはこれにて成功です。
main.goを変えてbuildとdeployを実行すると更新されることも確認できます
CICDパイプラインの構築
ここからはCodeBuild
を用いてCICDを行います
GithubのmainブランチにPushされた段階でLambdaを更新できるようにします
まずはリポジトリを作成します
ここは詳しくは説明しませんので各自調べてください
以下のリポジトリを作成しました
リポジトリ名 : go_lambda_cicd (パブリック)
$ git init
$ git add .
$ git commit -m "create"
$ git remote add origin [リポジトリのURL]
$ git branch -M main
$ git push origin main
リポジトリが作成できました
次にAWSからCodeBuild
を検索します
ビルドプロジェクトを作成する
をクリック
項目 | 値 |
---|---|
プロジェクト名 | go_lambda_cicd |
ソースプロバイダ | GitHub |
リポジトリ | GitHubアカウントのリポジトリ |
GitHubリポジトリ | 作成したリポジトリ |
コードの変更がこのリポジトリに.. | ☑ |
イベントタイプ | プッシュ |
HEAD_REF | ^refs/heads/main$ |
オペレーションシステム | Ubuntu |
ランタイム | Ubuntu |
イメージ | aws/codebuild/standard:6.0 |
イメージのバージョン | aws/codebuild/starndard:6.0-22.06.30 |
特権付与 | ☑ |
追加設定の環境変数を設定
名前 | 値 |
---|---|
AWS_ACCESS_KEY_ID | アクセスキーの値 |
AWS_SECRET_ACCESS_KEY | シークレットアクセスキーの値 |
設定したら「ビルドプロジェクトを作成する」をクリック
「ビルドを開始」をクリックして動作するか確認します
最後まで成功したら実際にCICDができるか確かめます
CICDの確認
src/handler/main.go
のAPIが返すMessageを変更してみます
(省略)
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "CICD Check!",
})
})
(省略)
Gitに変更内容をPushします。
$ git add .
$ git commit -m "modify"
$ git push origin main
AWSの左メニューから「ビルドプロジェクト」をクリックするとPushでCodeBuildが動いたことがわかります
成功したら、ログにでてきたURLの変更をした/ping
のほうにアクセスします
変更が確認できました!
後片付け
以下のコマンドでserverless frameworkの削除を行います
$ docker-compose -f docker-compose.production.yml run --env AWS_ACCESS_KEY_ID=[アクセスキーの値] --env AWS_SECRET_ACCESS_KEY=[シークレットアクセスキーの値] --rm app serverless remove --verbose
それ以外に作成したAWSリソースを削除します
- CodeBuild
- CloudWatchロググループ
- IAMユーザー
- IAMロール
- IAMポリシー (CodeBuild)
おわりに
以前Rails6+MySQLのDocker環境構築 (CI/CDまでの道①)という記事を作成した際にかなり苦戦してCICDを構築したのですが、それに比べてserverless frameworkを使うことでかなり簡単にデプロイができました
次回はReact
をS3
とCloudFront
を利用してCICDしようかと思います
今回作成したリポジトリは以下になります
参考
- Serverless FrameworkでGolangのAWS Lambda関数を作る
- AWS LambdaでGolangのWebフレームワークGinを利用してみた
- Serverless Framework で deploy したサービスを全て削除する方法
- error "fork/exec /var/task/main: no such file or directory" while executing aws-lambda function
- Goのテストに入門してみよう!
- Serverless Frameworkのインストールと初期設定
- 【Docker】コンテナでNode.jsを実行してみた
- DockerでVolumeをマウントするとき一部を除外する方法
Discussion