20190318のGoに関する記事は5件です。

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が出来、ロジックは初期化されてました。

とはいえ、それなりにちゃんと使えそうな気がするんですけどね。もう少しいろいろ検証してみます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Golangの依存関係管理ツールメモ(dep)

参考:
公式:https://github.com/golang/dep/blob/master/README.md

depインストール

$ go get -u github.com/golang/dep/cmd/dep

下記のフォルダに入っています。

$GOPATH/bin/dep
$GOPATH/github.com/golang/dep

depコマンド

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-only

Gopkg.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.go

vendor配下のパッケージを利用してアプリ実行ができる。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のコードリーディングをする際などに役立つはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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のコードリーディングをする際などに役立つはずです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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を右クリックで開く

Screen Shot 2019-03-18 at 1.46.36.png

?Go Formatと入力するとFormat Toolsが出てくる

Screen Shot 2019-03-18 at 1.54.43.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む