- 投稿日:2019-03-18T15:07:15+09:00
go で gorma を使ってAPI開発してみる。まずはgoa編
ちょっとしたAPIを作ることになりそうなんで、goでAPI+DB設計するとしたらどんなもんがあるのだろう・・・と調べていたら、 gorma ってのがそれなりに使えそうな気がしてきました。
ただ、こういうのって、
- 自動化はたしかに楽
- ただしその縛りを受けて意外と辛い
みたいのがあるあるだったりするので、自分の中で気になっていた点をいくつか確認してみました。
参考になったのは こちら 。(とても参考にさせて頂きました。ありがとうございます)
ただ、今のgoにあわせて dep を使わないように修正しました。
dep外し
以下が修正箇所です。
- こちら の0. goa/goagenをインストールする` の節で、自分のリポジトリは適当な場所に置けばいい(GOPATH意識しない)
- その中に、 こちら の
1. DSLでAPIデザインを書く
と同じ構造を作る./design ├── api.go // API全体の定義(APIで定義) ├── mediatypes // レスポンスのスキーマ(MediaTypeで定義) │ └── task.go ├── usertypes // リクエストのpayloadなどのスキーマ(Typeで定義) │ └── task.go └── resources // エンドポイントの定義(Resourceで定義) └── task.go
- その後は、
go mod init
するgo build
とかしてgo.modの内容を修正だけしておく。buildはコケても後でなんとかなるので気にしない
- この時点ではmain.goがないのでコケる。あとで自動で作られるのでそこで問題回避
2. goagen(コードジェネレーター)で自動生成する
までの流れは同じgoagen
は普通にgo install github.com/goadesign/goa/goagen
する- (これは自分だけですが)
"github.com/BcRikko/learning-goa/design/resources"
な部分は自分のリポジトリに合わせて修正- ちなみに個人的にimport時の名前変更は理由がない限りやらない主義なので、以下のコードでは
design -> goa
というエイリアスはdesign
のままにしてます。この状態で、
goagen bootstrap -d github.com/<your github user>/<your respository>/design
するとファイルが生成されます。この時点でmain.goも出来るのでgo buildも通ります。
あとは、TOPにある
task.go
を修正するだけ。こちら の
3. APIを実装する
を参考に進めます。ここから疑問
自分の中で、この後のフェーズで気になったのは2点です。
もしAPIのエンドポイントが修正になった場合等、再度生成するとロジックが全部上書きされてしまうのでは?
当然APIのエンドポイントが追加になることはありますよね。
例えば、list というエンドポイントで一覧を取得していた場合に、 list/detail というのを足したくなったとします。
その場合、
resources/task.go
に以下エンドポイントを足します。var _ = dsl.Resource("Tasks", func() { dsl.DefaultMedia(mediatypes.Task) dsl.BasePath("/tasks") dsl.Action("list", func() { dsl.Routing(dsl.GET("")) dsl.Description("Retrieve list of tasks") dsl.Response(design.OK, dsl.CollectionOf(mediatypes.Task)) }) dsl.Action("list_detailed", func() { <--足した部分 dsl.Routing(dsl.GET("/detail")) dsl.Description("Retrieve detail list of tasks") dsl.Response(design.OK, dsl.CollectionOf(mediatypes.Task)) })この状態で
goagen
するとどうなるのか・・・。具体的にはtopのtasks.go
はどうなるのか。・・・あれ?変わらないぞ?
ということでヘルプを見てみる。
% goagen bootstrap --help (git)-[develop] Equivalent to running the "app", "main", "client" and "swagger" commands. Usage: goagen bootstrap [flags] Flags: --force overwrite existing files -h, --help help for bootstrap --notest Prevent generation of test helpers --notool Prevent generation of cli tool --pkg string Name of generated Go package containing controllers supporting code (contexts, media types, user types etc.) (default "app") --regen regenerate scaffolding, maintaining controller implementations --tool string Name of generated tool (default "[API-name]-cli") --tooldir string Name of generated tool directory (default "tool") Global Flags: --debug enable debug mode, does not cleanup temporary files. -d, --design string design package import path -o, --out string output directory (default ".")
--regen
をすればいいのですね。でやってみました。すると、、、(以下、修正後のtasks.go)
// ListDetailed runs the list_detailed action. func (c *TasksController) ListDetailed(ctx *app.ListDetailedTasksContext) error { // TasksController_ListDetailed: start_implement // Put your logic here res := app.XLearningGoaCollection{} return ctx.OK(res) // TasksController_ListDetailed: end_implement }ちゃんとここだけ足されており、既存のロジック書いた部分は影響を受けていませんでした!!
これは非常に良かったです。まず問題1解決。
次。
エンドポイントのパスが変わったらどうなんの?
例えば、
/tasks/:taskID
とかのパスを、ある勢力からの圧力でどうしても /tasks/show/:taskID
にしてほしいんだよねっ!!!とか言われたらどうなるのか。その場合
resources/task.go
を修正することになりますね。変更前の resources/task.go
dsl.Action("show", func() { dsl.Routing(dsl.GET("/:taskID")) dsl.Description("Retrieve detail of task by specified ID") dsl.Params(func() { dsl.Param("taskID", design.Integer, "ID of task") }) dsl.Response(design.OK) dsl.Response(design.NotFound) dsl.Response(design.BadRequest, design.ErrorMedia) })変更後の resources/task.go
dsl.Action("show", func() { dsl.Routing(dsl.GET("/show/:taskID")) <-- ここを修正した dsl.Description("Retrieve detail of task by specified ID") dsl.Params(func() { dsl.Param("taskID", design.Integer, "ID of task") }) dsl.Response(design.OK) dsl.Response(design.NotFound) dsl.Response(design.BadRequest, design.ErrorMedia) })さて、ここで
goagen
すればいいのだと思うのですが、まずその直前のtopのtasks.goで、当該APIがどうなっているかというと・・・goagen再実行前の topのtasks.go
// Show runs the show action. func (c *TasksController) Show(ctx *app.ShowTasksContext) error { // TasksController_Show: start_implement // Put your logic here if ctx.TaskID == 0 { return ctx.NotFound() } res := &app.XLearningGoa{ ID: ctx.TaskID, Title: "example task title", Done: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } return ctx.OK(res) // TasksController_Show: end_implement }gogen 再実行前の app/controllers.go
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error { // Check if there was an error loading the request if err := goa.ContextError(ctx); err != nil { return err } // Build the context rctx, err := NewShowTasksContext(ctx, req, service) if err != nil { return err } return ctrl.Show(rctx) } service.Mux.Handle("GET", "/api/tasks/:taskID", ctrl.MuxHandler("show", h, nil)) service.LogInfo("mount", "ctrl", "Tasks", "action", "Show", "route", "GET /api/tasks/:taskID") <-- ここは関係しそうさて、
topのtasks.go
が変更されず、app/controllers.go
の当該箇所が正しく変更されればOKですよね。やってみます。
topのtasks.go
ロジックは維持されたままです。
// Show runs the show action. func (c *TasksController) Show(ctx *app.ShowTasksContext) error { // TasksController_Show: start_implement // Put your logic here if ctx.TaskID == 0 { return ctx.NotFound() } res := &app.XLearningGoa{ ID: ctx.TaskID, Title: "example task title", Done: false, CreatedAt: time.Now(), UpdatedAt: time.Now(), } return ctx.OK(res) // TasksController_Show: end_implement }app/controllers.go
こちらはちゃんと変わってますね。
// Check if there was an error loading the request if err := goa.ContextError(ctx); err != nil { return err } // Build the context rctx, err := NewShowTasksContext(ctx, req, service) if err != nil { return err } return ctrl.Show(rctx) } service.Mux.Handle("GET", "/api/tasks/show/:taskID", ctrl.MuxHandler("show", h, nil)) service.LogInfo("mount", "ctrl", "Tasks", "action", "Show", "route", "GET /api/tasks/show/:taskID")ということで、とりあえず上記の2点のようなケースには対応できてそうですね。
番外編
resources/task.go
で、もしdsl.Action
の第一引数を変えるとします。つまり以下のようなこと。dsl.Action("show", func() { <-- ここのshowを変えたい! dsl.Routing(dsl.GET("/show/:taskID")) dsl.Description("Retrieve detail of task by specified ID") dsl.Params(func() { dsl.Param("taskID", design.Integer, "ID of task") }) dsl.Response(design.OK) dsl.Response(design.NotFound) dsl.Response(design.BadRequest, design.ErrorMedia) })このようなケースは流石に無理でしたね。Showが消えて、Show2が出来、ロジックは初期化されてました。
とはいえ、それなりにちゃんと使えそうな気がするんですけどね。もう少しいろいろ検証してみます。
- 投稿日:2019-03-18T14:54:01+09:00
Golangの依存関係管理ツールメモ(dep)
参考:
公式:https://github.com/golang/dep/blob/master/README.mddepインストール
$ go get -u github.com/golang/dep/cmd/dep下記のフォルダに入っています。
$GOPATH/bin/dep $GOPATH/github.com/golang/depdepコマンド
helpでコマンド確認
$ dep help Dep is a tool for managing dependencies for Go projects Usage: "dep [command]" Commands: init Set up a new Go project, or migrate an existing one status Report the status of the project's dependencies ensure Ensure a dependency is safely vendored in the project version Show the dep version information check Check if imports, Gopkg.toml, and Gopkg.lock are in sync Examples: dep init set up a new project dep ensure install the project's dependencies dep ensure -update update the locked versions of all dependencies dep ensure -add github.com/pkg/errors add a dependency to the project Use "dep help [command]" for more information about a command.プロジェクト初期化
$ dep initパッケージ管理の初期化コマンド。Gopkg.lockとGopkg.tomlとvendorディレクトリを作成します。
・Gopkg.toml
ソース内でimportしているパッケージのリストです。・Gopkg.lock
パッケージのバージョンを指定。直接的なパッケージとそのパッケージが依存しているパッケージ全てを指定している。・vendorディレクトリ
プロジェクトで使用するパッケージがインストールされます.gitignoreにはvendorを除外しましょう。パッケージは都度インストールすれば良いのでgit管理する必要はありません。
$ vi .gitignore vendor/*パッケージインストール
$ dep ensureパッケージを追加した場合は、dep ensureコマンドを実行。
ソースを解析して追加のパッケージがある場合は更新してくれます。
最新版があってもvendorディレクトリにあるパッケージへの更新は行わない。$ dep ensure -update使用している全てのパッケージを最新版に更新してくれます。このタイミングで追加のパッケージがある場合は更新してくれます。
$ dep ensure -vendor-onlyGopkg.toml、Gopkg.lockに設定してあるバージョン、パッケージのみを対象にする。
新規のパッケージがあっても追加でインストールはしない。状態確認
$ dep status PROJECT CONSTRAINT VERSION REVISION LATEST PKGS USED github.com/gin-contrib/sse * 22d885f 1 github.com/gin-gonic/gin ^1.3.0 v1.3.0 b869fe1 v1.3.0 4 github.com/golang/protobuf * 5a0f697 1 github.com/json-iterator/go 1.0.0 1.0.0 36b1496 1.0.0 1 github.com/mattn/go-isatty * 57fdcb9 1 github.com/ugorji/go * c88ee25 1 golang.org/x/sys branch master branch master a2f829d a2f829d 1 gopkg.in/go-playground/validator.v8 v8.18.1 v8.18.1 5f57d22 v8.18.1 1 gopkg.in/yaml.v2 * a5b47d3 1 dep status 1.22s user 2.42s system 162% cpu 2.240 total上記はginのチュートリアルをしているプロジェクトの状態
アプリ実行
$ go run main.govendor配下のパッケージを利用してアプリ実行ができる。
- 投稿日:2019-03-18T09:30:00+09:00
Dockerに統合されたBuildKitのLLB (low-level builder)の仕様を探ってみよう
こんにちは。po3rin です。今回は前回の記事で解説し損ねた「そもそものLLBの中身はどうなってんねん」というところを解説します。
そもそもLLBとは
BuildKit は、LLB というプロセスの依存関係グラフを定義するために使用されるバイナリ中間言語を利用して。イメージをビルドしています。
なぜこの中間言語を挟むかというと、LLB は DAG 構造(上の画像のような非循環構造)を取ることにより、ステージごとの依存を解決し、並列実行可能な形で記述可能だからです。これにより、BuildKit を使った docker build は並列実行を可能にしています。
参考: Buildkit の Goのコードを読んで Dockerfile 抽象構文木から LLB を生成するフローを覗いてみよう!!
LLB の構造を覗く
まずはLLBの中身を探るために簡単なDockerfileからLLBに変換します。ここではLLBの構造を見やくするために
buildctl
コマンドを使います。buildctl を使えるようにする
インストールはこちらを参考にしてみて下さい。
参考: Docker に正式統合された BuildKit の buildctl コマンドの実行環境構築
buildctl でLLBを確認する
まずはDockerfileからLLBを生成して構造を目視で確認しましょう。まずは今回のターゲットになるDockerfileです。
FROM golang:1.12 AS stage0 ENV GO111MODULE on WORKDIR /go ADD ./po-go /go RUN go build -o go_binそしてGo言語の環境で下記を実行します。Go言語が分からなくても安心してください。Dockerfileを読み込んでbuildkitが提供する関数でLLBに変換して、見やすい形で標準出力に出しているだけです。
package main import ( "io/ioutil" "os" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" "github.com/moby/buildkit/util/appcontext" ) func main() { df, _ := ioutil.ReadFile("./Dockerfile") st, _, _ := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{}) def, _ := st.Marshal() llb.WriteTo(def, os.Stdout) }実行します。
go run main.go | buildctl debug dump-llb | jq .
出力は長いので折り畳んでおきます。
LLBのJSON
{ "Op": { "Op": { "Source": { "identifier": "docker-image://docker.io/docker/dockerfile-copy:v0.1.9@sha256:e8f159d3f00786604b93c675ee2783f8dc194bb565e61ca5788f6a6e9d304061", "attrs": { "image.recordtype": "internal" } } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b", "OpMetadata": { "description": { "llb.customname": "[internal] helper image for file operations" }, "caps": { "source.image": true } } } { "Op": { "Op": { "Source": { "identifier": "local://context", "attrs": { "local.followpaths": "[\"po-go\"]", "local.sharedkeyhint": "context", "local.unique": "0vlmkjq7ccgxx0c2pmhknu7gr" } } }, "constraints": {} }, "Digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa", "OpMetadata": { "description": { "llb.customname": "[internal] load build context" }, "caps": { "source.local": true, "source.local.followpaths": true, "source.local.sharedkeyhint": true, "source.local.unique": true } } } { "Op": { "Op": { "Source": { "identifier": "docker-image://docker.io/library/golang:1.12" } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "FROM golang:1.12", "llb.customname": "[1/3] FROM docker.io/library/golang:1.12" }, "caps": { "source.image": true } } } { "Op": { "inputs": [ { "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b", "index": 0 }, { "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "index": 0 }, { "digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa", "index": 0 } ], "Op": { "Exec": { "meta": { "args": [ "copy", "--unpack", "/src-0/po-go", "go" ], "cwd": "/dest" }, "mounts": [ { "input": 0, "dest": "/", "output": -1, "readonly": true }, { "input": 1, "dest": "/dest", "output": 0 }, { "input": 2, "selector": "./po-go", "dest": "/src-0/po-go", "output": -1, "readonly": true } ] } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "ADD ./po-go /go", "llb.customname": "[2/3] ADD ./po-go /go" }, "caps": { "exec.meta.base": true, "exec.mount.bind": true, "exec.mount.selector": true } } } { "Op": { "inputs": [ { "digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c", "index": 0 } ], "Op": { "Exec": { "meta": { "args": [ "/bin/sh", "-c", "go build -o go_bin" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GO111MODULE=on" ], "cwd": "/go" }, "mounts": [ { "input": 0, "dest": "/", "output": 0 } ] } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "RUN go build -o go_bin", "llb.customname": "[3/3] RUN go build -o go_bin" }, "caps": { "exec.meta.base": true, "exec.mount.bind": true } } } { "Op": { "inputs": [ { "digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40", "index": 0 } ], "Op": null }, "Digest": "sha256:33db869df4ee6fae4e29c22dcf328725c6ca6d20470680be6c6b0adf7e14cf3e", "OpMetadata": { "caps": { "constraints": true, "meta.description": true, "platform": true } } }LLBの中身をformat定義と比べながら見ていく
LLBの仕様はprotobufで定義されています。
https://github.com/moby/buildkit/blob/master/solver/pb/ops.proto先ほど説明した通り、LLBは有向非循環グラフの構造をとります。すなわち、ここで定義されているのは主に各ノード(接点。頂点)のデータフォーマット(Op)とノードを枝(エッジ)を表現するータフォーマット(Definition)です。まず基本的なところから見ていきましょう。Opはグラフ構造のノードを表しています。
// Op represents a vertex of the LLB DAG. message Op { // inputs is a set of input edges. repeated Input inputs = 1; oneof op { ExecOp exec = 2; SourceOp source = 3; CopyOp copy = 4; BuildOp build = 5; } Platform platform = 10; WorkerConstraints constraints = 11; }これは実際のLLBでは下の部分に対応します。
FROM golang:1.12
の一行がこのようにOpに変換されていることがわかります。{ "Op": { "Op": { "Source": { "identifier": "docker-image://docker.io/library/golang:1.12" } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "FROM golang:1.12", "llb.customname": "[1/3] FROM docker.io/library/golang:1.12" }, "caps": { "source.image": true } } }見るとわかる通り、Docker系の情報はOpMetadataの中にメタデータとして格納されるだけです。よってLLBがDockerfileの構造だけに依存している訳ではないことがわかります。
ここらでOpの種類を見てみましょう。Opの定義の
oneof op
を見るとわかる通り、実はOpの種類はわずか4種類しかありません。例えばExecOpは下記の部分です。"Op": { "inputs": [ // ... ], "Op": { "Exec": { "meta": { "args": [ "/bin/sh", "-c", "go build -o go_bin" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GO111MODULE=on" ], "cwd": "/go" }, "mounts": [ { "input": 0, "dest": "/", "output": 0 } ] } }, "platform": { // ... }, "constraints": {} }, "Digest": "sha256:f2dd48bf7656705cfbefa478dca927bbe298d9adabd39576bfb7131ff57fd261", "OpMetadata": { // ... }meta情報を見ると、argsにRUNで指定したコマンドが、cwdにWORKDIRの値が、envにENVで指定した環境変数がセットされています。実は、LLBの世界での環境変数ENVはExecOpだけに紐づいており、全てのOpに共通した設定というのができない構造になっています。
LLBが有向非循環構造になっているのを確認する
さてLLBの構成因子Opを確認したところでこれらが有向非循環グラフになっていることを確認しましょう。ポイントはdigestです。もう一度LLBを見てみましょう。
{ "Op": { "Op": { // ... }, "Digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5", "OpMetadata": { // ... } }Opには一つdigestがセットされています。一方でこのOpに依存するOpをみてみましょう.
{ "Op": { "inputs": [ { "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b", "index": 0 }, { "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "index": 0 }, { "digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5", "index": 0 } ], "Op": { // ... }, "platform": { // ... }, "constraints": {} }, "Digest": "sha256:d9fce5f43b1af2d2b9192de6eae55a2ea79a190c058cb9cfddb1af3ffbdb21d2", "OpMetadata": { // ... } } }inputsとしてエッジで紐付くOpのdigestの値が記載されています。このようにしてLLBはDAG構造を表現しています。例えば。下記のようなDockerfileは
FROM golang:1.12 AS stage0 WORKDIR /go ADD ./ /go RUN go build -o stage0_bin FROM golang:1.12 AS stage1 WORKDIR /go ADD ./ /go RUN go build -o stage1_bin FROM golang:1.12 COPY --from=stage0 /go/stage0_bin / COPY --from=stage1 /go/stage1_bin /digest値を追っていくと下記のようなDAG構造を取っていることがわかります。
なぜLLBを挟むことで並列化を実現できるかが一目でわかりますね。
まとめ
簡単にLLBの構造を追ってみて、なぜBuildKitでビルドの並列化ができるのかをみていきました。docker build の内部的な理解や、buildkikのコードリーディングをする際などに役立つはずです。
- 投稿日:2019-03-18T09:30:00+09:00
Dockerに統合されたBuildKitのLLB (low-level builder)の仕様を探ってみよう。
こんにちは。po3rin です。今回は前回の記事で解説し損ねた「そもそものLLBの中身はどうなってんねん」というところを解説します。
そもそもLLBとは
BuildKit は、LLB というプロセスの依存関係グラフを定義するために使用されるバイナリ中間言語を利用して。イメージをビルドしています。
なぜこの中間言語を挟むかというと、LLB は DAG 構造(上の画像のような非循環構造)を取ることにより、ステージごとの依存を解決し、並列実行可能な形で記述可能だからです。これにより、BuildKit を使った docker build は並列実行を可能にしています。
参考: Buildkit の Goのコードを読んで Dockerfile 抽象構文木から LLB を生成するフローを覗いてみよう!!
LLB の構造を覗く
まずはLLBの中身を探るために簡単なDockerfileからLLBに変換します。ここではLLBの構造を見やくするために
buildctl
コマンドを使います。buildctl を使えるようにする
インストールはこちらを参考にしてみて下さい。
参考: Docker に正式統合された BuildKit の buildctl コマンドの実行環境構築
buildctl でLLBを確認する
まずはDockerfileからLLBを生成して構造を目視で確認しましょう。まずは今回のターゲットになるDockerfileです。
FROM golang:1.12 AS stage0 ENV GO111MODULE on WORKDIR /go ADD ./po-go /go RUN go build -o go_binそしてGo言語の環境で下記を実行します。Go言語が分からなくても安心してください。Dockerfileを読み込んでbuildkitが提供する関数でLLBに変換して、見やすい形で標準出力に出しているだけです。
package main import ( "io/ioutil" "os" "github.com/moby/buildkit/client/llb" "github.com/moby/buildkit/frontend/dockerfile/dockerfile2llb" "github.com/moby/buildkit/util/appcontext" ) func main() { df, _ := ioutil.ReadFile("./Dockerfile") st, _, _ := dockerfile2llb.Dockerfile2LLB(appcontext.Context(), df, dockerfile2llb.ConvertOpt{}) def, _ := st.Marshal() llb.WriteTo(def, os.Stdout) }実行します。
go run main.go | buildctl debug dump-llb | jq .
出力は長いので折り畳んでおきます。
LLBのJSON
{ "Op": { "Op": { "Source": { "identifier": "docker-image://docker.io/docker/dockerfile-copy:v0.1.9@sha256:e8f159d3f00786604b93c675ee2783f8dc194bb565e61ca5788f6a6e9d304061", "attrs": { "image.recordtype": "internal" } } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b", "OpMetadata": { "description": { "llb.customname": "[internal] helper image for file operations" }, "caps": { "source.image": true } } } { "Op": { "Op": { "Source": { "identifier": "local://context", "attrs": { "local.followpaths": "[\"po-go\"]", "local.sharedkeyhint": "context", "local.unique": "0vlmkjq7ccgxx0c2pmhknu7gr" } } }, "constraints": {} }, "Digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa", "OpMetadata": { "description": { "llb.customname": "[internal] load build context" }, "caps": { "source.local": true, "source.local.followpaths": true, "source.local.sharedkeyhint": true, "source.local.unique": true } } } { "Op": { "Op": { "Source": { "identifier": "docker-image://docker.io/library/golang:1.12" } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "FROM golang:1.12", "llb.customname": "[1/3] FROM docker.io/library/golang:1.12" }, "caps": { "source.image": true } } } { "Op": { "inputs": [ { "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b", "index": 0 }, { "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "index": 0 }, { "digest": "sha256:a883cfd2f89c3fb66a76a7d88935d83686830c0c16b5e7dcdf35b93a94ac09aa", "index": 0 } ], "Op": { "Exec": { "meta": { "args": [ "copy", "--unpack", "/src-0/po-go", "go" ], "cwd": "/dest" }, "mounts": [ { "input": 0, "dest": "/", "output": -1, "readonly": true }, { "input": 1, "dest": "/dest", "output": 0 }, { "input": 2, "selector": "./po-go", "dest": "/src-0/po-go", "output": -1, "readonly": true } ] } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "ADD ./po-go /go", "llb.customname": "[2/3] ADD ./po-go /go" }, "caps": { "exec.meta.base": true, "exec.mount.bind": true, "exec.mount.selector": true } } } { "Op": { "inputs": [ { "digest": "sha256:980921cdf627d58bc3fe76e71cae9bb81d3aa769db0b4bbcc5095786e720906c", "index": 0 } ], "Op": { "Exec": { "meta": { "args": [ "/bin/sh", "-c", "go build -o go_bin" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GO111MODULE=on" ], "cwd": "/go" }, "mounts": [ { "input": 0, "dest": "/", "output": 0 } ] } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "RUN go build -o go_bin", "llb.customname": "[3/3] RUN go build -o go_bin" }, "caps": { "exec.meta.base": true, "exec.mount.bind": true } } } { "Op": { "inputs": [ { "digest": "sha256:8c78ad1616ec77e3c8d847abf071adf565058033586f5ddf78cb7d748059cb40", "index": 0 } ], "Op": null }, "Digest": "sha256:33db869df4ee6fae4e29c22dcf328725c6ca6d20470680be6c6b0adf7e14cf3e", "OpMetadata": { "caps": { "constraints": true, "meta.description": true, "platform": true } } }LLBの中身をformat定義と比べながら見ていく
LLBの仕様はprotobufで定義されています。
https://github.com/moby/buildkit/blob/master/solver/pb/ops.proto先ほど説明した通り、LLBは有向非循環グラフの構造をとります。すなわち、ここで定義されているのは主に各ノード(接点。頂点)のデータフォーマット(Op)とノードを枝(エッジ)を表現するータフォーマット(Definition)です。まず基本的なところから見ていきましょう。Opはグラフ構造のノードを表しています。
// Op represents a vertex of the LLB DAG. message Op { // inputs is a set of input edges. repeated Input inputs = 1; oneof op { ExecOp exec = 2; SourceOp source = 3; CopyOp copy = 4; BuildOp build = 5; } Platform platform = 10; WorkerConstraints constraints = 11; }これは実際のLLBでは下の部分に対応します。
FROM golang:1.12
の一行がこのようにOpに変換されていることがわかります。{ "Op": { "Op": { "Source": { "identifier": "docker-image://docker.io/library/golang:1.12" } }, "platform": { "Architecture": "amd64", "OS": "darwin" }, "constraints": {} }, "Digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "OpMetadata": { "description": { "com.docker.dockerfile.v1.command": "FROM golang:1.12", "llb.customname": "[1/3] FROM docker.io/library/golang:1.12" }, "caps": { "source.image": true } } }見るとわかる通り、Docker系の情報はOpMetadataの中にメタデータとして格納されるだけです。よってLLBがDockerfileの構造だけに依存している訳ではないことがわかります。
ここらでOpの種類を見てみましょう。Opの定義の
oneof op
を見るとわかる通り、実はOpの種類はわずか4種類しかありません。例えばExecOpは下記の部分です。"Op": { "inputs": [ // ... ], "Op": { "Exec": { "meta": { "args": [ "/bin/sh", "-c", "go build -o go_bin" ], "env": [ "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", "GO111MODULE=on" ], "cwd": "/go" }, "mounts": [ { "input": 0, "dest": "/", "output": 0 } ] } }, "platform": { // ... }, "constraints": {} }, "Digest": "sha256:f2dd48bf7656705cfbefa478dca927bbe298d9adabd39576bfb7131ff57fd261", "OpMetadata": { // ... }meta情報を見ると、argsにRUNで指定したコマンドが、cwdにWORKDIRの値が、envにENVで指定した環境変数がセットされています。実は、LLBの世界での環境変数ENVはExecOpだけに紐づいており、全てのOpに共通した設定というのができない構造になっています。
LLBが有向非循環構造になっているのを確認する
さてLLBの構成因子Opを確認したところでこれらが有向非循環グラフになっていることを確認しましょう。ポイントはdigestです。もう一度LLBを見てみましょう。
{ "Op": { "Op": { // ... }, "Digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5", "OpMetadata": { // ... } }Opには一つdigestがセットされています。一方でこのOpに依存するOpをみてみましょう.
{ "Op": { "inputs": [ { "digest": "sha256:e391a01c9f93647605cf734a2b0b39f844328cc22c46bde7535cec559138357b", "index": 0 }, { "digest": "sha256:bd2b1f44d969cd5ac71886b9f842f94969ba3b2db129c1fb1da0ec5e9ba30e67", "index": 0 }, { "digest": "sha256:b84b3a1967b1f5741f950a7b8b5d6145d791cccb414b0c044f19b432e306def5", "index": 0 } ], "Op": { // ... }, "platform": { // ... }, "constraints": {} }, "Digest": "sha256:d9fce5f43b1af2d2b9192de6eae55a2ea79a190c058cb9cfddb1af3ffbdb21d2", "OpMetadata": { // ... } } }inputsとしてエッジで紐付くOpのdigestの値が記載されています。このようにしてLLBはDAG構造を表現しています。例えば。下記のようなDockerfileは
FROM golang:1.12 AS stage0 WORKDIR /go ADD ./ /go RUN go build -o stage0_bin FROM golang:1.12 AS stage1 WORKDIR /go ADD ./ /go RUN go build -o stage1_bin FROM golang:1.12 COPY --from=stage0 /go/stage0_bin / COPY --from=stage1 /go/stage1_bin /digest値を追っていくと下記のようなDAG構造を取っていることがわかります。
なぜLLBを挟むことで並列化を実現できるかが一目でわかりますね。
まとめ
簡単にLLBの構造を追ってみて、なぜBuildKitでビルドの並列化ができるのかをみていきました。docker build の内部的な理解や、buildkikのコードリーディングをする際などに役立つはずです。
- 投稿日:2019-03-18T01:55:07+09:00
Goでインポートしたパッケージが消されるときの対処法
問題
VSCodeでGo言語でプログラムを作成しているとき、
パッケージを利用しようとしてimportに参照先を追加しても削除されます。main.go
package main import "github.com/kaleidot725/suezo"環境
ちなみに次の環境を利用しています。
項目 内容 IDE Visual Studio Code 1.32.1 Extension Go Extension Golang Tdd 解決策
どうやらフォーマッタの問題のようです、
フォーマッタをgoreturnsからgofmtに変更したら解決しました。1.「Open Folder Settings」を開く
2. 「Format Tools」を「goreturs」から「gofmt」に変える設定へのたどり着き方メモ
?Open Folder SettingはExplorerを右クリックで開く
?Go Formatと入力するとFormat Toolsが出てくる