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

Echoのロガーを使う

はじめに

GoのフレームワークEchoを使うことが最近は多いのですが、そのログ周りの機能を試してみようかと思います。

Echoのロガー

Echoにはロガーに関するサポートが以下の2つあります。

このブログでは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コマンドで初期設定可能なものとしては、以下のようなビルダーをサポートします。

また、skaffold initではプロジェクトのディレクトリ内を走査して、以下のようなファイルからビルドの設定を読み込みます。

  • Dockerfile
  • build.gradle/pom.xml
  • package.json
  • requirements.txt
  • go.mod
  • init

この際、500MB以上のファイルは無視されるようです。
例えば今回作成下アプリの場合Dockerfilego.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.yamlk8sのリソース定義ファイルである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 buildskaffold runskaffold devなどで実行されるようです。
Buildステージでは以下のようなビルドの方法があるみたいです。

各ビルド手法でのビルダーサポートはここをご確認ください。
ローカルでは基本的にどのようなビルダーもサポートしているようです。

Buildに関する設定はskaffold.yamlbuildセクションで行います。

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を指定するとタグの上書きが可能になるようです。

今回はcustomTemplateiinputDigestdatetimeを用いてタグ付けをしてみます。 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 testskaffold runskaffold 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 devskaffold runskaffold deployなどのコマンドで実行されます。

デプロイ時には以下のステップが実行されます。

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 devskaffold debugskaffold deployskaffold 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

(省略)

ログからポートフォワードが動作しているのがわかります。
cURLlocalhost:1323にアクセスしてみます。

$ curl localhost:1323/hello
Hello, World!

Lifecycle Hooks

Lifecycle Hooksでは、Skaffoldのライフサイクルイベントによってコードを実行したりすることができます。
buildsyncdeployのフェーズでそれぞれbeforeafterのホックを設定できます。
以下の2つのタイプのホックが存在します。

  • Host hooks : ランナー(例えばホストマシン)内で実行されるホック
  • Container hooks : 実行されるコンテナ内で実行されるホック

また、Host hooksでは、フェーズによって利用できるいくつかの環境変数が存在しており、例えばbuildsyncのフェーズではSKAFFOLD_IMAGEというイメージ名が設定された環境変数が利用可能です。
環境変数の一覧とその環境変数が利用できるフェーズの詳細はこちらをご確認ください。

今回は、Host hooksを利用して、ビルドの前と後にechoコマンドを実行してイメージ名を出力してみます。
具体的にはskaffold.yamlbuildセクションを以下のように書き換えます。

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 devskaffold 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のコマンドには以下のようなものがあります。

この中からいくつかのコマンドは利用してみたいと思います。

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を使ってプロジェクトを作成します。
設定は以下のように

f:id:yuya_hirooka:20210926121552p:plain

コントローラーを作って登録する

リクエストで名前を受け付けて、挨拶を返すコントローラーを作成します。
まずは、リクエストとレスポンスを受ける用の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を使ってRequestMappingHandlerMappingGreetingControllerを受け取ってます。
また、RequestMappingInfoを使ってリクエストのマッピング設定を記述し、その定義とコントローラーのリクエストをハンドルするMethodオブジェクト、そしてコントロラー本体をRequestMappingHandlerMappingに登録しています。
この際に色々処理を入れてロジックでハンドラーの定義をごにょごにょできるみたいですね。

さっくり触っただけですが、ほぼ同じような記述でMVC側も動くみたいです。
あと、Recordクラスはほんとに今まで通りな感じで使えましたね。
関数エンドポイントの方でも今までと全くおんなじように利用できました。