CI/CDツールのDaggerを動かす

はじめに

CICDのパイプラインを記述する際はツールごとに独自のフォーマットや記述方法で記述する必要があるため移植性が低かったり、ローカルで試しに実行するのがなかなか難しかったりすることが多いと思います。DaggerはCICDの開発キッドで上記のような課題を削減することができます。 今回はこのDaggerをつかって簡単なパイプラインを作成し、ローカルで実行してみたいと思います。

Daggerとは

上記のようにDaggerはCICDのための開発キッドでCICDのパイプラインをCUEと呼ばれるようなGoogleで開発されて宣言的な言語で記述します。また特徴的なのが一度パイプラインを記述するとおおよそメジャーなCI環境で動かすことができます。そのため、CIのロックインを減らすことができます。また、ローカルマシーンで記述したパイプラインのテストやデバックを行うこともできます。
今回は他CIとのインテグレーションの機能は試しません。そちらについて知りたい人はこちらをご確認ください。

動かしてみる

動作環境

$ uname -srvmpio
Linux 5.14.0-1033-oem #36-Ubuntu SMP Mon Apr 4 15:15:49 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux


$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal

$ docker version
Client: Docker Engine - Community
 Version:           20.10.14
 API version:       1.41
 Go version:        go1.16.15
 Git commit:        a224086
 Built:             Thu Mar 24 01:48:02 2022
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.14
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.15
  Git commit:       87a90dc
  Built:            Thu Mar 24 01:45:53 2022
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.5.11
  GitCommit:        3df54a852345ae127d1fa3092b95168e4a88e2f8
 runc:
  Version:          1.0.3
  GitCommit:        v1.0.3-0-gf46b6ba
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

インストール

インストールは以下のコマンドで行います。

$ cd /usr/local
$  curl -L https://dl.dagger.io/dagger/install.sh | sudo sh
$ dagger version
dagger 0.2.11 (0088c621) linux/amd64

これでDaggerのインストールは完了しました。 もし、Daggerを/user/local以外の場所にインストールしたい場合は、単純に自分がインストールしたいロケーションにcdしてください。
また、特定のバージョンをインストールしたい場合やLinux以外でのインストールを行いたい場合はこちらを確認してください。

コアコンセプト

Daggerのパイプラインははアクションと呼ばれるベーシックなブロックから構成されます。アクションは複雑な自動化の部分をカプセル化し抽象化することでシンプルなソフトウェアコンポーネントとして動作し、安全にシェアすることができます。
Actionはdagger doで実行することもできますし、もっと複雑なコンポーネントから実行することともできます。
Actionにはcore actioncomposite actionと呼ばれる2種類のActionが存在します。

core actionはプリミティブにDaggerエンジンに実装されているActionです。よりハイレベルなActionから利用されます。このActionを利用する場合はdagger.io/dagger/coreパッケージをインストールする必要があります。core actionのリファレンスはこちらを参照ください。
composite actionは他のActionから構成されるActionです。このActionはcorecomposite両方のActionから構成されることがあります。

Actionのライフサイクル

composite actionの以下のような4つのライフサイクルで実行されます。

  1. Definition
  2. Integration
  3. Discovery
  4. Execution

Definition

ActionはCUE Definitionと呼ばれるテンプレートで記述されます。
DefinitionはActionのインプットとアウトプット、サブアクションを定義します。

package main

import (
    "dagger.io/dagger"
    "dagger.io/dagger/core"
)

// Write a greeting to a file, and add it to a directory
#AddHello: {
    // The input directory
    dir: dagger.#FS

    // The name of the person to greet
    name: string | *"world"

    write: core.#WriteFile & {
        input: dir
        path: "hello-\(name).txt"
        contents: "hello, \(name)!"
    }

    // The directory with greeting message added
    result: write.output
}

上記のサンプルはcore.#WriteFileと呼ばれるサブアクションを含んでいます。1つのActionは複数のサブアクションを組み込むことができます。

inputはその名と通りインフットで、Integrationのタイミングで値が決定します。外部からの入力を受け取ることができます。上記の例ではdirnameがインプットにあたります。
outputは逆に値を生成します。その値はIntegrationのタイミングで他のActionなどから参照することができます。上記の例ではresultがアウトプットにあたります。

dirresultなどのフィールドの名前に成約はありません。

Integration

Action definitionは直接実行できず、planに統合される必要があります。
planは実行コンテキストで、いかのようなことが定義します。

  • エンドユーザに提供されるAction
  • それらのタスクの依存関係
  • タスクと client システム(ローカルマシンとのインテグレーション)の相互作用

Actionの各CUEファイルはplanの CUR definitionにマージされ統合されます。
以下にplanの definition例を示します。

package main

import (
    "dagger.io/dagger"
)

dagger.#Plan & {
    // Say hello by writing to a file
    actions: hello: #AddHello & {
        dir: client.filesystem.".".read.contents
    }
    client: filesystem: ".": {
        read: contents: dagger.#FS
        write: contents: actions.hello.result
    }
}

上記の例では@AddHelloが直接planに統合されており、そして、core.#WriteFileは間接的に統合されています。
Planの詳細に関しては、後述します。

Discovery

planに統合されたActionはエンドユーザから利用することができます。
(ここではまだ下記のコマンドは実行できませんが、後ほど実行してみます)

$ dagger do --help
Usage: 
  dagger do  [flags]

Options


Available Actions:
 hello Say hello by writing to a file

(省略)

planにとうごうされたhello actionがDagger側から認識されます。

Execution

daggerから認識されたactionは、以下のように実行できます。
(ここではまだ下記のコマンドは実行できませんが、後ほど実行してみます)

$ dagger do hello

Hello WorldのActionを動かしてみる

コアコンセプトで利用したHello Worldのdefinitionを実行してみたいと思います。 まずはプロジェクトを初期化する必要があります。

$ mkdir helloworld && cd helloworld

$ dagger project init
Project initialized! To install dagger packages, run `dagger project update`

次にdagger packageをインストールするためにdagger project updateコマンドを実行します。

$ dagger project update
10:21PM INF system | installing all packages...
10:21PM INF system | installed/updated package dagger.io@0.2.11
10:21PM INF system | installed/updated package universe.dagger.io@0.2.11

ここまでで準備が完了です。
先程のコアコンセプトで利用したActionとplanのDefinitionをそれぞれhello.cueplan.cueという名前で保存します。 するとDagger側からActionを認識できるようになります。

d$ dagger do --help
Usage: 
  dagger do  [flags]

Options


Available Actions:
 hello Say hello by writing to a file

(省略)

