11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

朝日新聞社Advent Calendar 2022

Day 24

Go言語をつかった Lambda関数の開発をもっと楽にしたい〜Codespacesを添えて〜

Last updated at Posted at 2022-12-23

メリークリスマス!
この記事は朝日新聞社 Advent Calendar 2022の24日目の記事です。

TL;DR

Dockerコンテナ上で RIE と air つかってサクサク開発環境を作りましょう
サンプルは https://github.com/duck8823/sample-lambda にあります。
Open in GitHub Codespaces

今回の開発スタイル

こんな感じで開発するとしましょう。

  • スキーマをOpenAPIで管理したい
  • Go でLambda関数を書きたい
  • curl で動作確認したい
  • ホットリロードほしい
  • スキーマから生成されたクライアントを試したい

Lambda 関数の開発の何がめんどくさいのか

Lambda関数はコードを書くのはとっても簡単です。
公式ドキュメントの通り、ハンドラー関数を記述するだけです。

また、コンテナイメージを利用して cURL で動作確認をする方法も公式ドキュメントに記載されています。

RIE を使った場合、関数を実行する際のパスは /2015-03-31/functions/function/invocations になります。

実際には前述の通り、 OpenAPI でスキーマを管理し、生成されたクライアントコードを使って開発します。
その際、スキーマで定義されたパスと RIE で用意されるパスが異なります。

要は API Gateway の役割を果たしてくれる何かがあればグッと楽になりそうでうですね。
上記開発スタイルを一つ一つ見ながら開発環境を整えていきましょう。

Lambda関数を開発するよ

スキーマをOpenAPIで管理したい

今回はこんな感じのスキーマを想定します。

openapi.yml
---
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
main.go
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.shDockerfile を用意しましょう。

entry.sh
#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
  exec /usr/bin/aws-lambda-rie "$@"
else
  exec "$@"
fi        

実行可能ファイルにします。

chmod +x entry.sh

ファイルの変更の度にDockerイメージのビルドを行うのは手間暇がかかるので、ボリュームマウント前提で書きます。

Dockerfile
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 経由に書き換えます。

.air.toml
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 をインストールしましょう

Dockergile
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...

これで、

  1. コードを変更する
  2. cURL で確認する

のイテレーションがとても楽になりました。
既存の Lambda 関数でも Air と RIE を追加するだけでグンと開発生産性があがると思うのでぜひお試しください。

スキーマから生成されたクライアントを試したい

OpenAPIでスキーマを定義しているので、自動生成されたクライアントを利用して実行したいです。
試しに以下のようなコードを書いてみました。

main_test.go
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 を用意してみました。

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 については明日のアドベントカレンダーで紹介する予定です。お楽しみに。

.devcontainer/devcontainer.json
{
  "name": "sample-lambda",
  "dockerComposeFile": [
    "../docker-compose.yml"
  ],
  "service": "sample-lambda",
  "workspaceFolder": "/workspace",
  "remoteUser": "root"
}

コンテナ内で開発をするので、必要なツールのインストールを Dockerfile に書いてしまいましょう。
紹介した RIE および Air に加えて Linter や コードジェネレーター をインストールしています。

Dockerfile
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 が生えています。
image.png

起動するとまさに VSCode な画面が映し出されます。
スクリーンショット 2022-12-22 13.35.11.png
これで年末年始に実家に帰った際にパソコンがなくってもブラウザからコードを書けますね!

ワンステップで開発に必要なツール群が揃うのは便利ですが、
コンテナ内で操作するので 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 のタスクを指定して実行できるアクションにしてみました。

action.yml
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ができてしまいます。

.github/workflows/test.yml
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)に必要なツールだけをインストールできるようにマルチステージビルドできる記述にしてみました。

Dockerfile
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 の統合プロキシを利用する場合にどういう構成にすると開発が楽になりそうかなどは引き続き検討していきたいです。


なお、朝日新聞社では、技術職の中途採用を強化しています。
ご興味のある方は下記リンクから希望職種の募集ページに進んでください。
皆様からのご応募、お待ちしております!

11
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?