いろいろなビルド用の分岐にタグを使ってみる(例えばlambda用のビルド)

いい感じにできないか考えたりしていた。アイデア段階で途中だけど途中経過をメモ。

TODOアプリを模したコードがあったとして、以下の様な操作が提供されているとする。

type TodoApp interface {
    List() ([]Todo, error)
    Add(todo Todo) (Todo, error)
}

これをいろいろな形態で提供してみたい。 例えば、1つは手元で動かすwebアプリ、もう1つはaws lambda。

このときにいい感じに取り扱うことができないか?と言う話。まぁ今回は実際のデプロイまでは行わず、ビルドしてバイナリを作るところまででおしまいにする。

$ ls a*
a.out           a.out.lambda.add    a.out.lambda.list

ファイル構成

以下の様なファイル構成で実験してみる。hello world的なコードなのであまり真面目に考えない。 interactor.goが冒頭であげたインターフェイスを実装しているようなコード。

$ tree .
.
├── Makefile
├── interactor.go
├── main.go
├── main_lambda_add.go
└── main_lambda_list.go

0 directories, 5 files

それぞれのファイルは以下の様な形になっている。

main.go

// +build !lambda

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"

    "github.com/go-chi/chi"
    "github.com/go-chi/render"
)

type Controller struct {
    Interactor *Interactor
}

... // 省略 (全コードは付録として)

func main() {
    r := chi.NewRouter()

    c := &Controller{Interactor: GetInteractor()}
    r.Get("/", c.List)
    r.Post("/", c.Add)

    addr := os.Getenv("ADDR")
    if addr == "" {
        addr = ":44444"
    }

    if err := http.ListenAndServe(addr, r); err != nil {
        log.Fatalf("!!%+v", err)
    }
}

main_lambda_list.go

// +build lambda
// +build list

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

func main() {
    ir := GetInteractor()
    lambda.Start(ir.List)
}

main_lambda_add.go

// +build lambda
// +build add

package main

import (
    "github.com/aws/aws-lambda-go/lambda"
)

func main() {
    ir := GetInteractor()
    lambda.Start(ir.Add)
}

ビルド

という形にファイルを分けた上で、タグによって利用するmain.goを分岐させてビルドしてみる。

$ make clean default
rm -f a.out*
go build -o a.out
go build -o a.out.lambda.list --tags lambda,list
go build -o a.out.lambda.add --tags lambda,add

a.outはmain.go。それ以外はlambda用のバイナリ。このmain.go達がかなりコピペ感があって微妙だなーと言う気持ちになったりしていた。

このときのMakefile

Makefile

TARGETS := $(shell cat Makefile | grep '^a\.out' | cut -d : -f 1)
default: $(TARGETS)

a.out:
  go build -o $@
a.out.lambda.list:
  go build -o $@ --tags lambda,list
a.out.lambda.add:
  go build -o $@ --tags lambda,add

clean:
  rm -f a.out*
.PHONY: clean

付録: lambdaやhandler/controllerの実装の詳細

main.goの詳細

長すぎて省略したmain.goの全貌はこんな感じ。

main.go

// +build !lambda

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"

    "github.com/go-chi/chi"
    "github.com/go-chi/render"
)

type Controller struct {
    Interactor *Interactor
}

func (c *Controller) List(w http.ResponseWriter, r *http.Request) {
    items, err := c.Interactor.List()
    if err != nil {
        render.Status(r, 500)
        render.JSON(w, r, map[string]interface{}{"message": err.Error()})
        return
    }
    render.JSON(w, r, map[string]interface{}{"items": items})
}

func (c *Controller) Add(w http.ResponseWriter, r *http.Request) {
    var item Todo
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&item); err != nil {
        render.Status(r, 400)
        render.JSON(w, r, map[string]interface{}{"message": err.Error()})
    }

    added, err := c.Interactor.Add(item)
    if err != nil {
        render.Status(r, 500)
        render.JSON(w, r, map[string]interface{}{"message": err.Error()})
        return
    }
    render.JSON(w, r, added)
}

func main() {
    r := chi.NewRouter()

    // このままが良いのか、interactorの粒度を細かくしたほうが良いのかはわからない
    // github.com/awslabs/aws-lambda-go-api-proxy を使うよりは意義があるのではないか?
    // Controllerが肥大化するならたぶん分けたほうが良い。Interactorも同様ならhandler関数を生成する関数を定義する形のほうが綺麗かもしれない

    c := &Controller{Interactor: GetInteractor()}
    r.Get("/", c.List)
    r.Post("/", c.Add)

    addr := os.Getenv("ADDR")
    if addr == "" {
        addr = ":44444"
    }

    if err := http.ListenAndServe(addr, r); err != nil {
        log.Fatalf("!!%+v", err)
    }
}

Handlerを共有?Interactorを共有?

この実装とは別の考え方として、Handlerを共有する方法もある。awslabs/aws-lambda-go-api-proxy を使う。

今回はController(Handler)がInteractorを持ち、これを共有するということを考えてみた1

ちなみに、lambdaは以下のようなインターフェイスを許容する(lambda.Start()に渡せるのは以下の様なシグネチャを持つ関数)。

        func ()
        func () error
        func (TIn) error
        func () (TOut, error)
        func (TIn) (TOut, error)
        func (context.Context) error
        func (context.Context, TIn) error
        func (context.Context) (TOut, error)
        func (context.Context, TIn) (TOut, error)

詳しくはドキュメントに

ちなみにミドルウェアのようなことがしたければlambda/handlertraceある。

使い方はPRを見るとわかりやすい。

おわりに

main.goをいい感じに書かずに済んだりしないかな。。あと、api-gatewayまで含めてlambdaにdeployしちゃいたい。。

gist


  1. 主題はビルドの話なので設計の話は枝葉の話ではある