実際にActionを実行してみます。

$ dagger do hello
10:35PM INF upgrading buildkit    have host network=true version=v0.10.3                                                                                                             
[✔] client.filesystem.".".read                                                                                                                                                                                 0.1s
[✔] actions.hello.write                                                                                                                                                                                        0.0s
[✔] client.filesystem.".".write                                                                                                                                                                                0.0s

すると、hello-world.txtというファイルが作成されます。

$ ls
cue.mod  hello-world.txt  hello.cue  plan.cue

$ cat hello-world.txt 
hello, world!

次はplanを少しだけ書き換えて、greetする人を変えてみます。
plan.cueを以下のように変更します。

package main

import (
    "dagger.io/dagger"
)

dagger.#Plan & {
    // Say hello by writing to a file
    actions: hello: #AddHello & {
        dir: client.filesystem.".".read.contents
        //ここを追加
        name: "henoheno"
    }
    client: filesystem: ".": {
        read: contents: dagger.#FS
        write: contents: actions.hello.result
    }
}

再度Actionを実行してみます。

$ dagger do hello
[✔] client.filesystem.".".read                                                                                                                                                                                 0.1s
[✔] actions.hello.write                                                                                                                                                                                        0.0s
[✔] client.filesystem.".".write                                                                                                                                                                                0.0s

$ ls
cue.mod  hello-henoheno.txt  hello-world.txt  hello.cue  plan.cue

$ cat hello-henoheno.txt 
hello, henoheno!

Kong Admin API GUIのKongaを試す

はじめに

Kongの諸々の管理する場合、Admin APIを使うのが1つの手かと思うのですが、いかんせんヒューマンフレンドリーでは無いと言うような課題を感じることがあります。
Technology RadarKongaというKongのAdmin API GUIを見つけたのでちょっと試して見ようかと思います。
ローカルで動かしてみて(Dockerで動かす想定)、GUIからためせそうな機能をひと通り見てみるぐらいをやってみようかと思います。Dockerなどの設定値はとりあえず動かすことを目的として設定しています。本番などで利用する際は値を適切なものに変えてください。SSLを使ってなかったりするのでセキュリティ的に問題が起こることがありえます。
そもそもKongに付いては過去にまとめてるのでよかったらこちら(※情報がちょっと古いかも知れませんが、基本はおそらくそこまで変わってないと思います) も見てみてください。

Kongaとは

前述していますが、KongaはKongを管理するためのGUIです。
GitHubによると以下のような機能を提供しているようです。

  • すべてのAdmin APIオブジェクトの管理
  • Consumer設定のリモートデータソースからのインポート(Database, files, APIsなど)
  • Kongの複数ノードの管理
  • スナップショット機能を使ったKong Nodeのバックアップとリストア、マイグレート機能
  • ノードのヘルスチェック、モニター機能
  • Email、Slackの通知機能
  • 複数ユーザの管理
  • データベースとの連携機能(MySQL, postgresSQL, MongoDB)

GitHubによるとKongaは公式のアプリではないようです。

動かしてみる

環境

動作環境は以下の通り

$ uname -srvmpio
Linux 5.4.0-100-generic #113-Ubuntu SMP Thu Feb 3 18:43:29 UTC 2022 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.4 LTS
Release:    20.04
Codename:   focal



$ docker version
Client: Docker Engine - Community
 Version:           20.10.12
 API version:       1.41
 Go version:        go1.16.12
 Git commit:        e91ed57
 Built:             Mon Dec 13 11:45:33 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.12
  Git commit:       459d0df
  Built:            Mon Dec 13 11:43:42 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

諸々動かしてみる

Kongの起動

先ずはKongを動かしてみます。前述していますが、今回、KongとKongaはDokcerで動かそうと思います。また、DB Lessモードで動かそうと思います。Kongの動かし方は以前書いたブログをそのままやりますので深くは触れません。
まず、以下のようなKongの設定ファイルを用意します。

kong.yml

_format_version: "1.1"

services:
- name: my-service
  url: https://example.com
  plugins:
  - name: key-auth
  routes:
  - name: my-route
    paths:
    - /

consumers:
- username: my-user
  keyauth_credentials:
  - key: my-key

次にKongをDockerで起動します。

docker run -d --name kong \
     --network=host \
     -v "/$PATH_TO_WORK_DIR/kong.yml:/usr/local/kong/declarative/kong.yml" \
     -e "KONG_DATABASE=off" \
     -e "KONG_DECLARATIVE_CONFIG=/usr/local/kong/declarative/kong.yml" \
     -e "KONG_PROXY_ACCESS_LOG=/dev/stdout" \
     -e "KONG_ADMIN_ACCESS_LOG=/dev/stdout" \
     -e "KONG_PROXY_ERROR_LOG=/dev/stderr" \
     -e "KONG_ADMIN_ERROR_LOG=/dev/stderr" \
     -e "KONG_ADMIN_LISTEN=0.0.0.0:8001" \
     kong:2.7.1-alpine

$ docker ps
CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS                           PORTS     NAMES
495804f277d4   kong:2.7.1-alpine   "/docker-entrypoint.…"   2 seconds ago   Up 1 second (health: starting)             kong

Kongaの起動

こちらもDockerを使います。
基本的にはGitHubのREADMEを参考にしながら動かしています。

 docker run  -d --network=host \
             --name konga \
             -e "TOKEN_SECRET=$RANDOM_STRINGS" \
             -e "NO_AUTH=true" \
             pantsel/konga:0.14.9

設定の環境変数は最小限にしています。 上記のものと他に環境変数は以下のようなものがあります。 ここでは主要なものをまとめますSSLに関するものもあったりするので全量はこちらをご確認ください。

環境変数 説明 デフォルト値
HOST kongaにバインドするホスト名 '0.0.0.0'
PORT kongaにバインドするポート番号 1337
NODE_ENV ノードの環境。production or development development
KONGA_HOOK_TIMEOUT Kongaがタスクをスタートアップする際のタイムアウト値(ms) 60000ms
DB_ADAPTER Kongaが使うDBの種類。mongomysqlpostgres。設定されていない場合はローカルディスクのDBが利用される -
DB_URI DBのフルURIの設定。DB_ADAPTERによって変わる -
DB_HOST DB_URIが設定されていない場合。DBのホスト名を設定する localhost
DB_PORT DB_URIが設定されていない場合。DBのポート番号 -
DB_USER DB_URIが設定されていない場合。DBのユーザ名 -
DB_PASSWORD DB_URIが設定されていない場合。DBのパスワード
DB_PG_SCHEMA Postgresを利用している場合のスキーマ public
KONGA_LOG_LEVEL Kongaのログレベル。sillydebuginfowarnerror development : debug、production : warn
TOKEN_SECRET KongaがJWTをサインする際に利用される文字列。ランダムな文字列を設定する -
NO_AUTH Kongaの認証を行うか否か。true or false false
BASE_URL Kongaの相対パス -
KONGA_SEED_USER_DATA_SOURCE_FILE 初回起動時のデフォルトユーザ設定。詳細はこちらを確認ください -
KONGA_SEED_KONG_NODE_DATA_SOURCE_FILE KongのAdminAPIに
KONGA_SEED_KONG_NODE_DATA_SOURCE_FILE 初回起動時に複数のKongノードにつなぎたい場合のAdmin APIの設定。詳細はこちらをご確認ください -

