GeekFactory

int128.hatenablog.com

あさりのリゾット

あさりのリゾットを作ったらめっちゃ美味しかったのでメモを残します。

材料(2人分)

材料 数量
1合
あさり 1パック
たまねぎ 半玉
にんにく 適量
じゃがいも 1玉
白ワイン 200ml
100ml
バター 適量
適量
黒胡椒 適量
オリーブオイル 大量

作り方

まず、あさりを酒蒸しにします。

  1. あさりは2時間ぐらい砂抜きしておきます。
  2. にんにくをみじん切りにします。
  3. フライパンにあさり、にんにく、白ワイン150ml、塩、バター、オリーブオイル、黒胡椒を入れて、蓋をして5分ほど蒸します。
  4. ざるで具材とスープに分けます。
  5. スープに水を加えて、弱火で保温しておきます。

それから、じゃがいもを蒸します。

  1. じゃがいもを適当な大きさに切ります。
  2. ラップで包んで電子レンジで蒸します。800Wで2分程度です。

じゃがいもと並行して米を炒めていきます。

  1. フライパンにオリーブオイルを引いて、塩を加えて、にんにくを炒めます。
  2. 香りがついてきたら、たまねぎを炒めます。
  3. 米を洗わずに入れて炒めます。オリーブオイルをたっぷり加えます。
  4. 米が透明になってきたら、スープを半分ぐらい加えます。蓋をして5分ほど蒸します。
  5. スープを1/4ぐらい加えて、さらに5分ほど蒸します。
  6. 残りのスープ、白ワイン50ml、あさり、じゃがいもを加えて、さらに5分ほど蒸します。
  7. 少し芯が残る程度(アルデンテ)になったら、黒胡椒を加えて完成です。

ポイント

米を蒸すときはフライパンが干上がらないように気をつけましょう。少しずつスープを足すとよいです。あと、蒸しすぎると味が落ちてしまうので、少し芯が残る程度がおすすめです。オリーブオイルはたっぷり入れましょう。

Swagger自動生成スタブの継続的デプロイ

SwaggerにはYAMLからAPIクライアントやAPIサーバのコードを自動生成する機能があります。コードの自動生成を利用することでドメインの実装に集中でき、また、スタブを利用することでチーム開発を円滑に進められるといったメリットがあります。

しかし、コードの自動生成を利用すると、API仕様を更新したのにスタブは古いままといったライフサイクルのズレが起こりがちです。そこで、CIで継続的に自動生成を実行することで、常に最新のスタブを共有しながらチーム開発を行えるようになります。

本稿では、CIとGradleを利用してスタブコードを自動生成する方法を説明します。具体的には、CIで以下を実行します。

  1. 開発者がSwagger YAMLを変更する。
  2. Gradleでスタブコードを自動生成する。
  3. GradleでスタブコードからJARをビルドする。
  4. JARをデプロイする。

前提

