メリークリスマス!
この記事は朝日新聞社 Advent Calendar 2022の24日目の記事です。
TL;DR
Dockerコンテナ上で RIE と air つかってサクサク開発環境を作りましょう
サンプルは https://github.com/duck8823/sample-lambda にあります。
今回の開発スタイル
こんな感じで開発するとしましょう。
- スキーマをOpenAPIで管理したい
- Go でLambda関数を書きたい
- curl で動作確認したい
- ホットリロードほしい
- スキーマから生成されたクライアントを試したい
Lambda 関数の開発の何がめんどくさいのか
Lambda関数はコードを書くのはとっても簡単です。
公式ドキュメントの通り、ハンドラー関数を記述するだけです。
また、コンテナイメージを利用して cURL で動作確認をする方法も公式ドキュメントに記載されています。
RIE を使った場合、関数を実行する際のパスは /2015-03-31/functions/function/invocations
になります。
実際には前述の通り、 OpenAPI でスキーマを管理し、生成されたクライアントコードを使って開発します。
その際、スキーマで定義されたパスと RIE で用意されるパスが異なります。
要は API Gateway の役割を果たしてくれる何かがあればグッと楽になりそうでうですね。
上記開発スタイルを一つ一つ見ながら開発環境を整えていきましょう。
Lambda関数を開発するよ
スキーマをOpenAPIで管理したい
今回はこんな感じのスキーマを想定します。
---
openapi: "3.0.0"
info:
version: 0.0.1
title: サンプル
servers:
- url: http://127.0.0.1
description: ローカル開発
- url: http://api-gateway
description: コンテナ開発
paths:
/users:
post:
operationId: create_user
summary:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/User'
example:
name: John
responses:
200:
description: |-
結果
content:
application/text:
schema:
$ref: '#/components/schemas/Result'
tags:
- user
components:
schemas:
User:
title: User
type: object
required:
- name
properties:
name:
type: string
Result:
title: Result
type: string
externalDocs:
description: 朝日新聞社では、技術職の中途採用を強化しています
url: https://www.asahishimbun-saiyou.com/information/career
tags:
- name: user
description: ユーザー
スキーマからコードを自動生成しましょう。
OpenAPI Generatorなどで作成することができます。
下記の例では --global-properties
を用いて生成するコードを絞っています。
openapi-generator generate \
-g go \
-i openapi.yml \
-o generated/go/user \
--package-name user \
--global-property=models,apis,supportingFiles=configuration.go:client.go:utils.go,apiTests=false,apiDocs=false,modelTests=false,modelDocs=false
以下が生成されるファイル一覧です。
generated
└── go
└── user
├── api
├── api_user.go
├── client.go
├── configuration.go
├── model_user.go
└── utils.go
model_user.go
では構造体(DTO)が定義されています。
生成された構造体を用いてJSONのペイロードをデコードすることができます。
Go でLambda関数を書きたい
上記で生成されたコードを利用して Go を使った Lambda 関数を作ります。
ほぼドキュメント( https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/golang-handler.html )のサンプルコードのままですが、ハンドラー関数の引数としてスキーマから生成された構造体を利用しています。
go mod init github.com/duck8823/sample-lambda
package main
import (
"context"
"fmt"
"github.com/aws/aws-lambda-go/lambda"
"github.com/duck8823/sample-lambda/generated/go/user"
)
func HandleRequest(ctx context.Context, user user.User) (string, error) {
return fmt.Sprintf("Hello %s!", user.Name), nil
}
func main() {
lambda.Start(HandleRequest)
}
cURL で動作確認したい
公式ドキュメント( https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-test.html )に従って entry.sh
と Dockerfile
を用意しましょう。
#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
exec /usr/bin/aws-lambda-rie "$@"
else
exec "$@"
fi
実行可能ファイルにします。
chmod +x entry.sh
ファイルの変更の度にDockerイメージのビルドを行うのは手間暇がかかるので、ボリュームマウント前提で書きます。
FROM golang:1.18
## for lambda development
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod +x /usr/bin/aws-lambda-rie
WORKDIR /workspace
ENTRYPOINT ["./entry.sh"]
通常通りイメージをビルドして起動しています。
docker build -t sample-lambda .
docker run -v ${PWD}:/workspace -p 127.0.0.1:9000:8080 sample-lambda go run main.go
これでコンテナが立ち上がったので確認してみましょう。
curl -XPOST "http://127.0.0.1:9000/2015-03-31/functions/function/invocations" -d '{"name": "John"}'
"Hello John!"
Lambda関数が cURL で試せました。
ホットリロードほしい
上述の起動方法では、ソースコードを変更しても実行中のプログラムには反映されず、コンテナを起動し直す必要があります。
Go製のプログラムのホットリロードには Air ( https://github.com/cosmtrek/air ) というツールが便利です。
go install github.com/cosmtrek/air@latest
ただし、デフォルトでは RIE を介した実行にならないので、設定ファイルを作成して書き換えましょう。
air init
すると .air.toml
というファイルが生成されるので、 bin
部分を ./entry.sh
経由に書き換えます。
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"
[build]
args_bin = []
bin = "./entry.sh ./tmp/main"
cmd = "go build -o ./tmp/main ."
delay = 1000
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
kill_delay = "0s"
log = "build-errors.log"
send_interrupt = false
stop_on_error = true
[color]
app = ""
build = "yellow"
main = "magenta"
runner = "green"
watcher = "cyan"
[log]
time = false
[misc]
clean_on_exit = false
[screen]
clear_on_rebuild = false
Dockerfile
を書き換えてコンテナで Air をインストールしましょう
FROM golang:1.18
## for lambda development
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod +x /usr/bin/aws-lambda-rie
## hot reload
RUN go install github.com/cosmtrek/air@latest
WORKDIR /workspace
CMD ["air", "-c", ".air.toml"]
docker build -t sample-lambda .
docker run -v ${PWD}:/workspace -p 127.0.0.1:9000:8080 sample-lambda
この状態でソースコードを書き換えると、以下のようにログが出力され、アプリケーションがリロードされます。
main.go has changed
building...
running...
これで、
- コードを変更する
- cURL で確認する
のイテレーションがとても楽になりました。
既存の Lambda 関数でも Air と RIE を追加するだけでグンと開発生産性があがると思うのでぜひお試しください。
スキーマから生成されたクライアントを試したい
OpenAPIでスキーマを定義しているので、自動生成されたクライアントを利用して実行したいです。
試しに以下のようなコードを書いてみました。
package main_test
import (
"context"
"github.com/duck8823/sample-lambda/generated/go/user"
"reflect"
"testing"
)
func Test_APIClient(t *testing.T) {
// given
want := "\"Hello John!\""
// when
got, _, err := user.NewAPIClient(user.NewConfiguration()).UserApi.
CreateUser(context.Background()).
User(user.User{Name: "John"}).
Execute()
// then
if err != nil {
t.Error(err)
}
// and
if !reflect.DeepEqual(got, want) {
t.Errorf("want: %#v, but got: %#v", want, got)
}
}
スキーマから生成されたクライアントコードにはパスの情報( /users
)が組み込まれています。
なんらかの形で /users
にきたリクエストを Lambda の実行環境の /2015-03-31/functions/function/invocations
に渡してやれば良さそうですね。
リバースプロキシーサーバーである traefik ( https://github.com/traefik/traefik ) は Docker 環境との相性もよく、ラベルを使って設定を実現できます。
既出の Docker の定義に加え、 AWS API Gateway に見立てたリバプロを立てた docker-compose.yml
を用意してみました。
version: "3.3"
services:
api-gateway:
image: "traefik:v2.9"
command:
- "--api.insecure=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--entrypoints.web.address=:80"
ports:
- 127.0.0.1:80:80
- 127.0.0.1:8080:8080
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
sample-lambda:
build: .
volumes:
- ${PWD}:/workspace
labels:
- "traefik.enable=true"
- "traefik.http.routers.sample-lambda.rule=Host(`127.0.0.1`, `api-gateway`)"
- "traefik.http.routers.sample-lambda.entrypoints=web"
- "traefik.http.services.sample-lambda.loadbalancer.server.port=8080"
- "traefik.http.middlewares.sample-lambda.replacepathregex.regex=^/users"
- "traefik.http.middlewares.sample-lambda.replacepathregex.replacement=/2015-03-31/functions/function/invocations"
- "traefik.http.routers.sample-lambda.middlewares=sample-lambda@docker"
これで起動し、
docker compose up
上記の設定で api-gateway
のコンテナ( 127.0.0.1
または api-gateway
をホストとして )で /users
にアクセスがあった場合は sample-lambda
コンテナの 8080
ポート、 /2015-03-31/functions/function/invocations
にプロキシしてくれるように記述されています。
なので、以下のコマンドで Lambda 関数が実行されます。
curl -XPOST "http://127.0.0.1/users" -d '{"name": "John"}'
"Hello John!"
これにより生成されたクライアントを使ったコードも動きます。 クライアントにおけるホストの情報は環境変数などを使って外から変更できるようにしておくと便利です。
cli := user.NewAPIClient(user.NewConfiguration())
if host, exist := os.LookupEnv("SAMPLE_HOST"); exist {
cli.GetConfig().Host = host
}
今回はプロキシを介してリクエストをそのまま Lambda関数 に渡しましたが、
プロキシ統合を利用する場合は
まとめ
RIE と Air、 そして traefik を使って Lambda関数 の開発が簡単な環境をつくりました。
リポジトリのクローンから数ステップで開発できるようになっていると嬉しいですよね。
サンプルは https://github.com/duck8823/sample-lambda にあります。ぜひ体験してみてください。
これらは既存のコードにコンフリクトせずに入れられるので、既存の開発リポジトリにもぜひ導入してみてはいかがでしょう。
おまけ その1(コンテナで開発するのが流行ってるらしい)
DevContainer や Codespaces といった、コンテナ上での開発が流行っているようです。
用意した docker-compose.yml
使って最低限実行できる設定を書いてみました。
Dev Container については明日のアドベントカレンダーで紹介する予定です。お楽しみに。
{
"name": "sample-lambda",
"dockerComposeFile": [
"../docker-compose.yml"
],
"service": "sample-lambda",
"workspaceFolder": "/workspace",
"remoteUser": "root"
}
コンテナ内で開発をするので、必要なツールのインストールを Dockerfile に書いてしまいましょう。
紹介した RIE および Air に加えて Linter や コードジェネレーター をインストールしています。
FROM golang:1.18
## node & generator
RUN apt update \
&& apt install -y nodejs npm \
&& npm install -g openapi-generator
## jq
RUN apt install -y jq
## for lambda development
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod +x /usr/bin/aws-lambda-rie
## hot reload
RUN go install github.com/cosmtrek/air@latest
## linter
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
WORKDIR /workspace
CMD ["air", "-c", ".air.toml"]
なお、.devcontainer/devcontainer.json
があれば、 Codespaces ( https://github.co.jp/features/codespaces )も動きます。
Codespaces はクラウド上にホスティングされている開発環境です。
ブラウザ上で VSCode のエディターを操作することができます。
サンプルのリポジトリでも Codespaces を試せるので触ってみてください。
リポジトリのトップ、クローンするときによく見るところに Codespaces が生えています。
起動するとまさに VSCode な画面が映し出されます。
これで年末年始に実家に帰った際にパソコンがなくってもブラウザからコードを書けますね!
ワンステップで開発に必要なツール群が揃うのは便利ですが、
コンテナ内で操作するので 127.0.0.1
では用意したリバースプロキシに到達しません。コンテナ内からはコンテナ名で接続することができます。
curl -XPOST "http://api-gateway/users" -d '{"name": "John"}'
"Hello John!"
おまけ その2( 用意したDockerfileをCIで使ったら楽ちゃうのん )
開発用に用意した Dockerfile には Linter なども用意されているので、
それ使えばCIの定義も楽になるんじゃ?と考えてみました。
GitHub Actions で Dockerアクション を作るのは簡単です。
Dockerfile と アクションの定義を用意してあげるだけです( https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action )。
今回は Makefile
のタスクを指定して実行できるアクションにしてみました。
name: 'develop'
description: '開発用のアクション'
inputs:
task:
description: タスクの設定
required: true
runs:
using: 'docker'
image: 'Dockerfile'
args:
- make
- ${{ inputs.task }}
作成したアクションをワークフローで使ってみましょう( https://docs.github.com/ja/actions/creating-actions/creating-a-docker-container-action#example-using-a-private-action )。
必要なツールセットはコンテナに含まれているのでチェックアウトだけで簡単にCIができてしまいます。
name: テスト
on:
push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./
with:
task: test
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./
with:
task: lint
このアクションの場合、ワークフローの実行時にDockerイメージのビルドを実行するので、ツールがもりもりの Dockerfile
を指定するとCIの実行時間が長くなってしまいます。
そこで、テスト(CI)に必要なツールだけをインストールできるようにマルチステージビルドできる記述にしてみました。
FROM golang:1.18 AS base
## linter
RUN go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.50.1
FROM base AS develop
## node & generator
RUN apt update \
&& apt install -y nodejs npm \
&& npm install -g openapi-generator
## jq
RUN apt install -y jq
## for lambda development
ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie
RUN chmod +x /usr/bin/aws-lambda-rie
## hot reload
RUN go install github.com/cosmtrek/air@latest
WORKDIR /workspace
CMD ["air", "-c", ".air.toml"]
FROM base AS test
このように書くことで、BuildKitが有効な場合、ターゲットを指定しなければ develop のステージはスキップしてくれます。
しかし、 実際に試したところ GitHub Actions では全てのステップが実行されてしまいました...BuildKit が有効じゃないみたい。
素直に CI用に Dockerfile を用意してあげるのがCI高速化には有効なようです。
今後
今回は非常にシンプルなリポジトリ構成の場合の紹介でした。
モノレポや、API Gateway の統合プロキシを利用する場合にどういう構成にすると開発が楽になりそうかなどは引き続き検討していきたいです。
なお、朝日新聞社では、技術職の中途採用を強化しています。
ご興味のある方は下記リンクから希望職種の募集ページに進んでください。
皆様からのご応募、お待ちしております!