今回はなるべく複雑度を下げるため、NO_AUTHtrueにしてTOKEN_SECRETだけ設定しています。

Kongaにアクセスして初回設定を行う

今回は、Dockerのネットワーク設定をホストにしていてかつデフォルトで起動しているのでhttp://localhost:1337/ブラウザーからアクセスすることで以下のようなKongaの画面を確認できます。

f:id:yuya_hirooka:20220226133649p:plain

まずは、Kong Admin APIの設定を行う必要があるみたいですね。
以下の設定値を設定してします。

設定が正しく完了すると下のようなダッシュボードが開きます。

f:id:yuya_hirooka:20220226135011p:plain

起動したKongaで何ができそうかざっくり見ていく

Kongaが立ち上がったところで、GUIからどういったことができそうか簡単に見ていきます。
先ずは先程の設定後に表示されたダッシュボードですが、こちらではノードの情報やKongに来ているリクエストの数等を確認することができます。

f:id:yuya_hirooka:20220226135011p:plain

次にInfoタブではより詳細なノードの情報を確認することができそうです。

f:id:yuya_hirooka:20220226140053p:plain

SERVICEタブではKongのService Entityの管理ができそうです。
同様にROUTESタブではRoutes Entityの管理、CONSUMERSタブではConsumer Entityの管理が行えます。

f:id:yuya_hirooka:20220226140124p:plain

PLUGINSタブではPluginの追加や削除が行えるようです。

f:id:yuya_hirooka:20220226140541p:plain

ざっとみ、プラグインKong Plugin Hubからいい感じに取得してきてくれてそうですね。

f:id:yuya_hirooka:20220226140604p:plain

ざっくりとはこんな感じでしょうか、今回はとりあえず動かすことができたのでスナップショットなどは一旦触らずにおきます。

GoのロギングライブラリZapを試す

はじめに

GoのLogライブラリは標準のものを使うことが多かったのですが、構造化したりするのがちょっと大変だったので、より高機能で使い勝手がよいものを試してみたくなってZapというのをどこかで見かけたのを思い出して、ちょっと試してみようと思います。
ここでは、コアな設定などは行わず先ずはプリセットの設定などを利用して、Quick Startを参考にしたコードを動かし足がかりにしながら、少しだけ深堀りして学んで行きたいと思います。

Zapとは

タイトルの通り、Goのロギングライブラリです。GitHubを見ているかぎり速さを売りにしているみたいです。1.x系では破壊的変更が行われないことが明記されています(おそらく普通にセマンティクスバージョニングだと思います)。また、サポートに関しては過去2つ分のマイナーバージョンまでサポートがされているみたいです。
ログのスタイルとしては構造化されているスタイルと単にprintfスタイルのAPIが提供されているみたいです。

動かしてみる

環境

今回の動作環境はこちら

$ uname -srvmpio
Linux 5.4.0-100-generic #113-Ubuntu SMP Thu Feb 3 18:43:29 UTC 2022 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

$ go version
go version go1.17.7 linux/amd64

プロジェクトの初期化&Zapのインストール

先ずはプロジェクトの初期化からします。

$ mkdir zap-example && cd zap-example
$ go mod init zap-exampe

次にプロジェクトにZapのインストールを行います。

$ go get -u go.uber.org/zap
go get: added go.uber.org/atomic v1.9.0
go get: added go.uber.org/multierr v1.7.0
go get: added go.uber.org/zap v1.21.0

これで下準備は完了です。

何はともあれログを出力してみる

ひとまずGitHubのQuick Startを参考にログを出力してみます。
以下のコードを記述します。

package main

import (
    "go.uber.org/zap"
    "time"
)

func main() {
    logger, _ := zap.NewProduction()
    defer logger.Sync()

    sugar := logger.Sugar()
    sugar.Infow("This is my first Log with Zap",
        "int num", 3,
        "Time", time.Now(),
        "String", "Hello, Zap",
    )
}

もろもろ気になることはありますが、実行してみます。

$ go run main.go
{"level":"info","ts":1645328356.9622054,"caller":"zap-example/main.go:14","msg":"This is my first Log with Zap","int num":3,"Time":1645328356.9622047,"String":"Hello, Zap"}

いい感じに出力されました。

ちょっとコードを詳細に見ていきます。

ZapのLoggerプリセット

まず、Zapを使うためプリセットの設定がいくつ用意されています。上記のコードではzap.NewProduction()などで設定済みのロガーを取得することができます。
このプリセットはfunc NewDevelopmentfunc NewProductionfunc NewExamplefunc NewNopが用意されておりそれぞれログの出力レベルと出力の形式が異なっています。

すでに出力していますがNewProductionを利用した場合は、ログがJson形式で出力されよりツールで利用されることを想定したログが出力されます。また、ここでのログレベルはInfoレベルになり標準エラーに出力されるようです。
これは、NewProductionConfig().Build(...Option)のショートカットとなっているようです。

次にNewDevelopmentを利用した場合以下のような出力になります。

$ go run main.go
2022-02-20T12:49:50.559+0900    INFO    zap-example/main.go:14  This is my first Log with Zap   {"int num": 3, "Time": "2022-02-20T12:49:50.559+0900", "String": "Hello, Zap"}

ログ出力はよりヒューマンリーダブルな形で出力され、更にDebugレベルでのログ出力になります。 こちらも標準エラーにログが出力されます。

NewExampleを利用した場合は以下のような出力になります。

$ go run main.go
{"level":"info","msg":"This is my first Log with Zap","int num":3,"Time":"2022-02-20T13:05:35.718+0900","String":"Hello, Zap"}

こちらはZapをちょっと試したい時ときに利用されるもののようです。諸々が事前にわかりきっている(ドキュメントには決定論的?と書かれていました)環境で利用されるもので、時間などが省略されたより短いログ出力になります。こちらも出力レベルはDebugになるようです。

