JavaでUnix Domain Socketを使ってみる(Dockerのコンテナ一覧を取得する)
はじめに
Java 16でUnix Domain Socketのサポートが入っています(JEP 380)。前から興味はありつつもかなか触れてなかったので今回少し触ってみようかと思います。
お題として、以下の2つをやってみようと思います。
Unix Domain Socket?
Unix Domain Socketは単一マシンで複数プロセスが、効率の良い通信を行なうためのソケットインターフェースです。
TCP/IPソケットと似たようなインターフェースですが、インターネットプロトコルの代わりにファイルシステムを利用しています。
以下のような特徴があるようです。
少し前にはなりますが、WindowsでもUnix Domain Socketサポートが入ったみたいです。
JavaでUnix Domain Socketを使ってみる
環境
$ uname -srvmpio Linux 5.4.0-89-generic #100-Ubuntu SMP Fri Sep 24 14:50:10 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.3 LTS Release: 20.04 Codename: focal $ java --version openjdk 17 2021-09-14 OpenJDK Runtime Environment (build 17+35-2724) OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing)
Echoサーバーを書く
文字列を受け取って、その文字列に"Sent words are:"と言うプリフィクスを付けて返してプロセスを終了するシンプルなEchoサーバとそのクライアントを書いてみます。
ここを参考にコードを書いていきます。
基本的にはTCP/IPのソケットを開くときと似たような感じで利用できるみたいです。
サーバー側は以下のような感じ
import java.io.IOException; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; public class UnixDomainSocketServer { public static void main(String[] args) { Path path = Path.of("/tmp", ".unixserver"); UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path); try (ServerSocketChannel server = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) { Files.deleteIfExists(path); server.bind(socketAddress); ByteBuffer buf = ByteBuffer.allocate(1024); try (SocketChannel channel = server.accept()) { channel.read(buf); buf.flip(); String input = StandardCharsets.UTF_8.decode(buf).toString(); System.out.println("input = " + input); String response = "Sent words are: %s".formatted(input); channel.write(ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8))); } catch (IOException e) { e.printStackTrace(); } } catch (IOException e) { e.printStackTrace(); } } }
UnixDomainSocketAddress
を使ってファイルシステムのアドレスをバインドすることできます。
また、Unix Domain Socketを利用するときはSocketを開く際にプロトコルファミリー(StandardProtocolFamily.UNIX)
)を指定してやる必要があるようです。
続いてクライアント側は以下のような感じです。
import java.io.IOException; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; public class UnixDomainSocketClient { public static void main(String[] args) { Path path = Path.of("/tmp", ".unixserver"); UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path); try (final SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) { channel.connect(socketAddress); final String input = "Hello, UNIX Domain Socket"; System.out.println("input = " + input); channel.write(ByteBuffer.wrap(input.getBytes(StandardCharsets.UTF_8))); ByteBuffer buf = ByteBuffer.allocate(1024); channel.read(buf); buf.flip(); System.out.println("response = " + StandardCharsets.UTF_8.decode(buf)); } catch (IOException e) { e.printStackTrace(); } } }
サーバと同様にUnixDomainSocketAddress
を使ってサーバに繋ぐができます。
これらをサーバ→クライアントの順で実行すると、それぞれで以下のような出力を得られます。
サーバ側
input = Hello, UNIX Domain Socket
クライアント側
input = Hello, UNIX Domain Socket response = Sent words are: Hello, UNIX Domain Socket
Dockerのコンテナ一覧を取得する
Docker エンジンはUnix Domain Socketを利用して、cURLなどで操作することができます。
各バージョンのAPIドキュメンテーションはこちらから確認できます。
少しDockerのバージョンが低いですがcURLでDockerエンジンを操作するブログも過去に書いているのでよかったら読んでみてください。
今回は、コンテナの一覧を取得するJavaのコードを書いてみます。
import java.io.IOException; import java.net.StandardProtocolFamily; import java.net.UnixDomainSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.StandardCharsets; import java.nio.file.Path; public class DockerClient { public static void main(String[] args) { Path path = Path.of("/var/run", "docker.sock"); UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(path); final String request = """ GET /containers/json HTTP/1.1 Host: localhost User-Agent: curl/7.68.0 Accept: */* """; try (SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX)) { channel.connect(socketAddress); channel.write(ByteBuffer.wrap(request.getBytes(StandardCharsets.UTF_8))); ByteBuffer buf = ByteBuffer.allocate(1024); channel.read(buf); buf.flip(); System.out.println("============= response ================"); System.out.println(StandardCharsets.UTF_8.decode(buf)); } catch (IOException e) { e.printStackTrace(); } } }
さほど複雑なことはしておらず、先程のクライアントのコードとほぼ同じです。ただHTTPのリクエスト形式で動いているコンテナの一覧を取得するリクエストを送っています。
(cURLのリクエストをパクったので、User-Agentがcurlになってるのはご愛嬌ということで。。。。)
このプログラムをコンテナが1つも動いていない状態で実行すると以下のような出力になります。
============= response ================ HTTP/1.1 200 OK Api-Version: 1.41 Content-Type: application/json Docker-Experimental: true Ostype: linux Server: Docker/20.10.9 (linux) Date: Sat, 23 Oct 2021 06:03:09 GMT Content-Length: 3 []
今度は適当なコンテナを立ち上げて再度コードを実行してみます。
$ docker run --name tmp-nginx-container -d nginx c1b1f0d91f368fcbe13113160dd06c1a755dc662e1d3ddfe82239e0775a3c6a1 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES c1b1f0d91f36 nginx "/docker-entrypoint.…" 4 seconds ago Up 3 seconds 80/tcp tmp-nginx-container
コードを実行すると今度は以下のような出力が得られ、実行されているコンテナの情報を取得が行えているのがわかります。
============= response ================ HTTP/1.1 200 OK Api-Version: 1.41 Content-Type: application/json Docker-Experimental: true Ostype: linux Server: Docker/20.10.9 (linux) Date: Sat, 23 Oct 2021 06:07:44 GMT Content-Length: 932 [{"Id":"c1b1f0d91f368fcbe13113160dd06c1a755dc662e1d3ddfe82239e0775a3c6a1","Names":["/tmp-nginx-container"],"Image":"nginx","ImageID":"sha256:08b152afcfae220e9709f00767054b824361c742ea03a9fe936271ba520a0a4b","Command":"/docker-entrypoint.sh nginx -g 'daemon off;'","Created":1634969175,"Ports":[{"PrivatePort":80,"Type":"tcp"}],"Labels":{"maintainer":"NGINX Docker Maintainers <docker-maint@nginx.com>"},"State":"running","Status":"Up About a minute","HostConfig":{"NetworkMode":"default"},"NetworkSettings":{"Networks":{"bridge":{"IPAMConfig":null,"Links":null,"Aliases":null,"NetworkID":"1ad0f3f401ba159560bd54403e670da4fd92e07b24f798cb2a97acc5b88e67d5","EndpointID":"f201ce886f9160d417fb4d9141253d591b54b9e2929884638321d057026f0436","Gateway":"172.17.0.1","IPAddress":"172.17.0.2","IPPrefixLen":16,"IPv6Gateway":"","Glob
Echoのロガーを使う
はじめに
GoのフレームワークはEchoを使うことが最近は多いのですが、そのログ周りの機能を試してみようかと思います。
Echoのロガー
Echoにはロガーに関するサポートが以下の2つあります。
- Logger Middleware : アクセスログを出力するミドルウェア
- Logger : 通常のロガーデフォルトではJsonでログを出力してくれる
このブログでは2つ両方とも試してみたいと思います。
また、リクエストにIDを割与えるRequest ID Middlewareも存在しておりこちらもLoggerに少し関わる機能を持っているので試してみようと思います。
動かす
環境
今回の動作環境は以下のような感じです。
$ go version go version go1.17.1 linux/amd64 $ uname -srvmpio Linux 5.4.0-88-generic #99-Ubuntu SMP Thu Sep 23 17:29:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.3 LTS Release: 20.04 Codename: focal
このLogger MiddlewareはLoggerWithConfig
でカスタマイズすることができます。
例えば以下のようにすると任意のフォーマットでよりシンプルなロガーを出力することができます。
server go
func main() { e := echo.New() // 削除 //e.Use(middleware.Logger()) // 追加 e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "time=${time_rfc3339_nano}, method=${method}, uri=${uri}, status=${status}\n", })) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
出力されるログは次の通りになります。
time=2021-10-15T08:56:03.378067938+09:00, method=GET, uri=/, status=200
サンプルアプリの作成
ベースとなるプロジェクトとサンプルアプリを作成します。
公式にあるやり方をさっくりとなぞります。
$ mkdir echo-logger && cd echo-logger $ go mod init echo-logger go: creating new go.mod: module echo-logger $ go get github.com/labstack/echo/v4 go get: added github.com/labstack/echo/v4 v4.6.1 go get: added github.com/labstack/gommon v0.3.0 go get: added github.com/mattn/go-colorable v0.1.8 go get: added github.com/mattn/go-isatty v0.0.14 go get: added github.com/valyala/bytebufferpool v1.0.0 go get: added github.com/valyala/fasttemplate v1.2.1 go get: added golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 go get: added golang.org/x/net v0.0.0-20210913180222-943fd674d43e go get: added golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 go get: added golang.org/x/text v0.3.7
次に以下のファイルを作成してEchoのインスタンスを作りハンドラーを登録します。
server.go
package main import ( "net/http" "github.com/labstack/echo/v4" ) func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
アプリを起動します。
$ go run server.go ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.6.1 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:1323 $ curl localhost:1323 Hello, World!
これで下準備は完了です。
Logger Middlewareを使う
前述の通り、Logger Middlewareはアクセスログを出力してくれます。
使い方は超簡単で、以下のように
server.go
package main import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "net/http" ) func main() { e := echo.New() // 追加 e.Use(middleware.Logger()) e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
このままでは必要な依存がまだ入っていないので、以下のコマンドを実行します。
$ go get -t .
サーバを再起動し、先ほどと同じようにcURLでアクセスすると今度はアクセスログが出力されることが確認できます。
$ go run server.go ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.6.1 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:1323 {"time":"2021-10-15T08:44:10.73485721+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"curl/7.68.0","status":200,"error":"","latency":7637,"latency_human":"7.637µs","bytes_in":0,"bytes_out":13}
デフォルトのログの設定は以下のようになっているみたいです。
DefaultLoggerConfig = LoggerConfig{ Skipper: DefaultSkipper, Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` + `"host":"${host}","method":"${method}","uri":"${uri}","user_agent":"${user_agent}",` + `"status":${status},"error":"${error}","latency":${latency},"latency_human":"${latency_human}"` + `,"bytes_in":${bytes_in},"bytes_out":${bytes_out}}` + "\n", CustomTimeFormat: "2006-01-02 15:04:05.00000", }
Request ID Middlewareを使う
Request ID Middlewareを使えばRequestに対してユニークなIDを割り振ることができます。
このミドルウェアはクライアント側がX-Request-ID
のリクエストヘッダーを付けている場合そのIDを利用し、ない場合はデフォルトでrandom.String(32)
を利用してIDを割り振ります。
本来は分散トレーシングがメインな理由で用いられるものかと推測していますが、このミドルウェアを利用しているとLogger MiddlewareでもRequest IDが出力れるようになりログを追うときに何かと便利です。
こちらも使い方は簡単で、以下のようにミドルウェアを追加します。
server.go
package main import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" "net/http" ) func main() { e := echo.New() // 追加 e.Use(middleware.RequestID()) e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "id=${id}, time=${time_rfc3339_nano}, method=${method}, uri=${uri}, status=${status}\n", })) e.Logger.SetLevel(log.INFO) e.Logger.Info("hello") e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
サーバを再起動し、先ほどと同じようにcURLでアクセスすると今度はアクセスログにIDが含まれていることが確認できます。
# Request ID Middlewareを使っていない時 {"time":"2021-10-15T08:44:10.73485721+09:00","id":"","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"curl/7.68.0","status":200,"error":"","latency":7637,"latency_human":"7.637µs","bytes_in":0,"bytes_out":13} # Request ID Middlewareを使った時 {"time":"2021-10-15T09:09:08.795038871+09:00","id":"DIO17Vgnl1NN9sxnoZPDJNXiSf4o4p53","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"curl/7.68.0","status":200,"error":"","latency":78319,"latency_human":"78.319µs","bytes_in":0,"bytes_out":13}
id
の項目に値が入っているのを確認できます。
今度はX-Request-ID
を付与して、リクエストを送ってみます。
$ curl -H "X-Request-ID: 12345" localhost:1323 Hello, World!
するとログ出力は以下のようになります。
{"time":"2021-10-15T09:26:56.894261759+09:00","id":"12345","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"curl/7.68.0","status":200,"error":"","latency":43280,"latency_human":"43.28µs","bytes_in":0,"bytes_out":13}
X-Request-ID
の値が入ってますね。
Loggerを使う
最後にEchoのLoggerを使ってみます。
これはEchoのインスタンスに生えています。
具体的には、以下のようにします。
package main import ( "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" "github.com/labstack/gommon/log" "net/http" ) func main() { e := echo.New() e.Use(middleware.RequestID()) e.Use(middleware.Logger()) // 追加 if l, ok := e.Logger.(*log.Logger); ok { l.SetLevel(log.INFO) } e.GET("/", func(c echo.Context) error { // 追加 e.Logger.Info("hello func is called") return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
ポイントは、e.Logger.SetLevel(log.INFO)
のところで、EchoのロガーはデフォルトでログレベルがERROR
になるのでそれ以上のレベルのエラーを出力したい場合は明示的に設定してやる必要があります。
アプリを再起動してcURLを叩くと以下のようなログが出力されているのが確認できます。
{"time":"2021-10-15T12:26:21.21687368+09:00","level":"INFO","prefix":"echo","file":"server.go","line":"20","message":"hello func is called"}
この辺まだ理解が甘いところではあるのですが、おそらくレイヤリングの設計をしているアプリケーション内で使っていくにはEchoのコンテキストを引き回していくか、DIして使うのが基本的な感じになるかなと想像しています。(もっとよいやり方があればコメントとかで教えてくださると嬉しいです)
このロガーのフォーマットを変えたい場合はEcho#Logger.SetHeader(string)
で行なうことができます。
例えば以下のように利用します。
func main() { e := echo.New() e.Use(middleware.RequestID()) e.Use(middleware.Logger()) if l, ok := e.Logger.(*log.Logger); ok { // 追加 l.SetHeader("${time_rfc3339} ${level}") l.SetLevel(log.INFO) } e.GET("/", func(c echo.Context) error { // 追加 e.Logger.Info("hello func is called") return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
アプリを再起動してcURLを叩くと以下のようなログが出力されているのが確認できます。
2021-10-15T12:32:48+09:00 INFO hello func is called
ここで、利用できるタグが以下のものがあるみたいです。
- time_rfc3339
- time_rfc3339_nano
- level
- prefix
- long_file
- short_file
- line
1つ1つは名前の通りなので説明はしませんが、大雑把な使い方は全体を通してこんな感じになってるみたいです。
SkaffoldのPipeline Stagesについてまとめる
はじめに
自分が所属する組織ではSkaffoldを開発ワークフローのツールとして導入しているのですが、しっかり使っていく上で動作の基本をきちんと抑えておこうかと。
今まででは、Javaのアプリをデプロイしてデバッグしてみたり、Helmを使って環境変数の切り替えをやってみたりやりたいことベースで学んでいたんですが、ツールの動作に関する全体感を抑えておこうかと思いました。
基本はSkaffold Pipeline Stagesに書かれる内容を自分が気になったところに関してまとめたり、動かしたりして深堀する感じで行なう感じでやろうかと思います。
より正確な情報は本家の方を参照するようにお願いします。
特定の文字列を返すだけのサンプルアプリを1つ作って、このアプリにSkaffoldを適用してそれぞれのStageで提供される機能を使ってみます。
動かすサンプルアプリはGoでEchoを使って作ろうかと思います。
Skaffold Pipeline Stagesについて
Skaffoldには以下のようなパイプラインのステージがあります。
Stage名 | 説明 |
---|---|
Init | Skaffoldプロジェクトの作成 |
Build | イメージのビルド。様々なビルダーを利用することが可能 |
Tag | ビルドしたイメージのタグ付け。様々なポリシーでタグ付けを行なうことが可能 |
Test | テスターを用いたテストの実行する |
Deploy | アプリをk8sクラスターにデプロイする。kubectl、Helm、kustomize等を用いることが可能 |
File Sync | ローカルのファイルを直接コンテナにシンクする |
Log Tailing | デプロイしたアプリやワークフローのログをテールする |
Port Forwarding | Serviceや任意のリソースをlocalhostへポートフォワードする |
Lifecycle Hooks | Skaffoldの実行ライフサイクルの中のざまざまなイベントでコードをHookする |
Clean Up | リソースやイメージのクリーンアップ |
Skaffoldのコマンドによって実行するステージが違っており、スキップ可能なものもあります。
各ステージの機能を少しほってみる(ものによっては動かしてみる)
動作環境
今回クラスターはminikube(docker driver)を用いて作成します。
作ったクラスターやその他のバージョンは以下の通り
$ uname -srvmpio Linux 5.4.0-88-generic #99-Ubuntu SMP Thu Sep 23 17:29:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.3 LTS Release: 20.04 Codename: focal $ minikube version minikube version: v1.23.2 commit: 0a0ad764652082477c00d51d2475284b5d39ceed $ kubectl version -o yaml clientVersion: buildDate: "2021-09-15T21:38:50Z" compiler: gc gitCommit: 8b5a19147530eaac9476b0ab82980b4088bbc1b2 gitTreeState: clean gitVersion: v1.22.2 goVersion: go1.16.8 major: "1" minor: "22" platform: linux/amd64 serverVersion: buildDate: "2021-09-15T21:32:41Z" compiler: gc gitCommit: 8b5a19147530eaac9476b0ab82980b4088bbc1b2 gitTreeState: clean gitVersion: v1.22.2 goVersion: go1.16.8 major: "1" minor: "22" platform: linux/amd64 $ docker version Client: Docker Engine - Community Version: 20.10.8 API version: 1.41 Go version: go1.16.6 Git commit: 3967b7d Built: Fri Jul 30 19:54:27 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12) Go version: go1.16.6 Git commit: 75249d8 Built: Fri Jul 30 19:52:33 2021 OS/Arch: linux/amd64 Experimental: true containerd: Version: 1.4.10 GitCommit: 8848fdb7c4ae3815afcc990a8a99d663dda1b590 runc: Version: 1.0.2 GitCommit: v1.0.2-0-g52b36a2 docker-init: Version: 0.19.0 GitCommit: de40ad0 $ go version go version go1.17.1 linux/amd64
また、Skaffoldのバージョンは以下のものを用います。
$ skaffold version v1.33.0
Skaffoldのインストールに関してはこちらをご確認ください。
サンプルアプリとDockerfileを用意する
以下のコマンドを実行してサンプルアプリを作成します。
$ mkdir skaffold-demo && cd skaffold-demo $ go mod init skaffold-demo $ go get github.com/labstack/echo/v4 go: downloading github.com/labstack/echo/v4 v4.6.1 go get: added github.com/labstack/echo/v4 v4.6.1 go get: added github.com/labstack/gommon v0.3.0 go get: added github.com/mattn/go-colorable v0.1.8 go get: added github.com/mattn/go-isatty v0.0.14 go get: added github.com/valyala/bytebufferpool v1.0.0 go get: added github.com/valyala/fasttemplate v1.2.1 go get: added golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 go get: added golang.org/x/net v0.0.0-20210913180222-943fd674d43e go get: added golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 go get: added golang.org/x/text v0.3.7
以下のように文字列を返すだけのサーバを記述します。
server.go
package main import ( "net/http" "github.com/labstack/echo/v4" ) func main() { e := echo.New() e.GET("/hello", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World!") }) e.Logger.Fatal(e.Start(":1323")) }
アプリを起動します。
$ go run server.go ____ __ / __/___/ / ___ / _// __/ _ \/ _ \ /___/\__/_//_/\___/ v4.6.1 High performance, minimalist Go web framework https://echo.labstack.com ____________________________________O/_______ O\ ⇨ http server started on [::]:1323 $ curl localhost:1323/hello Hello, World!
次にアプリのイメージをビルドするDockerfileを作成します。
FROM golang:1.17 as buildImage ENV CGO_ENABLED=0 ENV GOOS=linux ENV GOARCH=amd64 WORKDIR /usr/src COPY . . RUN go build -o skaffold-demo-app server.go FROM alpine:3.14.2 COPY --from=buildImage /usr/src/skaffold-demo-app / ENTRYPOINT ["/skaffold-demo-app"]
イメージをビルドして起動してみます。
(docker buildx install
コマンドでBuildxを有効化してます)
$ docker build -t skaffold-demo-app . [+] Building 18.9s (14/14) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 300B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load metadata for docker.io/library/alpine:3.14.2 2.2s => [internal] load metadata for docker.io/library/golang:1.17 2.2s => [auth] library/golang:pull token for registry-1.docker.io 0.0s => [auth] library/alpine:pull token for registry-1.docker.io 0.0s => [buildimage 1/4] FROM docker.io/library/golang:1.17@sha256:45d45a39258425b0386643efc863b3b3c1481173d64ec6151b18d48b565df9a0 0.0s => CACHED [stage-1 1/2] FROM docker.io/library/alpine:3.14.2@sha256:e1c082e3d3c45cccac829840a25941e679c25d438cc8412c2fa221cf1a824e6a 0.0s => [internal] load build context 0.1s => => transferring context: 6.86MB 0.1s => CACHED [buildimage 2/4] WORKDIR /usr/src 0.0s => [buildimage 3/4] COPY . . 0.0s => [buildimage 4/4] RUN go build -o skaffold-demo-app server.go 16.4s => [stage-1 2/2] COPY --from=buildImage /usr/src/skaffold-demo-app / 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:ecb267c14c4664a488afe8586e64c5bf6b80b1715b6a86e4e9efb626b6d3a279 0.0s => => naming to docker.io/library/skaffold-demo-app $ docker run -d -p 1323:1323 skaffold-demo-app 1fd8dc30d0e0e19ec439ebacc5a411427ab42d06dab5aa4efd93fd86a969fbc0 $ curl localhost:1323/hello Hello, World!
下準備はこれで完了です。
各ステージをみていく
Init
先ずはInit ステージからみていきます。
前述の通りこのステージではSkaffoldプロジェクトの作成を行います。
Initステージはskaffold init
コマンドで実行します。
skaffold init
コマンドで初期設定可能なものとしては、以下のようなビルダーをサポートします。
- Docker
- Jib(--XXenableJibInit フラグを指定)
- Cloud Native Build Pack(--XXenableBuildpacksInit フラグを指定)
また、skaffold init
ではプロジェクトのディレクトリ内を走査して、以下のようなファイルからビルドの設定を読み込みます。
この際、500MB以上のファイルは無視されるようです。
例えば今回作成下アプリの場合Dockerfile
とgo.mod
があるので以下のように2つの選択肢を提示してくれるようになります。
$ skaffold init ? Which builders would you like to create kubernetes resources for? [Use arrows to move, space to select, <right> to all, <left> to none, type to filter] > [ ] Buildpacks (go.mod) [ ] Docker (Dockerfile)
ここではDockerを選びます方向キーで上下の移動をし、スペースキーで選択します。
プロジェクト作成の際プロジェクト内にk8sのリソースファイルがない場合は以下のようなエラーが出力されます。
$ skaffold init ? Which builders would you like to create kubernetes resources for? Docker (Dockerfile) one or more valid Kubernetes manifests are required to run skaffold
skaffold init
を行なう場合先にマニフェストを容易しておくか、--generate-manifests
フラグを指定してマニフェストの自動生成を行なう必要があります。
今回は自動生成をしてもらうので以下のようにコマンドを実行します。
$ skaffold init --generate-manifests ? Which builders would you like to create kubernetes resources for? Docker (Dockerfile) ? Select port to forward for dockerfile-image (leave blank for none): 1323 apiVersion: skaffold/v2beta22 kind: Config metadata: name: skaffold-demo build: artifacts: - image: dockerfile-image docker: dockerfile: Dockerfile deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: dockerfile-image port: 1323 deployment.yaml - apiVersion: v1 kind: Service metadata: name: dockerfile-image labels: app: dockerfile-image spec: ports: - port: 1323 protocol: TCP clusterIP: None selector: app: dockerfile-image --- apiVersion: apps/v1 kind: Deployment metadata: name: dockerfile-image labels: app: dockerfile-image spec: replicas: 1 selector: matchLabels: app: dockerfile-image template: metadata: labels: app: dockerfile-image spec: containers: - name: dockerfile-image image: dockerfile-image ? Do you want to write this configuration, along with the generated k8s manifests, to skaffold.yaml? Yes Generated manifest deployment.yaml was written Configuration skaffold.yaml was written You can now run [skaffold build] to build the artifacts or [skaffold run] to build and deploy or [skaffold dev] to enter development mode, with auto-redeploy
実行が成功すると以下のようにSkaffoldの設定ファイルのskaffold.yaml
やk8sのリソース定義ファイルであるdeployment.yaml
が生成されているのが確認できます。
$ ls Dockerfile deployment.yaml go.mod go.sum server.go skaffold-demo-app skaffold.yaml
前のログでも出力されていますが、できたskaffold.yaml
は以下のようになっています。
apiVersion: skaffold/v2beta22 kind: Config metadata: name: skaffold-demo build: artifacts: - image: dockerfile-image docker: dockerfile: Dockerfile deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: dockerfile-image port: 1323
今回はStageをざっくり把握することが目的なので詳しくはみませんが skaffold.yaml
に関してはこちらを参照ください。
これでプロジェクトのinitは完了です。
Build
Buildステージではイメージのビルドを行います。
このステージはskaffold build
やskaffold run
、skaffold dev
などで実行されるようです。
Buildステージでは以下のようなビルドの方法があるみたいです。
- Local Build : デフォルトのビルドコンテキスト、ローカルのツール(Docker、Bazel、Mavenなど)を使ってビルドをおこなう。
- In Cluster Build : Custom Build ScriptやKanikoを用いたクラスター内でのビルド
- Remote on Google Cloud Build : Google Cloud Buildを用いたリモートでのビルド
各ビルド手法でのビルダーサポートはここをご確認ください。
ローカルでは基本的にどのようなビルダーもサポートしているようです。
Buildに関する設定はskaffold.yaml
のbuild
セクションで行います。
skaffold build
でコマンドでビルドしてみます。
設定的にはローカルのDockerが利用されビルドされるようです。
ちなみにBuild時のBuildKitの有効無効は設定で明示的にOnにしない限りはローカルのDockerの設定に依存するようです。
$ skaffold build $ docker images | grep dockerfile-image dockerfile-image 83b1cdbd0c82d697b527964e54a126ebb6b65bf690d1a40fe8b00f313ba380f8 83b1cdbd0c82 55 seconds ago 12.4MB dockerfile-image latest 83b1cdbd0c82 55 seconds ago 12.4MB
Tag
ビルドしたイメージのタグ付けを行います。
この際以下のようなポリシーでタグ付けを行えるようです。
gitCommit
: Gitのcommit referencesを用いたタグ付けinputDigest
: ソースファイルのダイジェスト情報を用いたタグ付けenvTemplate
: 環境変数を用いたタグ付けdatetime
: ビルド時の日付を用いたタグ付け。設定変更可能customTemplate
: 複数のタガーとテンプレートを用いたタグ付け
skaffold.yaml
に特に指定がない場合はgitCommit
が用いられるようです。
また、コマンド実行時に--tag
を指定するとタグの上書きが可能になるようです。
今回はcustomTemplate
とiinputDigest
、datetime
を用いてタグ付けをしてみます。
skaffold.yaml
ファイルを以下のように書き換えます。
apiVersion: skaffold/v2beta22 kind: Config metadata: name: skaffold-demo build: tagPolicy: customTemplate: template: "{{.FOO}}_{{.BAR}}" components: - name: FOO dateTime: format: "2006-01-02" timezone: "UTC" - name: BAR inputDigest: {} artifacts: - image: dockerfile-image docker: dockerfile: Dockerfile deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: dockerfile-image port: 1323
customTemplate
を用いる場合template
セクションでテンプレートを定義しcomponents
セクションでそれぞれのタガーのコンポーネントを定義するようです。
この設定でBuildを実行するとタグが先程と違い日付とダイジェストを指定したテンプレートに当てはめて作られていることが確認できます。
$ skaffold build (省略) $ docker images | grep dockerfile-image dockerfile-image 2021-10-09_a0660834a25ae146d26458a6b6247f101a093ec6fde33e3955fe488031ee13ac 83b1cdbd0c82 28 minutes ago 12.4MB dockerfile-image 83b1cdbd0c82d697b527964e54a126ebb6b65bf690d1a40fe8b00f313ba380f8 83b1cdbd0c82 28 minutes ago 12.4MB dockerfile-image latest 83b1cdbd0c82 28 minutes ago 12.4MB
Test
Skaffoldはパイプラインの中でテストを実行することも可能です。
skaffold test
やskaffold run
、skaffold dev
などのコマンドでテストが実行されます。
現状では以下の2つのタイプのテストをサポートしています。
- Custom Test : Skaffoldのパイプラインの中でテストフェーズとしてカスタムコマンドの実行をおこなう。ユニットテストやセキュリティスキャンの実行が可能
- Container Structure Test : ビルドされたコンテナをデプロイ前にバリデーションする。
ここではCustom Testの方を使ってGoのユニットテストの実行を試してみたいと思います。
まずは以下のようなテストを実行するスクリプトを用意します。
#!/bin/bash set -e echo "go custom test $@" go test .
次に適当に失敗するテストを用意しておきます。
server_test.go
package main import "testing" func TestServer(t *testing.T) { t.FailNow() }
このテストをSkaffoldのTestステージで実行するようにします。
skaffold.yaml
を以下のように書き換えます。
apiVersion: skaffold/v2beta22 kind: Config metadata: name: skaffold-demo build: tagPolicy: customTemplate: template: "{{.FOO}}_{{.BAR}}" components: - name: FOO dateTime: format: "2006-01-02" timezone: "UTC" - name: BAR inputDigest: {} artifacts: - image: dockerfile-image docker: dockerfile: Dockerfile test: - image: dockerfile-image custom: - command: ./test.sh timeoutSeconds: 60 dependencies: paths: - "*_test.go" - "test.sh" deploy: kubectl: manifests: - deployment.yaml portForward: - resourceType: service resourceName: dockerfile-image port: 1323
test
セクションを追加しています。
custom
セクションに諸々の設定を書いてます。
大体はセクション名の通りですが、dependencies
セクションにはファイル変更を検知してテストを再実行するための設定を記述しています。
skaffold dev
コマンドでテストを実行してみます。
(skaffold test
コマンドを用いる場合はビルド時にskaffold build --file-output tags.json
のようにしてアーティファクト情報を出力しテスト時にskaffold test --build-artifacts tags.json
の用に指定してやる必要があります)
$ skaffold dev (省略) Successfully built a68e7ef37e4e Successfully tagged dockerfile-image:2021-10-09_a22222e721470777f9403c07cef58d363eeb53a438486c9f999cd5499ca5eb44 Starting test... Testing images... Running custom test command: "./test.sh" with timeout 60 s go custom test --- FAIL: TestServer (0.00s) FAIL FAIL skaffold-demo 0.002s FAIL Command finished with non-0 exit code. running tests: command ./test.sh finished with non-0 exit code: exit status 1. Check the custom command contents: "./test.sh".
ビルド後にテストが実行されているのが確認できます。
テストを修正してもう一度skaffold dev
を実行してみます。
package main import "testing" func TestServer(t *testing.T) { }
なにもテストしてないですが、成功のステータスになればよいので一旦これで勧めます。
$ skaffold dev (省略) Successfully tagged dockerfile-image:2021-10-09_829325c7d6931da186db8c88d95d30cdbcc7de9fd7782f7c86db821e5834899a Starting test... Testing images... Running custom test command: "./test.sh" with timeout 60 s go custom test ok skaffold-demo 0.002s Command finished successfully. Tags used in deployment: - dockerfile-image -> dockerfile-image:a68e7ef37e4ecb1a02a8787b476221750e931017692745e632d7b2f8e83a5eee Starting deploy... - service/dockerfile-image created - deployment.apps/dockerfile-image created Waiting for deployments to stabilize... (省略)
今度はテストの実行も成功しているみたいですね。
Deploy
デプロイのステージではその名の通りアプリをk8sにデプロイします。
skaffold dev
やskaffold run
、skaffold deploy
などのコマンドで実行されます。
デプロイ時には以下のステップが実行されます。
- k8sのマニフェストファイルを実行時のコンテキストに合わせたイメージ名やタグを用いてレンダリングする。Helmやkustomizeを使えばより高機能なレンダリングが可能になる。
- レンダリングされたマニフェストファイルに基づいてクラスターにデプロイを行なう
- デプロイされたアプリが動作しているかチェックする(詳細はこちらを参照してください)
Skaffoldはデプロイ時に以下のようなツールをサポートします。
- kubectl
- helm
- kustomize
今回は生成されたskaffold.yaml
に記述されているようにkubectl
を用いたデプロイを行います。
skaffold dev
コマンドを実行します。
$ skaffold dev --kube-context=minikube (省略) Tags used in deployment: - dockerfile-image -> dockerfile-image:22f84ce796f1c77f1bc5fceb6b494b9a5f67b86f8eeeb345fee0328047b3c115 Starting deploy... - service/dockerfile-image created - deployment.apps/dockerfile-image created Waiting for deployments to stabilize... - deployment/dockerfile-image is ready. Deployments stabilized in 2.228 seconds Port forwarding service/dockerfile-image in namespace default, remote port 1323 -> http://127.0.0.1:1323 Press Ctrl+C to exit Watching for changes... [dockerfile-image] [dockerfile-image] ____ __ [dockerfile-image] / __/___/ / ___ [dockerfile-image] / _// __/ _ \/ _ \ [dockerfile-image] /___/\__/_//_/\___/ v4.6.1 [dockerfile-image] High performance, minimalist Go web framework [dockerfile-image] https://echo.labstack.com [dockerfile-image] ____________________________________O/_______ [dockerfile-image] O\ [dockerfile-image] ⇨ http server started on [::]:1323 (省略)
kubeclt
コマンドでリソースを確認してみます。
$ kubectl --context=minikube -n default get all NAME READY STATUS RESTARTS AGE pod/dockerfile-image-6bc7589896-vth7m 1/1 Running 0 6m16s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/dockerfile-image ClusterIP None <none> 1323/TCP 6m16s service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 99m NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/dockerfile-image 1/1 1 1 6m16s NAME DESIRED CURRENT READY AGE replicaset.apps/dockerfile-image-6bc7589896 1 1 1 6m16s
DeploymentやServiceなどがクラスターにデプロイされました。
--kube-context=minikube
でコンテキストを明示的に指定していますが、指定しない場合kubectl
のカレントコンテキストを用いるようです。
[skaffold config](https://skaffold.dev/docs/references/cli/#skaffold-config)
コマンドでカレントコンテキストを変えることも可能なようです。
File Sync
Skaffoldは不要なリビルドを避けるためにファイルをコンテナに直接コピーする方法も提供しています。
リビルドの必要のない設定ファイルやHTMLファイルをシンクしたい場合この方法が使えます。
今回は深く機能は試しませんが、以下のようなパターンでのシンクを実行できます。
- manual : ローカルのディレクトリと実行されているコンテナのファイルパスをマニュアルで指定してシンクする。すべてのアーティファクトに対応している
- infer : ビルダーから推測可能なファイルディレクトリの中をチェックしシンクする。dockerやkanikoの場合はDokcerfileから推測される。
- auto : Skaffoldがシンクの設定を自動的に設定する。JibやBuildpacksのアーティファクトに対して有効。Buildpacksを用いている場合はデフォルトで有効になる。
Port Forwarding
Skaffoldはビルドインでクラスターからローカルマシンへのポートフォワードのサポートを行ってくれています。
この機能はskaffold dev
、skaffold debug
、skaffold deploy
、 skaffold run
などのコマンドで有効にできます。
(コマンドによって、有効になるポートフォワードの種類が違います。詳細はこちらをご確認ください)
ポートフォワードはskaffold.yaml
内で以下のような種類で定義することが可能です。
- user : ユーザが明示的に有効にできるポートフォワード。k8sのリソースタイプを指定してぽーどフォワードを行なう(設定方法はこちら)
- services : Skaffoldでデプロイされた、Serviceリソースに対して設定できるポートフォワード
- debug : デバック用のポートを公開するためのポートフォワード(
skaffold debug
コマンドで有効にできる) - pods : SkaffoldがビルドしたイメージのすべてのPodの
containerPorts
に対するポートフォワード
また、ポートフォワードを明示的にoffにしたい場合はコマンドのフラグとして--port-forward=off
を指定します。
今回はデフォルトで生成されたuser
のタイプのServiceリソースに対するポートフォワードを行ってみます。
以下のコマンドを実行します。
$ skaffold dev --kube-context=minikube (省略) Port forwarding service/dockerfile-image in namespace default, remote port 1323 -> http://127.0.0.1:1323 (省略)
ログからポートフォワードが動作しているのがわかります。
cURLでlocalhost:1323にアクセスしてみます。
$ curl localhost:1323/hello Hello, World!
Lifecycle Hooks
Lifecycle Hooksでは、Skaffoldのライフサイクルイベントによってコードを実行したりすることができます。
build
とsync
、deploy
のフェーズでそれぞれbefore
とafter
のホックを設定できます。
以下の2つのタイプのホックが存在します。
- Host hooks : ランナー(例えばホストマシン)内で実行されるホック
- Container hooks : 実行されるコンテナ内で実行されるホック
また、Host hooksでは、フェーズによって利用できるいくつかの環境変数が存在しており、例えばbuild
とsync
のフェーズではSKAFFOLD_IMAGE
というイメージ名が設定された環境変数が利用可能です。
環境変数の一覧とその環境変数が利用できるフェーズの詳細はこちらをご確認ください。
今回は、Host hooksを利用して、ビルドの前と後にecho
コマンドを実行してイメージ名を出力してみます。
具体的にはskaffold.yaml
のbuild
セクションを以下のように書き換えます。
build: tagPolicy: customTemplate: template: "{{.FOO}}_{{.BAR}}" components: - name: FOO dateTime: format: "2006-01-02" timezone: "UTC" - name: BAR inputDigest: {} artifacts: - image: dockerfile-image docker: dockerfile: Dockerfile hooks: before: - command: ["sh", "-c", "echo Start Building: $SKAFFOLD_IMAGE"] os: [linux] after: - command: ["sh", "-c", "echo Finish Building: $SKAFFOLD_IMAGE"] os: [linux]
そして、skaffold build
コマンドを実行します。
$ skaffold build (省略) Starting pre-build hooks... Start Building: dockerfile-image:2021-10-09_7db982f9391c94bd44049fdefeb1e2e15d4a9f876009f322744cca7ca74c3667 Completed pre-build hooks (省略) Starting post-build hooks... Finish Building: dockerfile-image:2021-10-09_7db982f9391c94bd44049fdefeb1e2e15d4a9f876009f322744cca7ca74c3667 Completed post-build hooks
Clean Up
最後に、クリーンアップステージです。
Skaffoldはローカルマシンやクラスターに以下のような副作用を起こします。
Skaffoldは自動でこれらの副作用をクリーンアップしてくれる機能を有しています。
skaffold delete
を使えばそれらのリソースのクリーンアップが行えます。
また、skaffold dev
やskaffold debug
コマンドを用いていてクラスターへのデプロイを行っている場合はCtrl + c
を実行すれば自動的に作成されたリソースやイメージがクリーンアップされます。
$ skaffold dev --kube-context=minikube (省略) Watching for changes... ^CCleaning up... - service "dockerfile-image" deleted - deployment.apps "dockerfile-image" deleted
ログから不要なリソースがクリーンアップされていることがわかります。
実際にkubectl
コマンドで確認してみてもクリーンアップされています。
$ kubectl --context=minikube -n default get all NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 103m
Docker Buildxを使う
はじめに
DokcerにはBuildxというCLIプラグインが存在します。
いままでなんとなく使ってただけなのでちょっと調べてまとめてみようかと思います。
Docker Buildxとは
前述していますがDocker のCLIプラグインです。
これはBuildxの機能を拡張したもので以下のような機能や特徴を持ちます。
(Buildxに関しては以前ブログにまとめてます)
docker build
と似たUIで提供される- Docker BuildKitの機能をすべて備えている
- マルチノードビルドやクロスプラットフォームイメージの作成できる
- Compose Buildサポートをしている
- ハイレベルなビルド構成(bake)
BuildxはDokcer 19.03以降のバージョンを必要とします。
しかし、古いバージョンでもバイナリを直接実行することで、機能を限定した利用は可能なようです。
インストールに関しては最新のバージョンのDocker CEのパッケージをインストールしていれば一緒にダウンロードされているようです。
(もしバイナリをインストールしたい場合はこちらをご確認ください)
使ってみる
環境
今回の動作環境は以下の通り。
$ uname -srvmpio Linux 5.4.0-88-generic #99-Ubuntu SMP Thu Sep 23 17:29:00 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.3 LTS Release: 20.04 Codename: focal $ docker version Client: Docker Engine - Community Version: 20.10.8 API version: 1.41 Go version: go1.16.6 Git commit: 3967b7d Built: Fri Jul 30 19:54:27 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12) Go version: go1.16.6 Git commit: 75249d8 Built: Fri Jul 30 19:52:33 2021 OS/Arch: linux/amd64 Experimental: true containerd: Version: 1.4.10 GitCommit: 8848fdb7c4ae3815afcc990a8a99d663dda1b590 runc: Version: 1.0.2 GitCommit: v1.0.2-0-g52b36a2 docker-init: Version: 0.19.0 GitCommit: de40ad0
Buildxのコマンド
Buildxのコマンドには以下のようなものがあります。
- buildx bake: 高レベルでのビルドコマンド(今回は深堀りしませんが別途ブログを書くかもです)
- buildx build: ビルドコマンド。Buildkitを用いてビルドを行なう
- buildx create: Docker ContextやDockerエンドポイントをさす新しいビルダーインスタンスを作成する。
- buildx du: ディスクの使用率を取得する
- buildx imagetools
- buildx imagetools create: レジストリー内のDokcer Manifest(イメージに関するレイヤー、サイズ、ダイジェスト値等の情報を持つ)のリストから新しいマニフェストを作成する。ソースは複数のマニフェストを選択可能。
- buildx imagetools inspect: レジストリー内のイメージの詳細情報を表示
- buildx inspect: 現在利用しているビルダーインスタンスの詳細情報を表示。名前をビルダーインスタンスの名前を明示的に指定して他のインスタンスの情報を表示することも可能
- buildx install: dockerコマンドへのエイリアスを作成する
- buildx uninstall: dockerコマンドへのエイリアスを削除する
- buildx ls: ビルダーインスタンスをリスト表示
- buildx prune: ビルドキャッシュの削除
- buildx rm: 指定された(もしくは現在の)ビルダーインスタンスの削除する。デフォルトのビルダーインスタンスは削除不可
- buildx stop: 指定された(もしくは現在の)ビルダーインスタンスを停止する。
- buildx use: ビルダーインスタンスをスイッチする
この中からいくつかのコマンドは利用してみたいと思います。
dokcerコマンドにエイリアスを作成する
buildx installコマンドを使うと通常のdokcerコマンドに対するエイリアスを貼ることができます。
buildx は通常 docker buildx build
などのようにdokcer buildx [SUBCOMMAND]
の構成を取りますが、buildx installコマンドを実行すると
$ docker buildx install // docker buildコマンドが docker buildx buildへのエイリアスになっているのでBuildxでのビルドが実行される $ docker build . [+] Building 46.2s (11/11) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 155B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => resolve image config for docker.io/docker/dockerfile:experimental 8.8s => [auth] docker/dockerfile:pull token for registry-1.docker.io 0.0s => docker-image://docker.io/docker/dockerfile:experimental@sha256:600e5c62eedff338b3f7a0850beb7c05866e0ef27b2d2e8c02aa468e78496ff5 33.4s => => resolve docker.io/docker/dockerfile:experimental@sha256:600e5c62eedff338b3f7a0850beb7c05866e0ef27b2d2e8c02aa468e78496ff5 0.0s => => sha256:600e5c62eedff338b3f7a0850beb7c05866e0ef27b2d2e8c02aa468e78496ff5 1.69kB / 1.69kB 0.0s => => sha256:3c244c0c6fc9d6aa3ddb73af4264b3a23597523ac553294218c13735a2c6cf79 528B / 528B 0.0s => => sha256:b587adb6abfd8d6c87b1f649c2e924d53f148ae1c7f0ceaaded70b27b44dccb5 1.21kB / 1.21kB 0.0s => => sha256:d7f0373ffb1d5ac3477b10ab7f91cba7c5df586c72e7e4a12649024efdc0d531 9.64MB / 9.64MB 33.2s => => extracting sha256:d7f0373ffb1d5ac3477b10ab7f91cba7c5df586c72e7e4a12649024efdc0d531 0.1s => [internal] load metadata for docker.io/library/busybox:latest 2.9s => [auth] library/busybox:pull token for registry-1.docker.io 0.0s => [internal] load build context 0.0s => => transferring context: 155B 0.0s => [1/2] FROM docker.io/library/busybox@sha256:f7ca5a32c10d51aeda3b4d01c61c6061f497893d7f6628b92f822f7117182a57 0.7s => => resolve docker.io/library/busybox@sha256:f7ca5a32c10d51aeda3b4d01c61c6061f497893d7f6628b92f822f7117182a57 0.0s => => sha256:f7ca5a32c10d51aeda3b4d01c61c6061f497893d7f6628b92f822f7117182a57 2.29kB / 2.29kB 0.0s => => sha256:febcf61cd6e1ac9628f6ac14fa40836d16f3c6ddef3b303ff0321606e55ddd0b 527B / 527B 0.0s => => sha256:16ea53ea7c652456803632d67517b78a4f9075a10bfdc4fc6b7b4cbf2bc98497 1.46kB / 1.46kB 0.0s => => sha256:24fb2886d6f6c5d16481dd7608b47e78a8e92a13d6e64d87d57cb16d5f766d63 767.32kB / 767.32kB 0.6s => => extracting sha256:24fb2886d6f6c5d16481dd7608b47e78a8e92a13d6e64d87d57cb16d5f766d63 0.1s => [2/2] RUN --mount=type=bind,target=tmp cat tmp/Dockerfile > text.txt 0.3s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:00d095571d6c88a17fc804422a29bf628377bbd2d6616f0e11252e5959955b8f
エイリアスを削除したい場合はbuildx uninstall
コマンドを用います。
ビルダーインスタンスを作成する
buildxコマンドを使えば、複数のビルダーインスタンスを作成したり削除したりすることができます。
このビルダーインスタンスはスコープを持っており、例えば、共有のデーモンのステートを変更すること無い独立したビルダーを作成することができます。
また、このビルダーインスタンスにはリモートのノードを複数追加することが可能です。
ビルダーインスタンスの作成は以下のコマンドを用います。
$ docker buildx create default --name henoheno_builder henoheno_builder $ docker buildx use henoheno_builder $ docker buildx ls NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS henoheno_builder * docker-container henoheno_builder0 unix:///var/run/docker.sock inactive default docker default default running linux/amd64, linux/386
新しいビルダーインスタンスを作成する場合は引数にdocker contextを指定するかDOCKER_HOST
環境変数を指定します。
新しく作成したビルダーのステータスがinactiveになってしますがbuildxコマンドを利用時に自動的に起動してくれるようです。
明示的に起動したい場合は以下のコマンドを実行します。
$ docker buildx inspect --bootstrap [+] Building 19.5s (1/1) FINISHED => [internal] booting buildkit 19.5s => => pulling image moby/buildkit:buildx-stable-1 18.8s => => creating container buildx_buildkit_henoheno_builder0 0.6s Name: henoheno_builder Driver: docker-container Nodes: Name: henoheno_builder0 Endpoint: unix:///var/run/docker.sock Status: running Platforms: linux/amd64, linux/386
最後に作ったビルダーインスタンスを削除する場合は以下のコマンドを実行します。
$ docker buildx rm henoheno_builder $ docker buildx ls NAME/NODE DRIVER/ENDPOINT STATUS PLATFORMS default * docker default default running linux/amd64, linux/386
Springのアノテーション付きコントローラーのメソッドを明示的にハンドラーとして登録する
はじめに
Spring のWebFluxのドキュメント眺めていたら明示的なハンドラーの登録という項目が合ってなんとなく気になったので動かしてみたいと思います。
あとは、Java 16から入ってるRecordsクラスを試してみたい気持ちもあり。
ちなみに明示的なハンドラーの登録はMVCの方でも使えるみたいです。
Webfluxでは大きく以下の2つのハンドラー定義の方法をサポートしています(MVCでも同様)。
上記のやり方でニーズを満たせない場合、より高度なハンドラーの登録ロジックが必要な場合(動的なハンドラー登録、異なるURLで同一ハンドラーを登録など)明示的なハンドラーの登録を行なうことができます。
また、この方法はアノテーション付きコントローラーを使っている際に利用可能なようです。
やってみる
環境
$ java --version openjdk 17 2021-09-14 OpenJDK Runtime Environment (build 17+35-2724) OpenJDK 64-Bit Server VM (build 17+35-2724, mixed mode, sharing) $ mvn -v Apache Maven 3.6.3 Maven home: /usr/share/maven Java version: 17, vendor: Oracle Corporation, runtime: /home/******/.sdkman/candidates/java/17-open Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-86-generic", arch: "amd64", family: "unix"
プロジェクトの作成
Spring Initializrを使ってプロジェクトを作成します。
設定は以下のように
コントローラーを作って登録する
リクエストで名前を受け付けて、挨拶を返すコントローラーを作成します。
まずは、リクエストとレスポンスを受ける用のRecordクラスを作成します。
NameRequest.java
public record NameRequest(String value) {}
GreetingResponse
public record GreetingResponse(String value) { public GreetingResponse(String value){ this.value = String.format("Hello %s!", value); } }
次にコントローラーを実装します。
GreetingController.java
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Mono; @RestController public class GreetingController { public Mono<GreetingResponse> hello(@RequestBody Mono<NameRequest> name) { return name.map(it -> new GreetingResponse(it.value())); } }
@〇〇Mappting
みたいなのを付けてない以外は普通のコントローラーですね。
また、Recordクラスは今まで定義してたDTOみたいな感じで扱えるみたいですね。
このコントローラーのメソッドをハンドラーマッピングとして登録します。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.reactive.result.method.RequestMappingInfo; import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping; import reactor.core.publisher.Mono; import java.lang.reflect.Method; @Configuration public class Config { @Autowired public void handlerSetting(RequestMappingHandlerMapping mapping, GreetingController handler) throws NoSuchMethodException { RequestMappingInfo info = RequestMappingInfo .paths("/hello").methods(RequestMethod.POST).build(); Method method = GreetingController.class.getMethod("hello", Mono.class); mapping.registerMapping(info, handler, method); } }
@Autowired
を使ってRequestMappingHandlerMapping
とGreetingController
を受け取ってます。
また、RequestMappingInfoを使ってリクエストのマッピング設定を記述し、その定義とコントローラーのリクエストをハンドルするMethod
オブジェクト、そしてコントロラー本体をRequestMappingHandlerMapping
に登録しています。
この際に色々処理を入れてロジックでハンドラーの定義をごにょごにょできるみたいですね。
さっくり触っただけですが、ほぼ同じような記述でMVC側も動くみたいです。
あと、Recordクラスはほんとに今まで通りな感じで使えましたね。
関数エンドポイントの方でも今までと全くおんなじように利用できました。
pprofでGoアプリのプロファイル情報を取得する
はじめに
Javaでプロファイル情報を取得して分析するための方法は、いくつか思い当たるものはあるのですがGoだとやり方全くわからなかったので少しまとめておこうかと思いました。
Go言語は標準ライブラリとして、runtime/pprof
パッケージやnet/http/pprof
パッケージが用意されており、これらをちょっと使ってみようと思います。
また、プロファイル情報を分析するためのツールは公式のpprofを使うのが一般的なようなのでほんの少しだけ使い方をみてみようと思います。
pprofを用いたプロファイリング
Go言でプロファイル情報を取得するには以下の2つのやり方があるみたいです。
runtime/pprof
パッケージを用いてプロファイル情報をファイルに出力するnet/http/pprof
パッケージを用いてHTTP経由でプロファイル情報を取得する
上記の2つはprofile.proto形式で情報を出力するようです。
profile.protoは記号化されたコールスタックの情報がProtocol Buffersの書式で記述されているようです。
pprofで取得可能な項目には以下のようなものがあるようです。
- CPU: CPUの使用時間に関するプロファイル
- Heep: メモリアロケーションのプロファイル、メモリリークのチェック情報
- Threadcreate: OSのスレッド生成に関するプロファイル
- Goroutine: すべてのGoroutineのスタックトレース
- Block: Goroutineのブロッキングに関するプロファイル。デフォルトで無効なので
runtime.SetBlockProfileRate
を使って有効化する必要がある。 - Mutex: Mutexのロックに関するプロファイル。デフォルトで無効なので
runtime.SetMutexProfileFraction
を使って有効化する必要がある。
これらの詳細はこちらをご確認ください。
また軽く前述していますが、取得したprofile.protoの分析はpprofの可視化ツールを使って行なうのが一般的なようです。 このツールはのデータを読み込んで可視化を行ってくれます。
やってみる
フィボナッチ数列を計算する簡単なのアプリケーションを作成して、runtime/pprof
パッケージやnet/http/pprof
パッケージで情報を取得してみるのと、その情報を可視化するとことまでやってみようかと思います。
環境
動作環境は以下のような感じ。
$ uname -srvmpio Linux 5.4.0-84-generic #94-Ubuntu SMP Thu Aug 26 20:27:37 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ go version go version go1.17.1 linux/amd64 $ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.3 LTS Release: 20.04 Codename: focal
サンプルアプリの作成
アプリは引数で正数nを受け取ってフィボナッチ数列を計算してn番目のフィボナッチ数を返すアプリを作ろうと思います。
まずはプロジェクト作成から。
$ mkdir echo-fibo && cd echo-fibo $ go mod init echo-fibo
次にfibo
パッケージを作り、よくある定義のフィボナッチ数列を計算する関数を定義します。
また、その関数に対するテストも記述しておきます。
fibo.go
package fibo func Fibo(n int) int { if n < 2 { return n } return Fibo(n-2) + Fibo(n-1) }
fibo_test.go
package fibo import "testing" func TestFibo(t *testing.T) { tests := []struct { name string input int want int }{ { "input 10", 10, 55, }, { "input 11", 11, 89, }, { "input 20", 20, 6765, }, { "input 30", 30, 832040, }, { "input 44", 44, 701408733, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := Fibo(tt.input); got != tt.want { t.Errorf("Fibo() = %v, want %v", got, tt.want) } }) } }
ここでは、効率は度外視で直感的なフィボナッチ数列の計算アルゴリズムものを記述しました。
最後にこの関数を呼び出すmain関数を書きます。
package main import ( "echo-fibo/fibo" "fmt" "log" "os" "strconv" ) func main() { number, err := strconv.Atoi(os.Args[1]) if err != nil { log.Fatal(err) } fmt.Println(fibo.Fibo(number)) }
バリデージョンもなにもしてないほんとに最低限のものですが、pprofを動かすことが目的なだけなのでこのまま行きます。
runtime/pprof
パッケージを用いてプロファイル情報を取得する
runtime/pprof
パッケージを用いてプロファイル情報を取得する方法は以下の2通りの方法があります。
- Goのtestingパッケージにビルドインされているプロファイラを使う
pprof.StartCPUProfile(filename)
やpprof.WriteHeapProfile(filename)
等を使ってスタンドアローンアプリでプロファイルを有効化する
testingパッケージにビルドインされているプロファイラを使う
前者のやり方は簡単で、testingパッケージにはプロファイルのサポートがビルドインされているので、以下のコマンドを実行するだけでプロファイル情報を取得することが可能です。
$ cd fibo $ go test -cpuprofile cpu.prof -memprofile mem.prof -benchtime 1ms -bench . $ ls cpu.prof fibo.go fibo.test fibo_test.go mem.prof
上記のコマンドでは、-bench .
ですべてのベンチマークが実行され、-cpuprofile cpu.prof
や-memprofile mem.prof
で実行結果を出力する項目とファイル名を決定しているようです。
上記の例ではCPUとMemoryに対するプロファイル情報を取得しましたが、以下のオプションでそれぞれのプロファイル情報が取得可能なようです。
Option | 説明 |
---|---|
-blockprofile [outputFileName] | goroutineのブロッキングプロファイル |
-coverprofile [outputFileName] | すべてのテストが実行された時のカバレッジプロファイル |
-cpuprofile [outputFileName] | CPUプロファイル |
-memprofile [outputFileName] | Memoryプロファイル |
-mutexprofile | mutexの競合プロファイル |
また、プロファイルとは少し毛色が違いますが-trace [outputFileName]
実行のトレースを出力することも可能なようです。
スタンドアローンアプリでプロファイルを有効化する
testingパッケージは利用せずスタンドアローンのアプリに対してプロファイルを実行したい場合は少し工夫が必要です。
pprof.StartCPUProfile(filename)や
pprof.WriteHeapProfile(filename)などの関数をmain関数で呼び出しプロファイルを有効にできるようにする必要があるからです。 '
main.go`を以下のように書き換えます。
package main import ( "echo-fibo/fibo" "flag" "fmt" "log" "os" "runtime" "runtime/pprof" "strconv" ) var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") var memprofile = flag.String("memprofile", "", "write memory profile to `file`") var number = flag.String("number", "", "fibo number") func main() { flag.Parse() if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { log.Fatal("could not create CPU profile: ", err) } defer f.Close() // error handling omitted for example if err := pprof.StartCPUProfile(f); err != nil { log.Fatal("could not start CPU profile: ", err) } defer pprof.StopCPUProfile() } fmt.Println(os.Args) num, err := strconv.Atoi(*number) if err != nil { log.Fatal(err) } fmt.Println(fibo.Fibo(num)) if *memprofile != "" { f, err := os.Create(*memprofile) if err != nil { log.Fatal("could not create memory profile: ", err) } defer f.Close() // error handling omitted for example runtime.GC() // get up-to-date statistics if err := pprof.WriteHeapProfile(f); err != nil { log.Fatal("could not write memory profile: ", err) } } }
この状態でアプリをビルドし以下のように起動するとプロファイル情報が取得できます。
$ go build echo-fibo $ ./echo-fibo -number 44 -memprofile mem.prof
1つ1つプロファイルごとに設定していくのは流石に面倒に感じますが、ラップしてくれているライブラリがあるみたいなのでこれを使えばもう少し楽にできるのかも知れません。また、別で試してみようと思います。
net/http/pprof
パッケージ
net/http/pprof
を用いればプロファイル情報をHTTP経由で公開することも可能です。
基本的な使い方は簡単で、以下のインポートを自分のプログラムに追加するだけです。
import _ "net/http/pprof"
ここで、注意が必要で、もしアプリケーションがhttp serverを起動していない場合に以下のようなコードを記述して自分で起動してやる必要があります。
go func() { log.Println(http.ListenAndServe("localhost:6060", nil)) }()
net/http/pprof
を使えばデフォルトでハンドラーが実装され以下のようなURLでプロファイル情報を取得できるようになります。
- http://localhost:6060/debug/pprof/goroutine
- http://localhost:6060/debug/pprof/heap
- http://localhost:6060/debug/pprof/threadcreate
- http://localhost:6060/debug/pprof/block
- http://localhost:6060/debug/pprof/mutex
- http://localhost:6060/debug/pprof/profile
- http://localhost:6060/debug/pprof/trace?seconds=5
また、すべてのプロファイル情報を取得する場合はhttp://localhost:6060/debug/pprof/
で行けるみたいです。
main関数を以下のように書き換えてHTTPのパスパラメータで受け取った値のフィボナッチ数列の答えを返すアプリに変更します。
package main import ( "echo-fibo/fibo" "flag" "fmt" "log" "net/http" _ "net/http/pprof" "strconv" "strings" ) func main() { flag.Parse() http.HandleFunc("/fibo/", func(writer http.ResponseWriter, request *http.Request) { number := strings.TrimPrefix(request.URL.Path, "/fibo/") writer.WriteHeader(http.StatusOK) num, err := strconv.Atoi(number) if err != nil { log.Fatal(err) } fiboNum := fibo.Fibo(num) log.Println(fiboNum) fmt.Fprintf(writer, "The Answer is %d", fiboNum) }) log.Println(http.ListenAndServe("localhost:6060", nil)) }
普通のHTTPハンドラーの実装とサーバの起動を行っているだけですが、Importに_ "net/http/pprof"
を追加しています。
wgetでプロファイル情報を取得してみます。
$ wget -O heep.prof http://localhost:6060/debug/pprof/heap --2021-09-23 18:47:58-- http://localhost:6060/debug/pprof/heap localhost (localhost) をDNSに問いあわせています... 127.0.0.1 localhost (localhost)|127.0.0.1|:6060 に接続しています... 接続しました。 HTTP による接続要求を送信しました、応答を待っています... 200 OK 長さ: 特定できません [application/octet-stream] `heep.prof' に保存中 heep.prof [ <=> ] 2.79K --.-KB/s in 0s 2021-09-23 18:47:58 (8.25 MB/s) - `heep.prof' へ保存終了 [2858] $ ls echo-fibo fibo go.mod go.sum heep.prof main.go
pprof 可視化ツールを使ってみる
pprofの可視化ではいくつかのやり方があるようです。
今回はWeb UIを通した可視化を行ってみたいと思います。
Web UIを用いる場合はgraphvizがインストールされている必要があるみたいなので、以下のコマンドでインストールしておきます。
sudo apt install graphviz
可視化ツールの使い方自体はさほど難しくなく以下のコマンドでWebのUIが立ち上がります。
$ go tool pprof -http=localhost:8081 pprof heep.prof pprof: open pprof: no such file or directory Fetched 1 source profiles out of 2 Serving web UI on http://localhost:8081
HTTP経由でプロファイルを取得する場合は以下のようにします。
$ go tool pprof -http=localhost:8082 http://localhost:6060/debug/pprof/heap Fetching profile over HTTP from http://localhost:6060/debug/pprof/heap Saved profile in /home/someone/pprof/pprof.___go_build_echo_fibo.alloc_objects.alloc_space.inuse_objects.inuse_space.005.pb.gz Serving web UI on http://localhost:8082
力尽きたのでUIの詳細な見かたは別の機会に。
Docker Notaryで署名したイメージをDockerHubで公開する
はじめに
ソフトウェアをインストールするさいにHashの検証や署名の検証などで、ダウンロードしてきたバイナリの信頼性を検証するみたいなのはよくやると思います。Dockerのイメージに対してこれがどのように解決されるかというところに理解が浅かったので、Dockerのコンテントトラストのドキュメントを読みつつ、自分で作成したイメージに署名をするところまでやってみようかと思います。
どのようにDocker のコンテントトラストが実現されるか
DockerではDCT(Docker Content Trust)と呼ばれる機能でデジタル署名を利用してデータの整合性と公開者情報を検証できる仕組みを提供しているようです。
この機能を使うと特定のイメージタグに対して検証を行えるようになります。
DCTでは、タグ毎にサインを行いどのタグにサインを行なうかはイメージの公開者が決める必要があります。
また、1つのリポジトリで1つのイメージに対してサインされているタグは1つだけ存在するようです。
クライアント目線で言うと、DCTを有効にした場合実行できるイメージはサインされたイメージのみで、ほかは利用できなくなります。フィルターの概念が近いようです。
Notaryについて
Docker DCTの仕組みはNotrayという機能の上で実装されているみたいです。
Notrayはサーバサイドとクライアントサイドで提供されており、サーバサイドが利用するDockerリポジトリにアタッチされている必要があるようです。
このブログでは独自にリポジトリを用意してアタッチすることなどは行いません(そのやり方に付いてはこちらを確認ください)。
今回はDocker Hubを使います。
また、NotrayはTOFU(Trust On First Use)というモデルを採用しており、最初にダウンロードしたものを信じるという仕様になってます。
V2ではこれを改善するような議論がコミュニティで行われているようです(ソースを見つけられなかった...)
自分で作成したイメージに署名する
環境
今回は以下の環境で諸々を動かしてみます。
$ lsb_release -a LSB Version: core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch Distributor ID: Ubuntu Description: Ubuntu 20.04.2 LTS Release: 20.04 Codename: focal $ uname -srvmpio Linux 5.4.0-80-generic #90-Ubuntu SMP Fri Jul 9 22:49:44 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux $ docker version Client: Docker Engine - Community Version: 20.10.8 API version: 1.41 Go version: go1.16.6 Git commit: 3967b7d Built: Fri Jul 30 19:54:27 2021 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.8 API version: 1.41 (minimum version 1.12) Go version: go1.16.6 Git commit: 75249d8 Built: Fri Jul 30 19:52:33 2021 OS/Arch: linux/amd64 Experimental: true containerd: Version: 1.4.9 GitCommit: e25210fe30a0a703442421b0f60afac609f950a3 runc: Version: 1.0.1 GitCommit: v1.0.1-0-g4144b63 docker-init: Version: 0.19.0 GitCommit: de40ad0
イメージの作成
まずは署名を行なうイメージを作っておきます。
今回はnginxの公式イメージを使って作ります。
$ docker create nginx $ docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 896fa7872413 nginx "/docker-entrypoint.…" 27 seconds ago Created cranky_dewdney $ docker commit 896fa7872413 hirohiroyuya/nginx:singed sha256:ffcb0c4915f34a5d68f4eb6e8452db191d4c19e6ecfc4bc17a90b16689e0dfaa $ docker commit 896fa7872413 hirohiroyuya/nginx:non-singed sha256:8e321e701fd6f0f5bf730dfd55c464804f85570e0dcb31326f6e50e4c289b8a4 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE hirohiroyuya/nginx non-singed 8e321e701fd6 About a minute ago 133MB hirohiroyuya/nginx singed ffcb0c4915f3 About a minute ago 133MB nginx latest 08b152afcfae 3 weeks ago 133MB
今回は2つのタグを作り片方は署名しもう片方は署名せずにPushしようと思います。 下準備は完了です。
鍵の作成とNotaryサーバへの設定
署名をする前に鍵を作る必要があります。
鍵を作る場合には以下のコマンドを実行します。
$ docker trust key generate henoheno Generating key for henoheno... Enter passphrase for new henoheno key with ID 3eb42d4: Repeat passphrase for new henoheno key with ID 3eb42d4: Successfully generated and loaded private key. Corresponding public key available: /home/someone/henoheno.pub
今回は試しませんがすでに鍵がある場合は以下のようにして既存のロードできるみたいです。
$ docker trust key load key.pem --name jeff
次に作成された公開鍵を公開鍵を Notary サーバーへ追加します。 今回はDockerHubを使うので特にドメインなどは指定してませんが、必要な場合は指定してください。
$ docker trust signer add --key /home/yuya-hirooka/henoheno.pub henoheno hirohiroyuya/nginx Adding signer "henoheno" to hirohiroyuya/nginx... Enter passphrase for repository key with ID 7d993ef: Successfully added signer: henoheno to hirohiroyuya/nginx
ここまでで鍵の生成とサーバへの設定は終了です。
署名したイメージとしてないイメージをリポジトリにPushする
署名は以下のコマンドで行なうことができます。
$ docker trust sign hirohiroyuya/nginx:singed Signing and pushing trust data for local image hirohiroyuya/nginx:singed, may overwrite remote trust data The push refers to repository [docker.io/hirohiroyuya/nginx] e3135447ca3e: Mounted from library/nginx b85734705991: Mounted from library/nginx 988d9a3509bb: Mounted from library/nginx 59b01b87c9e7: Mounted from library/nginx 7c0b223167b9: Mounted from library/nginx 814bff734324: Mounted from library/nginx singed: digest: sha256:505db062138c1e3dd094c9e5811c6cd9baae8c7beb77b1c010db809f2e0d8fd3 size: 1570 Signing and pushing trust metadata Enter passphrase for henoheno key with ID 3eb42d4: Successfully signed docker.io/hirohiroyuya/nginx:singed
イメージをPushします。
この際にDOCKER_CONTENT_TRUST=1
を環境変数に指定してコンテントトラストを有効にする必要があるようです。
$ DOCKER_CONTENT_TRUST=1 docker push hirohiroyuya/nginx:singed The push refers to repository [docker.io/hirohiroyuya/nginx] e3135447ca3e: Layer already exists b85734705991: Layer already exists 988d9a3509bb: Layer already exists 59b01b87c9e7: Layer already exists 7c0b223167b9: Layer already exists 814bff734324: Layer already exists singed: digest: sha256:505db062138c1e3dd094c9e5811c6cd9baae8c7beb77b1c010db809f2e0d8fd3 size: 1570 Signing and pushing trust metadata Enter passphrase for henoheno key with ID 3eb42d4: Successfully signed docker.io/hirohiroyuya/nginx:singed
これでDockerHubでの公開が完了しました。
Pushしたイメージの署名の情報を見るためには以下のコマンドを実行します。
$ docker trust inspect --pretty hirohiroyuya/nginx:singed Signatures for hirohiroyuya/nginx:singed SIGNED TAG DIGEST SIGNERS singed 12d3e6084e8af99509bd65b1d4583953cfb0791ddd66c4db199b725f6463327c henoheno List of signers and their keys for hirohiroyuya/nginx:singed SIGNER KEYS henoheno 3eb42d4ad775 Administrative keys for hirohiroyuya/nginx:singed Repository Key: 7d993ef2d41d5473aa8556e987cf8449bda7edd07b34856a13788a495bf70e3c Root Key: 9f0717638ac4ef0e113004354e2946c8010e2f6cb5b425af4d0779986ad45c74
Pushしたイメージを利用する
まずは、比較を行なうために先程のhirohiroyuya/nginx:non-singed
の方もPushしておきます。
また、ローカルのイメージも綺麗にしておきます。
$ docker push hirohiroyuya/nginx:non-singed The push refers to repository [docker.io/hirohiroyuya/nginx] e3135447ca3e: Layer already exists b85734705991: Layer already exists 988d9a3509bb: Layer already exists 59b01b87c9e7: Layer already exists 7c0b223167b9: Layer already exists 814bff734324: Layer already exists non-singed: digest: sha256:1ab4fc461a4c9028fa375aefec46c862d9317a2b2009321273c0135f7bdcb6ec size: 1570 $ docker rmi -f $(docker images -a -q) $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE
これでOK。
DockerクライアントではDCTはデフォルトで無効になっているようです。
これを有効にするためにはDOCKER_CONTENT_TRUST=1
を環境変数に指定してコマンドを実行する必要があるようです。
署名が行われていない状態のnginx:non-signed
をdocker run
してみます。
$ DOCKER_CONTENT_TRUST=1 docker run hirohiroyuya/nginx:non-singed docker: No valid trust data for non-singed.
実行できないようになってますね。
今度は、署名がされているものをdocker run
してみます。
$ DOCKER_CONTENT_TRUST=1 docker run hirohiroyuya/nginx:singed /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh /docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh /docker-entrypoint.sh: Configuration complete; ready for start up 2021/08/14 07:41:54 [notice] 1#1: using the "epoll" event method 2021/08/14 07:41:54 [notice] 1#1: nginx/1.21.1 2021/08/14 07:41:54 [notice] 1#1: built by gcc 8.3.0 (Debian 8.3.0-6) 2021/08/14 07:41:54 [notice] 1#1: OS: Linux 5.4.0-80-generic 2021/08/14 07:41:54 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576 2021/08/14 07:41:54 [notice] 1#1: start worker processes 2021/08/14 07:41:54 [notice] 1#1: start worker process 36 2021/08/14 07:41:54 [notice] 1#1: start worker process 37 2021/08/14 07:41:54 [notice] 1#1: start worker process 38 2021/08/14 07:41:54 [notice] 1#1: start worker process 39 2021/08/14 07:41:54 [notice] 1#1: start worker process 40 2021/08/14 07:41:54 [notice] 1#1: start worker process 41 2021/08/14 07:41:54 [notice] 1#1: start worker process 42 2021/08/14 07:41:54 [notice] 1#1: start worker process 43
きちんと実行ができました。
この通り、クライアントサイドではDCTを有効にしている場合、署名されたものしか実行されず更に検証も行われているようです。