Go(gin)とLambda(serverless framework)でCICDする

2022/08/14に公開

はじめに

仕事で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

以下の内容をそれぞれ記載します

Dockerfile
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をいれました

docker-compose.yml
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とすることで開発のイメージを利用しています

docker-compose.production.yml
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で実行するためです

.env
MYSQL_DATABASE=myapp
MYSQL_PASSWORD=password
MYSQL_ROOT_PASSWORD=password

MySQLの設定ファイルです

buildspec.yml
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関数を実装していきます。

いかの記事を参考に作成していきます

https://blog.mmmcorp.co.jp/blog/2020/06/07/aws-lambda-with-golang-gin/

必要なファイルを作成します

$ 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

内容を記載します

src/.air.toml
# 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

ホットリロード用の設定ファイルです。

src/main_test.go
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)
			}
		})
	}
}

簡単なテストを以下の記事からもってきました

https://future-architect.github.io/articles/20200601/

src/go.mod
module sample-go

go 1.18

ライブラリ管理のためのファイルです

src/Makefile
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で動かせます。あとで利用します

src/serverless.yml
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 /pingGET /helloと設定しています

このエンドポイントがginのエンドポイントに対応します

src/handler/main.go
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を変更してみます

/src/handler/main.go
(省略)

    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

https://codenote.net/tool/4293.html

それ以外に作成したAWSリソースを削除します

  • CodeBuild
  • CloudWatchロググループ
  • IAMユーザー
  • IAMロール
  • IAMポリシー (CodeBuild)

おわりに

以前Rails6+MySQLのDocker環境構築 (CI/CDまでの道①)という記事を作成した際にかなり苦戦してCICDを構築したのですが、それに比べてserverless frameworkを使うことでかなり簡単にデプロイができました

次回はReactS3CloudFrontを利用してCICDしようかと思います

今回作成したリポジトリは以下になります

https://github.com/jinwatanabe/go_lambda_cicd

参考

GitHubで編集を提案

Discussion