最後に、NewNopを利用した場合の出力です。

$ go run main.go

こちらはログ出力を全く行わないロガーで、ユーザが定義したHooksなども実行されません。
こちらは、WithOptions関数などで、Coreやエラーアウトプットを置き換えることでログ出力を行うことが可能になるようです。
(おそらく、性能などの関心で全くログ出力を行わない場合やマニュアルでログを設定したい場合に利用されるものだと個人的に理解しています)

defer logger.Sync()

ロガーを取得した後のdefer logger.Sync()ですが、これはアプリケーション終了時にすべてのログを出力してくれるようです。

Sugar

そして、個人的に最も気になったsugar := logger.Sugar()このSugarはZapのLoggerラッパーでLoggerを生で使うよりはほんの少し遅いもののより人間が使いやすい形に設計されているようです。後述しますが、suger.Infow()などの関数はSugarの方で提供されているみたいです。
このブログでは先ずはSugarを使って、ログ出力するところにフォーカスして進めます。

ログ出力

ログ出力の部分です。

sugar.Infow("This is my first Log with Zap",
    "int num", 3,
    "Time", time.Now(),
    "String", "Hello, Zap",
)

前述の通りZapは構造化されたログの形式とprintfスタイルのログ出力両方をサポートしています。
上記の用に〇〇w()関数(func (*SugaredLogger) Infow func (*SugaredLogger) Debugwfunc (*SugaredLogger) Errorwfunc (*SugaredLogger) Fatalw等)というようなwが着く関数は第一引数にメッセージを受け取り第二引数以降はkey-valueの形で構造化されるログを受け取ります。

printfスタイルのログを出力する場合は関数の後にfが着く以下のような関数を利用することで行えます。

sugar.Infof("Hello, %s", "World")

出力結果

{"level":"info","ts":1645332593.3569844,"caller":"zap-example/main.go:20","msg":"Hello, World"}

構造化されたスタイルのログ出力を行う場合でマイクロ秒レベルのパフォーマンスが求められる場合は以下のように事前に型を指定してkey-valueでわたしてやることでより低コストでログ出力を行えるようです。

sugar.Infow("This is my first Log with Zap",
    zap.Int("int num", 3),
    zap.Time("Time", time.Now()),
    zap.String("String", "Hello, Zap"),
)

Sealed Secretsでk8s上で利用する機密情報をGitなどでセキュアに管理する

はじめに

k8sには、Secretという機密情報を扱うリソースがありますが、こいつは基本情報をBase64エンコードしたもので扱われます。例えばSecretのリソース定義ファイルをGitで管理したいとなった場合定義ファイルに書かれる機密情報はただBase64されてるだけなのでそのままでは管理できない(やりにくい)という問題があります。
こういった問題にたいして、様々な対象方法はあるかと思いますが、その中の1つであるSealed Secretsを試してみたいと思います。

Sealed Secretsとは

Sealed SecretsのGitHubには以下のように書かれています。

Problem: "I can manage all my K8s config in git, except Secrets."

Solution: Encrypt your Secret into a SealedSecret, which is safe to store - even to a public repository. The SealedSecret can be decrypted only by the controller running in the target cluster and nobody else (not even the original author) is able to obtain the original Secret from the SealedSecret.

わかりやすいProblemですね。前述の通りSecretではBase64しただけの機密情報を扱うことになるので、Gitでの管理がなかなか難しくなってきます。それに対して、Sealed Secretでは機密情報を暗号化し、その複合ができるのはクラスターで動くコントローラーのみとなります。
機密情報は暗号化されるため、パブリックのGitリポジトリにPushすることも可能になります。
Sealed Secretsは以下の2つのパートから成り立ちます。

  • クラスターサイド: controller / operator
  • クライアントサイドユーティリティ: kubeseal

kubeseal公開鍵暗号方式で情報を暗号化し、その複合を行えるのはコントローラーだけとなります。
SealedSecretとSecretは、完全に同じではありませんがDeploymentとPodの関係に似ていると説明されています。

kubesealは公開鍵をk8sAPIサーバーから取得し、情報を暗号化します。kubeseal --cert mycert.pemのようにして、Pemファイルを直接指定してオフラインでの暗号化も可能なようです。この場合は、kubeseal --fetch-cert >mycert.pemでPemファイルを取得できるようです。鍵はコントローラーの起動時にログにも出力されるようです。

動かしてみる

環境

今回のk8sクラスターはminikubeを用いて作成します。

$ uname -srvmpio
Linux 5.4.0-99-generic #112-Ubuntu SMP Thu Feb 3 13:50:55 UTC 2022 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.12
 API version:       1.41
 Go version:        go1.16.12
 Git commit:        e91ed57
 Built:             Mon Dec 13 11:45:33 2021
 OS/Arch:           linux/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.12
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.12
  Git commit:       459d0df
  Built:            Mon Dec 13 11:43:42 2021
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 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.6 linux/amd64

$ minikube version
minikube version: v1.25.1
commit: 3e64b11ed75e56e4898ea85f96b2e4af0301f43d

$ helm version
version.BuildInfo{Version:"v3.8.0", GitCommit:"d14138609b01886f544b2025f5000351c9eb092e", GitTreeState:"clean", GoVersion:"go1.17.5"}


$ kubectl version -o yaml
clientVersion:
  buildDate: "2021-12-16T11:41:01Z"
  compiler: gc
  gitCommit: 86ec240af8cbd1b60bcc4c03c20da9b98005b92e
  gitTreeState: clean
  gitVersion: v1.23.1
  goVersion: go1.17.5
  major: "1"
  minor: "23"
  platform: linux/amd64
serverVersion:
  buildDate: "2021-12-16T11:34:54Z"
  compiler: gc
  gitCommit: 86ec240af8cbd1b60bcc4c03c20da9b98005b92e
  gitTreeState: clean
  gitVersion: v1.23.1
  goVersion: go1.17.5
  major: "1"
  minor: "23"
  platform: linux/amd64

今後実行するコマンドは、特に指定がない場合はminikubeのコンテキストを指しています。

インストール

インストールはおおきく以下の2つのことを行う必要があります。

  • Sealed Secretsのクラスターへのデプロイ
  • kubesealedのインストール

先ずは、Sealed Secretsのクラスターへのデプロイを行います。
READMEのInstallationによるとこれを行うには以下の3つの方法が提供されているようです。

  • Kustomize
  • Helm Chart
  • Operator Framework