以下のマルチプロジェクト構成を前提に説明します。

  • ルートプロジェクト
    • api
      • src/main/swagger/api.yaml - API仕様
    • codegen
      • build.gradle - ビルドスクリプト
      • template/*.mustache - テンプレート(任意)
      • build/swagger-server - 自動生成コードの出力先
    • stub
      • build.gradle - ビルドスクリプト
      • build/libs/stub.jar - JARの出力先

Swaggerによるスタブコードの自動生成

ビルドスクリプトからswagger-codegen-cliを実行します。

// codegen/build.gradle
configurations {
    swagger
}

dependencies {
    swagger 'io.swagger:swagger-codegen-cli:2.2.1'
}

task generateSwaggerServer(type: JavaExec) {
  description 'Generate source code for API server'
  main = 'io.swagger.codegen.SwaggerCodegen'
  classpath = configurations.swagger
  args 'generate',
       '-i', file("${project(':api').projectDir}/src/main/swagger/api.yaml"),
       '-o', file("$buildDir/swagger-server"),
       '-l', 'spring',
       '--additional-properties', 'dateLibrary=java8'
}

swagger-codegen-cliに引数を渡すことで、自動生成されるコードをカスタマイズできます。上記の例では、Springのコードを生成する、Java 8の日付クラスを使うといったことを指定しています。

テンプレートのカスタマイズ

自動生成のテンプレートをカスタマイズするには、swagger-codegenリポジトリにあるテンプレートフォルダを使います。例えば、Springのテンプレートをカスタマイズするには以下のフォルダをローカルに配置します。

https://github.com/swagger-api/swagger-codegen/tree/master/modules/swagger-codegen/src/main/resources/JavaSpring

ビルドスクリプトではテンプレートの場所をswagger-codegen-cliの引数に渡します。

task generateSwaggerServer(type: JavaExec) {
  description 'Generate source code for API server'
  main = 'io.swagger.codegen.SwaggerCodegen'
  classpath = configurations.swagger
  args 'generate',
       '-i', file("${project(':api').projectDir}/src/main/swagger/api.yaml"),
       '-o', file("$buildDir/swagger-server"),
       '-t', file('template'),  // テンプレートの場所
       '-l', 'spring',
       '--additional-properties', 'dateLibrary=java8'
}

スタブのビルド

自動生成されたコードをビルドします。ここではSpring BootでJARを生成します。

// stub/build.gradle
buildscript {
    ext { springBootVersion = '1.4.0.RELEASE' }
    repositories { jcenter() }
    dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") }
}

plugins {
    id 'java'
    id 'spring-boot'
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

sourceSets.main.java.srcDir file("${project(':codegen').buildDir}/swagger-server/src/main/java")
sourceSets.main.resources.srcDir file("${project(':codegen').buildDir}/swagger-server/src/main/resources")

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-web'
    compile 'io.springfox:springfox-swagger2:2.5.0'
    compile 'io.springfox:springfox-swagger-ui:2.5.0'
}

compileJava.dependsOn project(':codegen').generateSwaggerServer

buildタスクを実行すると、JARファイルが出力されます。

./gradlew :stub:build

スタブのデプロイ

/stub/build/libs/stub.jar に出力されたJARをサーバにデプロイします。Dockerを利用すると簡単に実行できるので便利です。

# stub/Dockerfile
FROM java:8
EXPOSE 8080
COPY build/libs/stub.jar /
ENTRYPOINT ["java", "-jar", "/stub.jar"]
# stub/docker-compose.yml
version: "2"
services:
  app:
    build: .
    ports:
      - "8080:8080"

以下のコマンドを実行すると、スタブが実行されます。古いスタブが実行されている場合は新しいものに置き換わります。

docker-compose build
docker-compose up -d

サーバの /v1 にアクセスしたらSwagger UIが表示されます。また、YAMLに定義済みのパスにアクセスしたら空のJSONが返ってきます。

今後の課題

swagger-codegen-cliが自動生成するコードは空の値を返す実装になっています。これではスタブの利用側に不親切なので、 example セクションに書いたデータを返す実装にしてほしいと思います。

Gradleからswagger-codegenを利用するプラグインはすでにありますが、swagger-codegen-cliの最新版に追いついていないので、今回はJARを直接実行することにしました。本当はプラグインを利用する方がビルドスクリプトの可読性が高くてよいと思います。

Gradle SSH Plugin 2.6.0 released

Gradle SSH Plugin 2.6.0、Groovy SSH 2.6.0をリリースしました。

github.com

2.6.0の変更点

New feature

  • Add executeScript method for script execution (thanks to @matthiasbalke)
  • Add inputStream setting for command or shell (thanks to @matthiasbalke)
  • Automatically add host key to known_hosts

Bug fixes

  • Fix executeSudo works if error message is changed from default

Add executeScript method for script execution

リモートでスクリプトを実行するメソッド executeScript を追加しました。これまではファイルを転送してから実行する必要がありましたが、 executeScript では文字列で指定したスクリプトを直接実行できるようになりました。

executeScript '''#!/bin/bash -xe
echo 1
echo 2
'''

executeScript は文字列からShebangを読み取ってインタープリタを実行します。上記の例では /bin/bash -xe を実行します。そして、標準入力に文字列を流し込むことでスクリプトを実行します。

Add inputStream setting for command or shell

executeexecuteBackgroundexecuteSudo メソッドに標準入力を渡せるようになりました。前項の executeScript はこの仕組みで実現しています。

execute '/bin/sh', inputStream: '''#!/bin/sh
echo 1
echo 2
'''

executeSudo メソッドはsudoプロンプトとのやり取りがあるので実装が難しかったです。sudoプロンプトにパスワードを渡した後に標準入力にデータを渡します。ただし、パスワード認証に失敗した場合は再度プロンプトが現れることを考慮する必要があります。

Automatically add host key to known_hosts

2.6.0の目玉ともいえる機能です。これまではsshコマンドなどでknown_hostsを生成する必要がありましたが、Gradle SSH Plugin単体でknown_hostsに自動的にホストキーを追加できるようになりました。sshコマンドでいう StrictHostKeyChecking=ask ですね。

knownHosts = addHostKey(file('.ssh/known_hosts'))

これまでHost Key Checkingに起因する問題が多く寄せられていたため、この機能を追加することにしました。

実装にはだいぶ苦戦しました。苦労したのは以下の2点です。

  1. UserInfo を指定するとHost Key CheckingだけでなくUser Authenticationにも副作用がある。
  2. ゲートウェイ(踏み台)を経由する場合、トンネルのローカルポートがknown_hostsに書かれてしまう。

当初の実装では StrictHostKeyChecking=ask を指定して、JSchの UserInfo でHost Key Checkingのプロンプトにtrueを返すことでknown_hostsを更新する仕組みにしていました。しかし、この実装ではユーザ認証失敗時の例外のメッセージが Auth cancel に変わってしまいます。どうやら UserInfo がnullを返すと認証がキャンセルされたことになるようです。

また、ゲートウェイを経由する場合はトンネルのローカルポートを読み替える必要があります。 通常の方法では、known_hostsに書き出すホストを変えることは不可能です。

そこで、以下の流れに実装を変えることにしました。

  1. StrictHostKeyChecking=ask を指定してリモートホストに接続する。
  2. Host Key Checkingに成功した場合は、そのまま処理を続ける。
  3. Host Key Checkingに失敗した場合は、StrictHostKeyChecking=ask を指定してリモートホストに再接続する。そして、Host Key Repositoryに追加されたホストキーをknown_hostsに書き出す。もしゲートウェイ経由の場合はホスト名を読み替える。

テストを通っているので問題ないはずですが、もしバグを見つけたら教えてください。

Bump to Groovy 2.4.7

traitに10個以上のプロパティを含めるとコンパイルエラーになるバグが2.4.4以前に含まれるため、2.4.7に上げました。

[GROOVY-7387] Trait compilation error: IllegalArgumentException: Comparison method violates its general contract! - ASF JIRA

振り返り

StrictHostKeyCheckingやHostKeyRepositoryの挙動にだいぶ悩まされました。Server Integration Testを書いていたおかげでいろいろ試行錯誤してたどり着けたと思います。

少しずつCIの時間短縮を進めていて、5分台に乗るようになりました。Gradle 1.xサポートを廃止すればもっと短くなりそうです。