GKEなどでセットアップする場合はこちらを確認してください。
今回はHelm Chartを使ったデプロイの方法を試してみたいと思います。Sealed SecretsのHelm ChartはGitHubの公式レポジトリでホスティングされています。以下のコマンドを実行し、リポジトリの追加を行います。

$ helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
"sealed-secrets" has been added to your repositories

ここで少し注意が必要なのは、バージョニングのスキーマで、このHelm Chartはもともとはコミュニティで作成されていたもので、メジャーバージョンが1.x.yで始まっています。しかし、Sealed SecretsのVersionhaまた0なので、以下のような対応関係を持ちます。

  • Sealed SecretsのコントローラーのVersion: 0.X.Y
  • Helm ChatのVersion: 1.X.Y-rZ

と、思いましたが....どうもChartの方は2系がすでに出ているみたいですね...しかもバージョニングスキーマも説明されるものとは少し違うようです。

$ helm search hub sealed-secrets
URL                                                 CHART VERSION   APP VERSION DESCRIPTION                                  
https://artifacthub.io/packages/helm/bitnami-la...  2.1.2           v0.17.3     Helm chart for the sealed-secrets controller.
https://artifacthub.io/packages/helm/wenerme/se...  2.1.2           v0.17.3     Helm chart for the sealed-secrets controller.
https://artifacthub.io/packages/helm/openinfrad...  1.16.1          v0.16.0     Helm chart for the sealed-secrets controller.
https://artifacthub.io/packages/helm/cloudnativ...  1.0.2           0.7.0       A Helm chart for Sealed Secrets              
https://artifacthub.io/packages/helm/redhat-cop...  1.10.2          0.12.1      A Helm chart for Sealed Secrets     

この辺は、もしかしたらドキュメントが少し古くなってるのかも知れません。ひとまずSealedSecretsのバージョンとHelm Chartのバージョンは完璧には対応づかないようなので注意が必要です。
今回は最新のChartを使ってデプロイを行いたいと思います。デプロイはこちらを参考に以下のコマンドを実行します。

$ helm install --namespace kube-system my-release sealed-secrets/sealed-secrets
NAME: my-release
LAST DEPLOYED: Sat Feb 12 10:26:21 2022
NAMESPACE: kube-system
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
(省略)

これでデプロイは完了です。my-releaseのところは任意のものを入れます。もしクリーンアップしたい場合はhelm --namespace kube-system delete my-release のコマンドで実行できます。
kubectlコマンドで諸々がデプロイされていることを確認します。

$ kubectl -n kube-system get all
NAME                                             READY   STATUS    RESTARTS      AGE
pod/coredns-64897985d-qfl8f                      1/1     Running   0             34m
pod/etcd-minikube                                1/1     Running   1             34m
pod/kube-apiserver-minikube                      1/1     Running   1             34m
pod/kube-controller-manager-minikube             1/1     Running   1             34m
pod/kube-proxy-vlq5q                             1/1     Running   0             34m
pod/kube-scheduler-minikube                      1/1     Running   1             34m
pod/my-release-sealed-secrets-559446f98f-52szw   1/1     Running   0             83s
pod/storage-provisioner                          1/1     Running   1 (33m ago)   34m

NAME                                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
service/kube-dns                    ClusterIP   10.96.0.10       <none>        53/UDP,53/TCP,9153/TCP   34m
service/my-release-sealed-secrets   ClusterIP   10.101.162.188   <none>        8080/TCP                 83s

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kube-proxy   1         1         1       1            1           kubernetes.io/os=linux   34m

NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns                     1/1     1            1           34m
deployment.apps/my-release-sealed-secrets   1/1     1            1           83s

NAME                                                   DESIRED   CURRENT   READY   AGE
replicaset.apps/coredns-64897985d                      1         1         1       34m
replicaset.apps/my-release-sealed-secrets-559446f98f   1         1         1       83s

これでデプロイまでは完了しました。
次にクライアント側のインストールです。
READMEにとると、kubesealをインストールするには以下のようなやり方が用意されているようです。

  • Homebrew
  • MacPorts
  • Installation from source

私の環境はLinuxなので最後のInstallation from sourceの方法を試そうかと思ったのですが、ちょっとうまく行かなかったのでバイナリを直接ダウンロードしてパスを通そうと思います。
現状の最新である0.17.3のリリースページを確認し、以下のコマンドでインストールします。

$ wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.17.3/kubeseal-0.17.3-linux-amd64.tar.gz
$ sudo mkdir /opt/kubeseal
$ sudo tar -zxvf kubeseal-0.17.3-linux-amd64.tar.gz -C /opt/kubeseal

あとはええ感じにパスを通して、OKです。

$ kubeseal --version
kubeseal version: 0.17.3

kubesealで作ったSecretsを暗号化する

kubesealでSecretsを暗号化します。
今回はAPIサーバ経由で鍵を取得して、暗号化する方法を試したいと思います。もし、この方法が使えない場合は以下のようなコマンドを実行してPemファイルを取得します。

$ kubeseal --fetch-cert \
--controller-name=my-release-sealed-secrets \
--controller-namespace=kube-system \
> pub-cert.pem

少し脇道にそれましたが、話を戻します。
適当なSecretsを作って、そのJsonファイルを出力します。今回はfoo=barというSecretsを作成します。

$ echo -n bar | kubectl create secret generic mysecret --dry-run=client --from-file=foo=/dev/stdin -o json >mysecret.json

次にそのSecretsのJsonを暗号化します。

$ kubeseal --controller-name=my-release-sealed-secrets --controller-namespace=kube-system <mysecret.json >mysealedsecret.json

ここでのポイントはコントローラーの名前(今回の場合はDeploymentの名前で行けました)とネームスペースをきちんと指定してやることです。

出力されたファイルは暗号化されたデータとなっているので、Gitでも管理できます(Twitterに上げることもできるよ!って書いてあった)。

$ cat mysealedsecret.json 
{
  "kind": "SealedSecret",
  "apiVersion": "bitnami.com/v1alpha1",
  "metadata": {
    "name": "mysecret",
    "namespace": "default",
    "creationTimestamp": null
  },
  "spec": {
    "template": {
      "metadata": {
        "name": "mysecret",
        "namespace": "default",
        "creationTimestamp": null
      },
      "data": null
    },
    "encryptedData": {
      "foo": "AgCHg3oahao+sLw2gPlH+9SaxWlYdG06/M5CZAOZ2hOHXZ9deWFF/bMpt+YRlThK5c1mNIj0rf/NsVxWzWL3N4/LFeNFYmJ/orjSYln3Qu4+2F03kKH30kcz23X8CeTQjpRoIYYsy6S0bLMn+Svs5EB669K/n+nEWNjXb5BmO3438GamQ5jodLlcv5zjZLjEpwrqb31HTs44r3NKhzp7sJZ5DaU5Q28r9IkGQneDDi6Y4dRwF/Kp80uiA9DGXRPcG39l0xfsljdEdwTF9NucazQEZ14eI3VUQD/ofQ2gdpkAUaOKW4nr9pHsQk+KErifBGZQOtvaCxqp2NoFwAG9lXcW0PntzR3m0VY4bqYCslr+Ma5D0kyuYojsOZHWJEmVPnrBu18sSzgdQSHgK5lj85hHgWzMWTB9LCifzyjUMzpHpOkVqvf0RmWAVTIGC5KrT4lbN/w0rCMW6mKfcszYUtUshVZYgaTcTMi+MPNAmSknDUVU2owOIzYTMzyz7iGXaM8zhS3q95h98rJiXiVzJIxLKxxbbmSI6bNuTYalstwXLqx3V5kZqnfLNAfWIS5+Hz2FSjrRlXyZCrCnTKrbij4Q+G97URlKTgekjOKyMfT7XVUpIr6R7dxMwTVDBXveSuvXwqPDsNsyHdF6aSPdUHpkCpAdzCxJLN0F6rzbNbZO4RJmA6kP2PVG90vySur/5sBuMIc="
    }
  }
}

できたSealed Secretsをクラスターにデプロイします。

$ kubectl create -f mysealedsecret.json
sealedsecret.bitnami.com/mysecret created

これで完了です。デプロイされたSealed Secretsはコントローラーによって複合されてSecretsが作成されます。

$ kubectl describe secret mysecret 
Name:         mysecret
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
foo:  3 bytes

後は、普通にSecretsを使う要領で利用できます。

Flixをインストールしてテストを実行する

はじめに

最近所属する会社内でFlixというJVM系の言語が新しく出たみたいなのを見かけて面白そうだったので少し触ってみたいと思います。 色々深堀りをできそうなポイントはあるのですが一旦深く突っ込まず個々ではインストールして動かすことと、簡単に概要を把握することに努めます。

Flixとは

オーフス大学ウォータールー大学が主体となって開発されている言語で、様々な言語からインスパイヤーされています。
公式サイトには以下のように記述されています。

Flix is inspired by OCaml and Haskell with ideas from Rust and Scala. Flix looks like Scala, but its type system is based on Hindley-Milner. Two unique features of Flix are its polymorphic effect system and its support for first-class Datalog constraints. Flix compiles to efficient JVM bytecode, runs on the Java Virtual Machine, and supports full tail call elimination.

FlixはOCamlHaskellの影響を受けており、RustやSlacaのアイディアなども取り入れているみたいですね。見た目はScalaですが、型システムはHindley-Milner型らしいです。
Hindley-Milnerは型推論アルゴリズムの一種であるようですが、ここではあまり主題ではないため深堀はしません。
その他にも公式サイトのWhyによればGoのチャンネル通信ベースの非同期やElmのようなextensible records、あとはユニークな機能としてpolymorphic effect system、purity polymorphic functions、first-class Datalog constraints(こいつらについても別のブログでまとめようかとは思いますが、ここでは深くふれません)というものがあるらしいです。
JVM系の言語なのでJavaバイトコードコンパイルされJVM上で動きます。

動かしてみる

動作環境

動作環境は以下とおりです。

$ uname -srvmpio
Linux 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 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)

Flixのバージョンなどはインストール時に明記します。

インストール

とりあえずFlixを気軽に動かしてみるには、プレイグラウンドを使って見る方法が一番簡単にできると思います。
また、VS Codoプラグインも用意されているようでそちらをインストールするのが現段階ではコーディングをゴリゴリやっていく上では良い方法かと思います。
ただ、このブログでは、Flixのjarをダウンロードして実際に手動でテストの実行やコンパイルを行います。 flix.jarはGitHubのリリースページのところから入手できます。
今回は現状の最新である0.25.0をダウンロードしてきて、適当なディレクトリに配置します。 自分の場合は/opt/flixディレクトリを作成しそこにおきました。

ビルド&パッケージマネージャー

公式のBuild and Package Managementによると現状(2022/01/02)ではまだ、依存解決などの方法は提供されていないようですが、ビルドしてパッケージをシェアすること自体は可能なようです。
セントラルなパッケージのリポジトリなどはまだなくバージョンの管理や配布などはマニュアルで行わないと行けないようです。
バージョン管理などの方法については将来的には提供される予定のようです(foo-1.2.1.fpkgという名前のアーティファクト?に含まれる予定のようです)。
現状Flixは、/path/to/flix.jar <command>のような実行方法でコンパイルやテストの実行 ここで、コマンドには以下のようなものがあります。

コマンド 説明
init カレントディレクトリに新しいプロジェクトの作成
check カレントプロジェクトにエラーがないかチェック
build カレントプロジェクトのコンパイル
build-jar カレントプロジェクトのjarファイルの作成
build-pkg カレントプロジェクトのfpkg-fileの作成
run カレントプロジェクトのメイン関数の実行
test カレントプロジェクトのテストの実行

このブログでは一通りのコマンドは試そうと思います。

標準APIの一覧

標準APIの一覧はここから確認できます。

Hello World

インストールと簡単な概要をまとめたところで、先ずはHello Worldを記述して実行してみます。
先ずは、プロジェクトを作成します。

$ mkdir hello-world && cd hello-world

$ java -jar /opt/flix/flix.jar init

$ tree
.
├── HISTORY.md
├── LICENSE.md
├── README.md
├── build
├── lib
├── src
│   └── Main.flix
└── test
    └── TestMain.flix

必要なものができたみたいですね。
見た感じ、src/にflixのコードを追加していって、test/にテストコードを記述するみたいです。
すでにMain.flixと言う名前のファイルが作成されHello Worldのプログラムが作成されています。

// The main entry point.
def main(_args: Array[String]): Int32 & Impure =
  Console.printLine("Hello World!");
  0 // exit code

このPJを実行するとHello Worldの文字列が標準出力に出力されます。

$ java -jar /opt/flix/flix.jar run
Hello World!                                                                    
Main exited with status code 0.

テストの記述

プロジェクトを初期化するとすでに以下のようなテストがtest/TestMain.flixに記述されています。
テストに関してはprinciplesのBuilt-in unit testsの項目で軽くふれられています。

Built-in unit tests
Flix supports unit tests as part of the language. We believe such integration avoids fragmentation of the ecosystem and ultimately leads to better tool support.

ライブラリーなどを使うのでは無くビルトインでテストの方法がサポートされているようですね。

@test
def test01(): Bool = 1 + 1 == 2

1+1の実行結果をアサーションしているテストですね。
@testを付けてBool型を返す関数を記述すれば良いようです。
テストに関して深く説明されたドキュメンテーションが見つからなかったのですが、おそらくMockなどの方法は現時点ではまだサポートされていないのだと思います。

上記のテストを実行すると以下のような結果が得られます。

$ java -jar /opt/flix/flix.jar test
-- Tests -------------------------------------------------- root                

  ✓ test01
  
  Tests Passed! (Passed: 1 / 1)

テストを以下のように書き換えて失敗するようにしてみます。

@test
def test01(): Bool = false

実行すると以下のような結果を得られます。

$ java -jar /opt/flix/flix.jar test
-- Tests -------------------------------------------------- root                

  ✗ test01: Returned false. (test/TestMain.flix:2:5)
  
  Tests Failed! (Passed: 0 / 1)

当たり前ですがテストは失敗しました。

それでは、独自の関数を1つ書いて、それをテストするコードを書いてみます。
関数は、人物名の文字列を受け取って、その文字列を返すだけの簡単なものを想定して作成します。

先ずはテストをTestMain.flix以下のように追記します。

@test
def testGreeting(): Bool = greeting("moheji") == "Hello!! moheji!!"

そして、空の実装の方もMain.flixに作っておきます。

// The main entry point.
def main(_args: Array[String]): Int32 & Impure =
  Console.printLine(greeting("moheji"));
  0 // exit code

def greeting(name: String): String = ???

この状態でテストを実行するとエラーになります。

$ java -jar /opt/flix/flix.jar test
-- Tests -------------------------------------------------- root                

  ✓ test01
  ✗ testGreeting: Hole '?h26182' at src/Main.flix:6:38 (test/TestMain.flix:5:5)
  
  Tests Failed! (Passed: 1 / 2)

それでは実装の方を修正します。

// The main entry point.
def main(_args: Array[String]): Int32 & Impure =
  Console.printLine(greeting("moheji"));
  0 // exit code

def greeting(name: String): String =
  "Hello!!" + " " + name + "!!"

この状態で、テストを実行すると今度は成功します。

$ java -jar /opt/flix/flix.jar test
-- Tests -------------------------------------------------- root                

  ✓ test01
  ✓ testGreeting
  
  Tests Passed! (Passed: 2 / 2)

jarを作成する

flixコマンドを使えばプロジェクトをJarに固めることができます。 実行は以下の用にbuild-jarコマンドで行います。

$ java -jar /opt/flix/flix.jar  build-jar

$ tree -L 1 
.
├── HISTORY.md
├── LICENSE.md
├── README.md
├── build
├── hello-world.jar
├── lib
├── src
├── target
└── test

コマンドを実行すると特にログ出力も無くプロジェクトルートにhello-world.jarが作成されているのがわかります。
このjarを実行してみます。

$ java -jar hello-world.jar 
Hello!! moheji!!

正しく実行を行えているようですね。

Spring Boot 2.6.0についてメモ

はじめに

もうしばらく前ですがSpring Bootが11月9日に出てますね。
ちょっと仕事がバタバタしててモチベーションが出ず更新ができていなかったのですがまたゆっくりでも再開していきたいと思います。
このブログではすべての変更点を網羅するわけでは無く、個人的に気になったところをまとめてみたり使ってみたりしようと思います。
2.6.0のすべての変更に付いてはRelease Noteを確認してください。

ちなみに話題になっているLog4J2やLogbackなどの脆弱性の対応は12月23日に出る2.6.2(2.5系なら2.5.8)でライブラリのアップデートが入るみたいですね。

動かして確認してみる

環境

$ 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


$ uname -srvmpio
Linux 5.4.0-91-generic #102-Ubuntu SMP Fri Nov 5 16:31:28 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ ./mvnw -v
Apache Maven 3.8.3 (ff8e977a158738155dc465c6a97ffaf31982d739)
Maven home: /home/yuya-hirooka/.m2/wrapper/dists/apache-maven-3.8.3-bin/5a6n1u8or3307vo2u2jgmkhm0t/apache-maven-3.8.3
Java version: 17, vendor: Oracle Corporation, runtime: /home/yuya-hirooka/.sdkman/candidates/java/17-open
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-91-generic", arch: "amd64", family: "unix"


$ 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)

今回動作確認を行うアプリケーションはSpringを使って設定は以下のようにして作ります。

f:id:yuya_hirooka:20211218155111p:plain

Bootのバージョンはこのブログを書いている段階での最新である2.6.1をを使おうと思います。
動作確認の関係でWebとActuatorの依存だけ入れてます。

大まかな変更点

2.6.0では大きく以下のような変更が行われいます。

  • サーブレットアプリでのSameSite Cookie Attributeのサポート
  • Actuator周りの変更
  • WebTestClientを用いたMVCアプリケーションのテストサポート
  • Recordクラスで@ConfigurationPropertiesを使う際に@ConstructorBindingをつける必要がなくなった
  • Docker Imageビルド周りのサポートの追加

EOL

Spring Boot のEOLはこちらの公式のサイトにまとめられています。
それに寄ると2.6.x系のサポートは2023年5月18日に完全にサポートが終わるようですね。

まとめていく

サーブレットアプリでのSameSite Cookie Attributeのサポート

Servletのアプリケーションに置いて、SameSiteの設定をserver.servlet.session.cookie.same-siteプロパティを通して設定することが可能になったようです。
これはTomcat、Jetty、Undertowなどのサーバーで利用可能です。

Actuator周りの変更

メインポートや管理ポートに追加のパスを指定することができるようになった

ActuatorのHealth Groupの機能を使えば複数のヘルスインディケーターをグルーピングして公開したりすることができます。別のポートヘルスチェックのエンドポイントを公開するとその信頼性が下がってしまう場合があります。そのような場合に置いて追加パスを指定して管理用のポートではなくサーバのポートでヘルスチェックのエンドポイントを公開することができるようになりました。

例えば以下のような設定でグループを作ります。

application.properties

management.server.port=9090
management.endpoint.health.group.moheji.include=ping

ここでmehejiの部分は任意の文字列でつけるグループ名になります。
このヘルスグループにアクセスする際には以下のようにリクエストを送ります。

$ curl http://localhost:9090/actuator/health/moheji
{"status":"UP"}

これに対して/actuator/health/mohejiなどのPathでは無く追加のアクセス用のPathを作ることができます。
メインポートと管理用ポートのどちらかで公開することができます。 例えば下記のような設定を記述するとmainのポートの/henohenoのパスで mohejiグループのヘルスチェックのエンドポイントが公開できるようです。

management.endpoint.health.group.moheji.additional-path="server:/henoheno"

メインのポートでは無く管理用のポートを使いたい場合は server:の代わりにmanagement:を使うようにします。

infoのエンドポイントでJavaの実行環境の情報を取得できるようになった

management.info.java.enabled=trueと設定することでJavaの実行環境の情報を/actuator/infoから取得できるようになります。
以下のように設定を追加します。

management.endpoints.web.exposure.include=info
management.info.java.enabled=true

cURLでリクエストを投げると実行環境を取得できてます。

$ curl localhost:9090/actuator/info  | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   222    0   222    0     0  44400      0 --:--:-- --:--:-- --:--:-- 44400
{
  "java": {
    "vendor": "Oracle Corporation",
    "version": "17",
    "runtime": {
      "name": "OpenJDK Runtime Environment",
      "version": "17+35-2724"
    },
    "jvm": {
      "name": "OpenJDK 64-Bit Server VM",
      "vendor": "Oracle Corporation",
      "version": "17+35-2724"
    }
  }
}

WebTestClientを用いたMVCアプリケーションのテストサポート

SpringMVCをモック環境で利用することが可能になりました。
今までは、Mock環境のWebFluexか実際に立ち上がっているサーバに対するサポートしか行われていませんでしたが、今回のアップデートで @AutoConfigureMockMvcを付けたクラスはWebTestClientをDIで受け取ることができる用になり、テストを実行できます。
注意点としてはWebTestClientを使うためにはWebfluxが依存として追加されている必要があるので今回は以下の用に依存をPomに追加しておきます。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

依存の追加ができたら実際にテストを記述していきます。
例えば、以下のようなコントローラをテストしたい場合。

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

次のようにテストを記述することができます。

@SpringBootTest
@AutoConfigureMockMvc
class HelloControllerTest {

    @Autowired
    WebTestClient client;

    @Test
    public void testHelloController() {
        client.get()
                .uri("/hello")
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("hello");
    }
}

Recordクラスで@ConfigurationPropertiesを使う際に@ConstructorBindingをつける必要がなくなった

Recordsで@ConfigurationPropertiesを使う場合もしコンストラクターが1つである場合は@ConstructorBindingをつける必要がなくなりました。

例えば今までだとRecordクラスで@ConfigurationPropertiesを使う場合は以下のように記述する必要がありました。

@ConfigurationProperties("greeting")
@ConstructorBinding
public record MyProperties(String word) { }

これが2.6からはコンストラクターが1つしかない場合は以下のように@ConstructorBindingを省略することができます。

@ConfigurationProperties("greeting")
public record MyProperties(String word) {}

先程のWebTestClientでテストしたControllerをプロパティーを読み込んだ文字列を返すように変更してみます。

MyProperties.java

@ConfigurationProperties("greeting")
public record MyProperties(String word) {}

Config.java

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(MyProperties.class)
public class Config {
}

HelloController.java

@RestController
public class HelloController {
    final private MyProperties properties;

    public HelloController(MyProperties properties) {
        this.properties = properties;
    }

    @GetMapping("/hello")
    public String hello() {
        return properties.word();
    }
}

application.propertiesにプロパティーを追加します。

greeting.word=Hello

アプリを起動してcURLでアクセスします。

$ curl localhost:8080/hello
Hello

Docker Imageビルド周りのサポートの追加

追加のイメージタグ

Docker Imageビルドの際にMavenやGradleに設定を記述すれば追加のタグを指定できるようになり、同じイメージを複数の名前でビルドできるようになりました。
例えばMavenの場合以下のようにPomに設定を記述することで複数のイメージをビルドすることができます。

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <image>
                    <name>greeting-app</name>
                    <tags>
                        <tag>hoge</tag>
                        <tag>fuga</tag>
                    </tags>
                </image>
            </configuration>
        </plugin>
    </plugins>
</build>

そして、./mvnw spring-boot:build-imageを実行すると以下のように3つのイメージをビルドしてくれます。

$ ./mvnw spring-boot:build-image
(省略)
[INFO] Successfully built image 'docker.io/library/greeting-app:latest'
[INFO] 
[INFO] Successfully created image tag 'docker.io/library/hoge'
[INFO] 
[INFO] Successfully created image tag 'docker.io/library/fuga'
(省略)

キャッシュの設定

The CNB builder caches layers that are used when building and launching an image. By default, these caches are stored as named volumes in the Docker daemon with names that are derived from the full name of the target image. If the image name changes frequently, for example when the project version is used as a tag in the image name, then the caches can be invalidated frequently.

Cloud Native Buildpacksはイメージをビルドするサイト起動する際にそれぞれキャッシュを行いますが、デフォルトではフルのイメージ名を利用してそのキャシュを管理します。例えばイメージの名前がよく変わるなどする場合は(tagでバージョニングを行っている際など)このキャッシュが同様の頻度で利用できなくなってしまいまいます。
そういった問題を解決するためキャッシュの変わりの名前を設定できるようになりました。

この機能を利用するためには次のような設定をPomに記述します。

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <buildCache>
                            <volume>
                                <name>cache-${project.artifactId}.build</name>
                            </volume>
                        </buildCache>
                        <launchCache>
                            <volume>
                                <name>cache-${project.artifactId}.launch</name>
                            </volume>
                        </launchCache>
                    </image>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

JavaでUnix Domain Socketを使ってみる(Dockerのコンテナ一覧を取得する)

はじめに

Java 16でUnix Domain Socketのサポートが入っています(JEP 380)。前から興味はありつつもかなか触れてなかったので今回少し触ってみようかと思います。

お題として、以下の2つをやってみようと思います。

  • Echoサーバとそのクライアントを書く
  • JavaのプログラムからDockerを Unix Domain Socketを使って操作してみたいと思います。

Unix Domain Socket?

Unix Domain Socketは単一マシンで複数プロセスが、効率の良い通信を行なうためのソケットインターフェースです。 TCP/IPソケットと似たようなインターフェースですが、インターネットプロトコルの代わりにファイルシステムを利用しています。
以下のような特徴があるようです。

  • 同一マシン内で厳密にコミュニケーションを行い、リモートでの通信が必要ないためよりセキュアに通信できる
  • TCP/IPよりもセットアップが速い。スループットが高い

少し前にはなりますが、WindowsでもUnix Domain Socketサポートが入ったみたいです。

JavaUnix 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