Quarkusのテンプレートエンジン "qute"
この記事は赤帽エンジニア Advent Calendar 2019の12/11分の記事です。
今日は日本JBossユーザグループでQuarkus入門について話しましたが、スライドはこちらです。今日の記事では、スライドに入りきらなかった qute について紹介します。
背景
今年の4月にGlassFishユーザ会に向けてQuarkus0.14.0を触りながら、実際の開発で使うならば欲しいと思った機能が2つありました。
DBアクセスについては、Quarkus自体のアーキテクチャがVert.xベースになったことにより、スライドでも言及している Quarkus - Reactive SQL Clients が導入され、元々はノンブロッキングなDBアクセスを目的とする機能ですが、APIがJdbcTemplateに似てるのでこれで良いなと。
もう一つのテンプレートエンジンは、近年はフロントエンドは別に作り込まれる方が一般的になってきている一方で、エンタープライズなアプリではサーバサイドテンプレートでも十分機能するのでは、ちょっとした社内システムにそこまでフロントエンド必要なのかなと思っていま..(ごめんなさいクライアントサイドも勉強します)
テンプレートエンジン "qute"
quteはQuarkus向けに新たに開発された、Graal VMのnative-imageコマンドによるネイティブビルドに対応したテンプレートエンジンです。先日、Quarkusのmasterブランチにマージされましたが、まだ最新のリリース版であるQuarkus1.0.1には含まれていません。
シンプルな機能のみを提供しており { name }
のように {}
で囲まれた部分をテンプレートとして置き換えます。 以降にサンプルを紹介します。
qute Hello, world!
以下はテンプレート部分です。ソースコードの src/main/resources/templates
に以下のようなhello.htmlを配置します。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Hello Quarkus Qute template engine</title> </head> <body> <h1>Hello Quarkus Qute template engine</h1> <p>Hello, {name}!</p> </body> </html>
JAX-RSのエンドポイント実装からテンプレートを解決したhtmlを応答したい場合は、以下のように書きます。
import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; @Path("hello") public class HelloResource { @Inject Template hello; // src/main/resources/templates/hello.html の拡張子を除いた hello に合わせる @GET @Produces(MediaType.TEXT_HTML) public TemplateInstance get(@QueryParam("name") String name) { return hello.data("name", name); // 第1引数はテンプレートにかかれたキー name, 第2引数はテンプレートに適用する値 } }
コードの説明はコメントの通りで、シンプルに扱えることがわかります。
繰り返し each / 条件分岐 if
以下の例のように繰り返しやifなどの最低限の機能は満たされています。
<p>List of items over 100 yen</p> <ul> {#each items} {#if it.price > 100} <li>name: {it.name} , price: {it.price}</li> {/if} {/each} </ul>
{#each}
には、Iterable、MapのentrySet、Streamインスタンスが設定できるようになっています。it.name
のitはループ中の現在の要素を示しており、it.<プロパティ名>でItemクラスのnameプロパティにアクセスしています。
java側のコードは以下の通りです。
public class Item { private String name; private int price; // getter & setter 省略 } import io.quarkus.qute.Template; import io.quarkus.qute.TemplateInstance; import javax.inject.Inject; import javax.ws.rs.*; import javax.ws.rs.core.MediaType; import java.util.Arrays; import java.util.List; @Path("items") public class ItemResource { @Inject Template items; @GET @Path("/all") @Produces(MediaType.TEXT_HTML) public TemplateInstance getAll() { return items.data("items", getSnacks()); } private List<Item> getSnacks() { Item ice = Item.of("ice cream", 120); Item cookie = Item.of("cookie", 80); Item cake = Item.of("cake", 200); Item chocolate = Item.of("chocolate", 90); return Arrays.asList(ice, cookie, cake, chocolate); } }
GitHub上に上記のコードを置いています quarkus-qute-helloworld
本日時点はまだないですが、正式版がリリースされているときには quarkus-quickstarts にサンプルコードが他の機能と同様に用意されるはずです。
"qute" を実際に試す
quteはまだリリースされていないため、試すためにはquarkusのmasterブランチを自分でビルドします。テストをスキップするとGraalVMを用意しなくてもOpenJDK8でビルド可能です。
git clone https://github.com/quarkusio/quarkus.git cd quarkus mvnw clean package -DskipTests=true
自分でビルドしたmasterブランチのQuarkus(バージョン999-SNAPSHOT)をアプリケーションで利用するときに少し注意が必要です。
mvn io.quarkus:quarkus-maven-plugin:1.0.1.Final:create
で生成したひな型プロジェクトでは、pom.xmlにおいてquarkus-universe-bom
がbomが参照されていますが、前述ののビルドではquarkus-universe-bomはローカル上の~/.m2生成されていません。自分でビルドしたQuarkusでは、ビルド時に生成されるquarkus-bom
に書き換えます。
<?xml version="1.0"?> <project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> ... <properties> ... <quarkus-plugin.version>999-SNAPSHOT</quarkus-plugin.version> <!-- バージョンを999-SNAPSHOTに書き換える --> <quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id> <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id> <!-- quarkus-universe-bom から書き換える --> <quarkus.platform.version>999-SNAPSHOT</quarkus.platform.version> <!-- バージョンを999-SNAPSHOTに書き換える --> ....
上記については公式ガイドに言及があります。
quarkus-qute-helloworldに置いたコードでは、pom.xmlの修正は既に盛り込まれています。これでローカル環境でquarkus-qute-helloworldが mvn clean package
でビルドして試せるようになりました。
まとめ
- quteはQuarkusにこれから盛り込まれる予定のネイティブコンパイルに対応したテンプレートエンジンです
{ }
で囲った部分をテンプレートして置換します- Quarkus1.0おめでとう!
Galleonを使ってWildFlyのイメージサイズを小さくする
GalleonはWildFly16から対応した、WildFlyのバイナリを作るためのプロビジョニングツールです。詳細についてはWildFly News - WildFly 16 and Galleon, towards a cloud native EE application serverにまとめられていますが、Galleonにより以下が実現できます。
- JAX-RS/CDI/JPAなど、よく使うAPIだけ含んだサイズの小さなWildFlyサーバを作る
- WildFlyから使わない機能を削ってDockerイメージサイズを小さくする
- Galleonで作った小さなWildFlyは従来のWildFlyと同じようにAPサーバとしての使い勝手で使える。ThorntailのようなUber-jarではない。
Galleonを使う
実際に使ってみる方がイメージが湧きやすいので、使い方から紹介します。
GalleonはWildFlyコミュニティで開発されているツールですが、WildFlyにはバンドルされておらず、GitHubのGalloenのリポジトリからダウンロードします。このブログを書いた時点の最新バージョンは4.0.3.Finalです。
ダウンロードしたツールを任意のディレクトリに展開します
$ mkdir work $ cp ~/Downloads/galleon-4.0.3.Final.zip . $ unzip galleon-4.0.3.Final.zip Archive: galleon-4.0.3.Final.zip creating: galleon-4.0.3.Final/ creating: galleon-4.0.3.Final/bin/ inflating: galleon-4.0.3.Final/LICENSE inflating: galleon-4.0.3.Final/bin/galleon.sh inflating: galleon-4.0.3.Final/bin/galleon-cli-logging.properties inflating: galleon-4.0.3.Final/bin/galleon-cli.jar inflating: galleon-4.0.3.Final/bin/galleon.bat
まずは早速GalleonでWildFlyサーバを作成してみます。対象のWildFlyのバージョンはこの記事を書いている時点で最新の17.0.1.Final、--layersにはサーバに含める機能を指定するオプションでcloud-profile
とはJAX-RS/CDI/JPA/JTAなどのよく使われるAPI実装に加えて、WildFly自体の動作に必要なサーバのコア機能のみ持つことを示します。--dirは作成したWildFlyサーバの出力先です。
Maven経由で必要なサブシステムをダウンロードしてWildFlyを組み立てるため、初回実行時は数分かかる場合もあります。
$ cd galleon-4.0.3.Final $ bin/galleon.sh install wildfly:current#17.0.1.Final --layers=cloud-profile --dir=cloud-profile Feature-packs resolved. Feature-packs resolved. Packages installed. JBoss modules installed. Configurations generated. Feature pack installed. ======= ============ ============== Product Build Update Channel ======= ============ ============== wildfly 17.0.1.Final current
Galleonによって生成された機能がそぎ落とされたWildFlyサーバの構成を確認すると一見して同じようなディレクトリ構造に見えます。GalleonはThorntailのように、Uber-jarに必要な機能をまとめるのではなく、通常のWildFlyと同じ構成を保ったまま、サブシステムを削ることで小サイズ化を実現しています。
$ cd cloud-profile $ ls LICENSE.txt bin jboss-modules.jar standalone README.txt copyright.txt modules
$WILDFLY_HOME/bin の中身を見ると、WildFlyを使ったことがある人なら違和感を感じると思います。jboss-cli.sh、add-user.shや、LinuxのOS起動時にWildFlyを起動させるinit.dスクリプト、jboss-client.jarが含まれていません。
これらの$WILDFLY_HOME/binに含まれていたスクリプトを含んだWildFlyサーバを生成したい場合は、--layersにcore-tools
を追加します。
$ bin/galleon.sh install wildfly:current#17.0.1.Final --layers=cloud-profile,core-tools --dir=cloud-profile-with-tools
他にもどんなlayerがあるかは、WildFly Admin Guideの12.4. WildFly Galleon layersに解説があります。
ユースケース例として、WildFlyにSpring Frameworkを使ったアプリケーションをデプロイしたいので、GalleonによってEJBやremotingサブシステム(JMSサーバ接続やリモートEJBに使われるWildFly固有の機能)を削って、欲しい機能だけ持つWildFlyを作ることが考えられます。
Galleonが生まれた背景
コンテナのイメージサイズの削減がGalleonの目的です。
WildFlyには、元々@Statelessが付与されたコードがデプロイされてからEJBサブシステムをクラスロードするなど、アプリケーションで使われている機能をデプロイ処理で検知して必要な機能だけロードして余分なMetaspaceを消費しないようにする仕組みが備わっています。このため、Galleonによって使われないサブシステムを削除しても、メモリ削減にはあまり効果は期待できません。
前述のcloud-profileの場合、通常のWildFlyと比較しても起動直後のJavaヒープメモリのフットプリント削減の効果は手元で計測する限りでは10MB程度です。
APサーバとJDK、S2Iツール群など含んだJBossのコンテナイメージはどうしてもサイズが大きくなります。JBoss EAP7の場合、最新のイメージでは940MBと大きなイメージです。
$ docker pull registry.redhat.io/jboss-eap-7/eap72-openjdk11-openshift-rhel8:1.0-4 $ docker images REPOSITORY TAG IMAGE ID CREATED SIZE registry.redhat.io/jboss-eap-7/eap72-openjdk11-openshift-rhel8 1.0-4 cbc9919e54f5 3 weeks ago 940MB
Dockerイメージのアプリのレイヤだけ別で、全て最新の同一バージョンのJBoss EAPが同一のワーカーノードで起動している場合はDockerによるイメージレイヤの共有が期待できます。しかし、別バージョンのJBoss EAPが複数起動している場合はディスクサイズをそれなりに消費します。
Web管理コンソールやリモートJMXサーバなど、EAP自体にもコンテナ環境ではあまり使わない機能が含まれています。このモノリスなAPサーバから使わない機能を削ってイメージサイズをなるべく小さくすることがGalleonの目的です。
必要な機能だけアーカイブに含める考え方はThorntailでも実現できていましたが、Quarkusの登場に伴い、Thorntailは今後メジャーバージョンアップせずにメンテナンスモードとなる*1ため、WildFlyとしてもイメージサイズ削減に手を打っています。
OpenShiftにGalleonで作ったWildFlyをデプロイする
Galloenを使ったWildFlyコンテナをデプロイするためのテンプレートやImageStreamはwildfly-s2iリポジトリで配布されています。テンプレートの使い方などの詳しいドキュメントは READMEに書かれています。
Galleonには前述のCLIの他にもMavenプラグインのgalleon-maven-pluginがあり、WildFlyのS2Iビルダイメージでは、BuildConfigによるイメージのビルド時にMavenプラグインのGalleonを動かして小さなWildFlyバイナリを生成します。
単純にS2Iビルド時にGalleonでWildFlyをビルドしても、元々のコンテナイメージのレイヤのサイズは小さくならず、ただレイヤを重ねてビルダイメージよりも大きくなるだけです。このためS2Iのツール群やJDK、WildFly本体を含んだビルダイメージquay.io/jfdenise/wildfly-centos7:latestと、WildFly本体やS2Iツール群を削ってJDKと便利スクリプトのみ含められたランタイムイメージquay.io/jfdenise/wildfly-runtime-centos7:latestの2種類のコンテナイメージが配布されています。
テンプレートには2つのBuildConfigが含まれています。1つはWildFlyにビルダイメージとソースコードをインプットに従来と同等のアプリケーションイメージを作成するBuildConfigと、生成されたアプリケーションイメージからGalleonで生成したディレクトリを抜き取って、ランタイムイメージにマージしたサイズの小さなアプリケーションイメージを生成するBuildConfigです。絵にすると以下のようなイメージです。
"抜き取る"とは、具体的には以下のようにBuildConfigを定義して、アプリケーションイメージの /s2i-output/server/ の内容をランタイムイメージにコピーしています。
- apiVersion: build.openshift.io/v1 kind: BuildConfig ... spec: output: ... source: dockerfile: |- FROM wildfly-runtime-centos7:latest COPY /server $JBOSS_HOME USER root RUN chown -R jboss:root $JBOSS_HOME && chmod -R ug+rwX $JBOSS_HOME RUN ln -s $JBOSS_HOME /wildfly USER jboss CMD $JBOSS_HOME/bin/openshift-launch.sh images: - from: kind: ImageStreamTag name: ${APPLICATION_NAME}-build-artifacts:latest paths: - sourcePath: /s2i-output/server/ destinationDir: "."
minishiftで実際に試してみます。
# プロジェクトの作成 $ oc new-project wildfly-galleon # ImageStreamとTemplateのロード $ oc create -f https://raw.githubusercontent.com/wildfly/wildfly-s2i/wf-17.0/templates/wildfly-builder-imagestream.yml $ oc create -f https://raw.githubusercontent.com/wildfly/wildfly-s2i/wf-17.0/templates/wildfly-runtime-imagestream.yml $ oc create -f https://raw.githubusercontent.com/wildfly/wildfly-s2i/wf-17.0/templates/wildfly-s2i-chained-build-template.yml # テンプレートwildfly-s2i-chained-build-templateによるイメージのビルド $ oc new-app --template=wildfly-s2i-chained-build-template -p APPLICATION_NAME=cloudprofile-app -p GIT_REPO=https://github.com/nagetsum/eap-debug.git -p GIT_CONTEXT_DIR=cloudprofile-app -p IMAGE_STREAM_NAMESPACE=wildfly-galleon -p GALLEON_PROVISION_SERVER=cloud-profile-h2
テンプレートのパラメータGALLEON_PROVISION_SERVER
がGalleonに渡すパラメータで、この例ではJAX-RS/CDI/JPA + H2インメモリDBを示すcloud-profile-h2
を指定します。その他のパラメータは前述のREADMEに言及があります。
イメージのサイズは従来のアプリケーションイメージが1.63GBに対し、小さいなアプリケーションイメージは568MBと約3分の1となりました。
$ minishift ssh [docker@minishift ~]$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE 172.30.1.1:5000/wildfly-galleon/cloudprofile-app latest 43b73a025880 28 minutes ago 568 MB 172.30.1.1:5000/wildfly-galleon/cloudprofile-app-build-artifacts latest a18f05787add 29 minutes ago 1.63 GB quay.io/jfdenise/wildfly-centos7 <none> 6983f22f7f32 6 weeks ago 1.22 GB quay.io/jfdenise/wildfly-runtime-centos7 <none> c449eca1cc04 6 weeks ago 436 MB
テンプレートを実行しただけではイメージがビルドされるだけなので、作ったイメージをデプロイします。
$ oc new-app cloudprofile-app:latest --> Found image 43b73a0 (37 minutes old) in image stream "wildfly-galleon/cloudprofile-app" under tag "latest" for "cloudprofile-app:latest" Tags: wildfly, wildfly17 * This image will be deployed in deployment config "cloudprofile-app" * Ports 8080/tcp, 8778/tcp will be load balanced by service "cloudprofile-app" * Other containers can access this service through the hostname "cloudprofile-app" --> Creating resources ... deploymentconfig.apps.openshift.io "cloudprofile-app" created service "cloudprofile-app" created --> Success Application is not exposed. You can expose services to the outside world by executing one or more of the commands below: 'oc expose svc/cloudprofile-app' Run 'oc status' to view your app. $ oc expose svc/cloudprofile-app route.route.openshift.io/cloudprofile-app exposed
このアプリケーションではJAX-RSのエンドポイントにアクセスすると、JPAを使ってインメモリのH2データベースから本情報を取り出しています。
$ oc get route NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD cloudprofile-app cloudprofile-app-wildfly-galleon.192.168.64.7.nip.io cloudprofile-app 8080-tcp None $ curl cloudprofile-app-wildfly-galleon.192.168.64.7.nip.io/cloudprofile-app/api/books/1; echo {"author":"Joshua Bloch","id":1,"title":"Effective Java"}
まとめ
RESTEasy BeanValidationエラーメッセージのカスタマイズ
JAX-RS Specでは、BeanValidationのバリデーションエラー時には、エラー原因を返すことのみが規定されており、メッセージのフォーマット自体は実装依存です。
7.6 Validation and Error Reporting
In all cases, JAX-RS implementations SHOULD include a response entity describing the source of the error; however, the exact content and format of this entity is beyond the scope of this specification.
この記事ではRESTEasyの場合にどのようにメッセージフォーマットをカスタマイズするか言及します。
RESTEasyデフォルトの振る舞い
例として以下のバリデーションを実行を考えます。
public class Customer { @NotBlank(message = "Customer id is required.") private String id; @NotBlank(message = "Customer name is required.") private String name; @NotBlank(message = "Customer location is required.") private String location; ... } @Path("/customer") public class CustomerResource { @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_JSON) public Customer post(@Valid Customer cu) { ... } }
WildFlyやQuarkusが内蔵するJAX-RS実装のRESTEasyの場合、バリデーションエラー時にはHTTPレスポンスヘッダにvalidation-exception:
を含むと共に、以下のようなフォーマットのメッセージを返します。各プロパティはバリデーションの種類を示しています。例の場合はメソッドパラメータでバリデーションしているため、
trueparameterViolations
にメッセージが含まれています。
// 見やすいように改行を含めていますが、実際には改行は含まれません { "exception":null, "fieldViolations":[], "propertyViolations":[], "classViolations":[], "parameterViolations":[ { "constraintType":"PARAMETER", "path":"post.arg0.name", "message":"Customer name is required.", "value":"" }, { "constraintType":"PARAMETER", "path":"post.arg0.id", "message":"Customer id is required.", "value":"" } ], "returnValueViolations":[] }
エラーメッセージのカスタマイズ
もっとシンプルに以下のようなエラーメッセージのリストを返すことを考えます。
{"errors":["Customer id is required.","Customer name is required."]}
org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapperクラスをオーバーライドするとメッセージフォーマットのカスタマイズができます。
コード例は以下の通りです。ResteasyViolationExceptionMapperクラスでは後で拡張できるように、いくつかのメソッドのスコープがprotectedに設定されています。WildFly17で動作します。Quarkus0.16.1では原因は詳しくデバッグしていませんが動作しません。
package sample; import org.jboss.resteasy.api.validation.ResteasyViolationException; import org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper; import org.jboss.resteasy.api.validation.Validation; import javax.json.Json; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.Response.ResponseBuilder; import javax.ws.rs.ext.Provider; import java.util.List; import java.util.stream.Collectors; @Provider public class ViolationExceptionMapper extends ResteasyViolationExceptionMapper { @Override protected Response buildViolationReportResponse(ResteasyViolationException exception, Response.Status status) { ResponseBuilder builder = Response.status(status); builder.header(Validation.VALIDATION_HEADER, "true"); if (isAcceptJson(exception.getAccept())) { builder.type(MediaType.APPLICATION_JSON); List<String> errors = exception.getViolations() .stream() .map(violation -> violation.getMessage()) .collect(Collectors.toList()); builder.entity(Json.createObjectBuilder() .add("errors", Json.createArrayBuilder(errors).build()) .build()); return builder.build(); } // Default media type. builder.type(MediaType.TEXT_PLAIN); builder.entity(exception.toString()); return builder.build(); } private boolean isAcceptJson(List<MediaType> accepts) { return accepts.stream() .anyMatch(mt -> MediaType.APPLICATION_JSON_TYPE.getType().equals(mt.getType()) && MediaType.APPLICATION_JSON_TYPE.getSubtype().equals(mt.getSubtype())); } }
org.jboss.resteasy.api.validation
パッケージにクラスパスが通るように、pom.xmlのdependencyにresteasy-jaxrs
を追加します。
<dependencies> <dependency> <groupId>org.jboss.spec</groupId> <artifactId>jboss-javaee-web-8.0</artifactId> <version>1.0.3.Final</version> <type>pom</type> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jaxrs</artifactId> <version>3.7.0.Final</version> <scope>provided</scope> </dependency> </dependencies>
まとめ
- JAX-RS仕様ではBeanValidationエラー時のメッセージフォーマットは規定されておらず実装依存
- RESTEasyの場合、エラー時にはHTTPレスポンスに
validation-exception : true
を含み、独自フォーマットのメッセージを返す - メッセージフォーマットのカスタマイズには
org.jboss.resteasy.api.validation.ResteasyViolationExceptionMapper
の継承クラスを作る - 参考 RESTEasyドキュメント Chapter 55. Validation
Quarkusのスレッドプール設定
Quarkusで実行されるブロッキングタスク(JAX-RS/Servletなど)は、従来のAPサーバと同様にランタイム内部で管理されるスレッドプールにより実行されます。
WildFly/JBoss EAPでtask-max-threadsに相当するパラメータは、ソースコード中のapplication.propertiesにおいてquarkus.thread-pool.max-threads
として設定します。
src/main/resources/application.properties
# スレッドプールのアイドルスレッド数。未設定時のデフォルトは1。 quarkus.thread-pool.core-threads=32 # スレッドプールの最大数。未設定時のデフォルトは論理プロセッサ数x8。 quarkus.thread-pool.max-threads=64
このスレッドの実態はスレッドダンプを取得すると executor-thread-n
として確認できます。
kill -3 <pid> "executor-thread-1" #24 daemon prio=5 os_prio=31 tid=0x00007f966b09b000 nid=0x5c03 waiting on condition [0x00007000112da000] java.lang.Thread.State: TIMED_WAITING (sleeping) at java.lang.Thread.sleep(Native Method) at sample.quarkus.jpajaxrscdijta.Sleeper.sleep(Sleeper.java:7)
スレッドプールのチューニングはQuarkus0.16.1の時点では公式ドキュメントに言及がないですが、ブロッキングタスクを動かすならばチューニングした方が良いです。
- デフォルト値が小さい。WildFlyではtask-max-threadsは論理プロセッサ数 x 16ですが、Quarkus0.16.1では論理プロセッサ数 x 8です。JDBCによるブロッキングI/O待ちですぐに枯渇します。
- dockerの
--cpuset-cpus=0
など、コンテナレイヤで論理プロセッサ数を絞っている場合は、絞られたCPU数 x 8になる。- Quarkusの実装ではLinux環境の場合はコア数算出にRuntime.availableProcessor()依存せず、/proc/self/statusのCpus_allowedの値からコア数算出するorg.wildfly.common.cpu.ProcessorInfoクラスを使っているため、Java8のマイナーバージョンに関わらずcgroupsが反映されます。
Quarkus入門
GlassFish Users Group Japan 勉強会 2019 SpringでQuarkusについて紹介しました。話そうと思っていたけれどもスライドに入らなかったことをブログで補足してみます。
Quarkusとは何か
とにかく起動が高速な Java EE / MicroProfile ベースのAPIでコードが書けるフレームワークです。スライド中でも言及していますが、GraalVMのnative-imageコマンドによってネイティブコンパイルすると100ミリを切るスピードでJAX-RS/CDI/JPAで構成されるCRUDアプリケーションが起動します。
Javaは起動が遅い、Goのようなシングルバイナリによる起動の方が便利でJavaは手間が掛かると言われる課題を解決しています。
続きについてはスライドを参照してください。ここからは、スライドに入らなかった内容や、当日話そうと思っていたけど忘れていた内容をまとめます。
Quarkusがビルド時に自動生成するクラス
mvn clean package
すると、target/wiring-classes
にQuarkusがビルド時にクラスファイルを展開してuber-jarの中に含めるような動きをします。JAX-RS/CDI/JPAのサンプルアプリケーションの場合、アプリケーションのパッケージsample.quarkus.jpajaxrscdijta
だけでなく、ランタイム自体のクラスファイルが生成されている様子がわかります。
$ tree target/wiring-classes target/wiring-classes ├── META-INF │ ├── build-config.properties │ ├── quarkus-default-config.properties │ └── services │ ├── io.quarkus.arc.ComponentsProvider │ └── io.quarkus.arc.ResourceReferenceProvider ├── io │ └── quarkus │ ├── agroal │ │ └── runtime │ │ ├── DataSourceProducer.class │ │ ├── DataSourceProducer_Bean$$function$$6.class │ │ ├── DataSourceProducer_Bean.class │ │ ├── DataSourceProducer_ClientProxy.class │ │ └── DataSourceProducer_ProducerMethod_createDefaultDataSource_7c487e3ef869f878aa871e917c94f4d26d5d5c56_Bean.class │ ├── arc │ │ ├── ActivateRequestContextInterceptor_Bean.class │ │ ├── runtime │ │ │ └── LifecycleEventRunner_Bean.class │ │ ├── runtimebean │ │ │ └── RuntimeBeanProducers.class │ │ └── setup │ │ └── Default_ComponentsProvider.class │ ├── deployment │ │ └── steps │ │ ├── AgroalProcessor$build3.class │ │ ├── AgroalProcessor$configureRuntimeProperties6.class │ │ ├── ArcAnnotationProcessor$build10.class │ │ ├── ConfigBuildStep$validateConfigProperties11.class │ │ ├── HibernateOrmProcessor$build8.class │ │ ├── HibernateOrmProcessor$build9.class │ │ ├── HibernateOrmProcessor$startPersistenceUnits14.class │ │ ├── LifecycleEventsBuildStep$startupEvent17.class │ │ ├── LoggingResourceProcessor$setupLoggingRuntimeInit5.class │ │ ├── LoggingResourceProcessor$setupLoggingStaticInit1.class │ │ ├── NarayanaJtaProcessor$build4.class │ │ ├── ResteasyScanningProcessor$setupInjection12.class │ │ ├── RuntimeBeanProcessor$build2.class │ │ ├── ThreadPoolSetup$createExecutor7.class │ │ ├── UndertowArcIntegrationBuildStep$integrateRequestContext13.class │ │ ├── UndertowBuildStep$boot16.class │ │ └── UndertowBuildStep$build15.class │ ├── hibernate │ │ └── orm │ │ ├── panache │ │ │ └── PanacheEntity.class │ │ └── runtime │ │ ├── DefaultEntityManagerProducer_Bean.class │ │ ├── DefaultEntityManagerProducer_ProducerField_entityManager_Bean.class │ │ ├── JPAConfig_Bean.class │ │ ├── RequestScopedEntityManagerHolder_Bean$$function$$7.class │ │ ├── RequestScopedEntityManagerHolder_Bean.class │ │ ├── RequestScopedEntityManagerHolder_ClientProxy.class │ │ └── TransactionEntityManagers_Bean.class │ ├── narayana │ │ └── jta │ │ └── runtime │ │ ├── NarayanaJtaProducers_Bean.class │ │ ├── NarayanaJtaProducers_ProducerMethod_transactionManager_9989455b3b53ac81c17ca945c636473b7202fe4e_Bean$$function$$8.class │ │ ├── NarayanaJtaProducers_ProducerMethod_transactionManager_9989455b3b53ac81c17ca945c636473b7202fe4e_Bean.class │ │ ├── NarayanaJtaProducers_ProducerMethod_transactionManager_9989455b3b53ac81c17ca945c636473b7202fe4e_ClientProxy.class │ │ ├── NarayanaJtaProducers_ProducerMethod_transactionSynchronizationRegistry_ad29dd72d7aa0c9be8f98e90052c29fc262ea31a_Bean$$function$$9.class │ │ ├── NarayanaJtaProducers_ProducerMethod_transactionSynchronizationRegistry_ad29dd72d7aa0c9be8f98e90052c29fc262ea31a_Bean.class │ │ ├── NarayanaJtaProducers_ProducerMethod_transactionSynchronizationRegistry_ad29dd72d7aa0c9be8f98e90052c29fc262ea31a_ClientProxy.class │ │ └── interceptor │ │ ├── TransactionalInterceptorMandatory_Bean.class │ │ ├── TransactionalInterceptorNever_Bean.class │ │ ├── TransactionalInterceptorNotSupported_Bean.class │ │ ├── TransactionalInterceptorRequired_Bean.class │ │ ├── TransactionalInterceptorRequiresNew_Bean.class │ │ └── TransactionalInterceptorSupports_Bean.class │ ├── runner │ │ ├── ApplicationImpl1.class │ │ ├── AutoFeature.class │ │ └── GeneratedMain.class │ └── runtime │ └── generated │ ├── BuildTimeConfig.class │ ├── BuildTimeConfigRoot.class │ ├── RunTimeConfig.class │ ├── RunTimeConfigRoot.class │ └── RunTimeDefaultConfigSource.class ├── javax │ ├── enterprise │ │ ├── context │ │ │ └── control │ │ │ └── ActivateRequestContext_Shared_AnnotationLiteral.class │ │ └── inject │ │ └── Produces_Shared_AnnotationLiteral.class │ ├── persistence │ │ └── PersistenceContext_Shared_AnnotationLiteral.class │ └── transaction │ └── Transactional_Shared_AnnotationLiteral.class └── sample └── quarkus └── jpajaxrscdijta ├── EmployeeResource_Bean.class ├── EmployeeService_Bean$$function$$1.class ├── EmployeeService_Bean.class ├── EmployeeService_ClientProxy.class ├── EmployeeService_Subclass$$function$$2.class ├── EmployeeService_Subclass$$function$$3.class ├── EmployeeService_Subclass$$function$$4.class ├── EmployeeService_Subclass$$function$$5.class └── EmployeeService_Subclass.class
スライド中で言及している以下のようなコードの@Injectの解決の場合、
package sample.quarkus. jpajaxrscdijta; @Path("/") public class EmployeeResource { @Inject private EmployeeService service;
wiring-classes/sample/quarkus/jpajaxrscdijta/EmployeeResource_Bean.class
にインジェクション実装が含まれており、putfield命令でシンプルにフィールド名service
に値を設定するクラスファイルが自動生成されている様子が確認できます。
javap -v wiring-classes/sample/quarkus/jpajaxrscdijta/EmployeeResource_Bean.class ... public sample.quarkus.jpajaxrscdijta.EmployeeResource create(javax.enterprise.context.spi.CreationalContext); descriptor: (Ljavax/enterprise/context/spi/CreationalContext;)Lsample/quarkus/jpajaxrscdijta/EmployeeResource; flags: ACC_PUBLIC ... 30: aload_3 31: checkcast #57 // class sample/quarkus/jpajaxrscdijta/EmployeeService 34: putfield #61 // Field sample/quarkus/jpajaxrscdijta/EmployeeResource.service:Lsample/quarkus/jpajaxrscdijta/EmployeeService;
これらのクラスをデプロイしてから動的バイトコード生成で行うのではなく、事前に可能な処理はビルド時に実行するため、GraalVMによってネイティブバイナリを生成しなくても、java -jarでも従来のThorntail/WildFlyと比較して高速に起動しています。
Quarkusでスレッドダンプ
GraalVM Community Editionのnative-imageコマンドで生成された実行バイナリでは、kill -3 (SIGQUIT) が投げられるとHotSpotJVMと異なりプロセスが終了します。
Quarkusではシグナルハンドラを独自に実装しており、kill -3を実行プロセスに投げるとコンソールにスレッドダンプを出力します。内部的にはsun.misc.SignalHandler
を使って実装しています。
2019-05-21T14:22:58.915Z Thread dump follows: "XNIO-1 Accept" #22 prio=5 tid=0x7f6cb77e9700 java.lang.thread.State: RUNNABLE at com.oracle.svm.core.posix.headers.linux.LinuxEPoll.epoll_wait(LinuxEPoll.java) at sun.nio.ch.EPollArrayWrapper.epollWait(EPollArrayWrapper.java:326) at sun.nio.ch.EPollArrayWrapper.poll(EPollArrayWrapper.java:269) at sun.nio.ch.EPollSelectorImpl.doSelect(EPollSelectorImpl.java:93) at sun.nio.ch.SelectorImpl.lockAndDoSelect(SelectorImpl.java:86) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:97) at sun.nio.ch.SelectorImpl.select(SelectorImpl.java:101) at org.xnio.nio.WorkerThread.run(WorkerThread.java:532) at com.oracle.svm.core.thread.JavaThreads.threadStartRoutine(JavaThreads.java:473) at com.oracle.svm.core.posix.thread.PosixJavaThreads.pthreadStartRoutine(PosixJavaThreads.java:193) ...
ヒープダンプはGraalVM Enterprise EditionによりGraalVMレイヤで実現されています。無償で利用可能なGraalVM Community Editionでは現状ヒープダンプを取得することはできません。
OpenShiftでJBossクラスタを構成する仕組み
この記事は赤帽エンジニア Advent Calendar 2018の12/4分の記事です。
Red HatでJBoss EAPのサポートエンジニアをしています。自由なAdvent Calendarなので、自由に勉強メモを書いてみます。
JBossのようなAPサーバはクラスタメンバの探索に従来マルチキャストを使ってきましたが、OpenShift*1やk8sの環境が一般的になると、マルチキャストが使えないネットワークの方が多いです。
Red Hat Container Catalogで公開されているOpenShift向けのJBossコンテナイメージJBoss EAP CD for OpenShiftがどのようにクラスタを組んでいるかメモします。
EAP CDってなんぞやから、テストアプリケーションのデプロイ、クラスタディスカバリの仕組みDNS_PINGについて順を追って紹介します。
JBoss EAP CD for OpenShift
JBoss EAP Continuous Delivery for OpenShiftとは、2018年4月から3〜4ヶ月単位でリリースされている、OpenShift環境向けのEAPのバイナリです。今のところはTechnology Preview*2で、今後導入される予定のEAP新機能を先行して試す検証用を目的としています。EAP CDの3〜4つのリリースを組み合わせて、従来の同様にフルサポートされるEAPがリリースされるモデルとなっています。
従来のEAPと異なり、zip版はリリースされておらず、Red Hat Container Catalogを通じて、コンテナイメージをダウンロードする方式のみ提供されています。
デフォルトのプロファイルは standalone-openshift.xml であり、コンテナ起動時に /opt/eap/bin/launch に含まれるスクリプト類でstandalone-openshift.xmlをsedで色々置換して設定を作成する仕組みになっています。
OpenShiftがなくても、手元のDockerで起動してイメージの中身を確認できます。イメージにはbashが入っているので、通常のEAPとの差分については /opt/eap を見てみてください。
$ docker run --name eap-cd -p 8080:8080 registry.access.redhat.com/jboss-eap-7-tech-preview/eap-cd-openshift:14.0-4 $ docker exec -it eap-cd /bin/bash cd /opt/eap
テストアプリケーションのデプロイ
OpenShiftの環境があることを前提に、テストアプリケーションのデプロイをします。
eap-debug/session-test at master · n-agetsu/eap-debug · GitHubにある、簡単なセッションレプリケーションの動作確認用アプリです。
EAP CD向けのImageStreamとTemplateの登録
はじめにEAP CD向けの最新のImageStreamとTemplateをプロジェクト名 openshift に登録します。12/4時点ではEAP CD 14です。
$ oc project openshift $ for resource in eap-cd-image-stream.json eap-cd-amq-persistent-s2i.json eap-cd-amq-s2i.json eap-cd-basic-s2i.json eap-cd-https-s2i.json eap-cd-mongodb-persistent-s2i.json eap-cd-mongodb-s2i.json eap-cd-mysql-persistent-s2i.json eap-cd-mysql-s2i.json eap-cd-postgresql-persistent-s2i.json eap-cd-postgresql-s2i.json eap-cd-sso-s2i.json eap-cd-third-party-db-s2i.json eap-cd-tx-recovery-s2i.json; do oc replace --force -f https://raw.githubusercontent.com/jboss-container-images/jboss-eap-7-openshift-image/eap-cd/templates/${resource}; done
テンプレートによるアプリケーションのデプロイ
テンプレートeap-cd-basic-s2i を使ってセッションレプリケーションテスト用のアプリケーションをデプロイします。
$ oc new-app --template=eap-cd-basic-s2i -p IMAGE_STREAM_NAMESPACE=openshift -p SOURCE_REPOSITORY_URL="https://github.com/n-agetsu/eap-debug" -p SOURCE_REPOSITORY_REF="master" -p CONTEXT_DIR="session-test" -p APPLICATION_NAME="session-test"
テンプレートを実行すると、以下のような処理が行われます。
- https://github.com/n-agetsu/eap-debugをgit cloneして、eap-cd-openshiftイメージにソースコードを転送した上で、eap-cd-openshiftイメージに入っているMavenを利用してwarファイルをビルド。
- ビルドしたwarファイルを、eap-cd-openshiftイメージの /opt/eap/standalone/deployments に含めた、アプリケーションイメージを作成し、OpenShiftの内部レジストリにpush。eap-cd-openshiftのようにEAPのランタイムとMavenのようなビルドツールが入っているコンテナイメージを、OpenShiftではBuilder Imageと呼んでいて、Red Hat Container Catalogでは色々なBuilder Imageが公開されています。一方でBuilder Imageにビルド済みのwarファイルを追加して、Podとして起動するイメージをApplication Imageと呼んでいます。
- アプリケーションイメージのビルドが完了した後、DeploymentConfigの設定に沿ってアプリケーションイメージをPodとして起動。
アプリケーションイメージのPod起動が完了すると、以下のリソースが展開されます。ServiceやRouteなど、アプリケーションの動作に必要なリリースを個別に設定する必要はなく、テンプレートによって展開されます。
$ oc get all NAME READY STATUS RESTARTS AGE pod/session-test-1-build 0/1 Completed 0 31m pod/session-test-1-vldx2 1/1 Running 0 29m NAME DESIRED CURRENT READY AGE replicationcontroller/session-test-1 1 1 1 29m NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/session-test ClusterIP 172.30.133.229 <none> 8080/TCP 31m service/session-test-ping ClusterIP None <none> 8888/TCP 31m NAME REVISION DESIRED CURRENT TRIGGERED BY deploymentconfig.apps.openshift.io/session-test 1 1 1 config,image(session-test:latest) NAME TYPE FROM LATEST buildconfig.build.openshift.io/session-test Source Git@master 1 NAME TYPE FROM STATUS STARTED DURATION build.build.openshift.io/session-test-1 Source Git@5b856c7 Complete 31 minutes ago 1m53s NAME DOCKER REPO TAGS UPDATED imagestream.image.openshift.io/session-test docker-registry.default.svc:5000/cluster-test/session-test latest 29 minutes ago NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD route.route.openshift.io/session-test session-test-cluster-test.cloudapps.example.com session-test <all> edge/Redirect None ||< テンプレートの中身やパラメータの一覧を確認したい場合は、以下のコマンドで確認してください。 >|sh| $ oc get template eap-cd-basic-s2i -o yaml -n openshift
アプリケーションの動作確認
テストアプリケーションでは、アクセスカウンタと共に、アクセスされたホスト名を返しています。eap-cd-basic-s2i ではPodのレプリカ数は1で起動するため、毎回同じホスト名が返されます。
$ curl --insecure -c cookie.txt https://session-test-cluster-test.cloudapps.example.com/session-test/count count: 1, host: session-test-1-vldx2 $ curl --insecure -b cookie.txt https://session-test-cluster-test.cloudapps.example.com/session-test/count count: 2, host: session-test-1-vldx2 $ curl --insecure -b cookie.txt https://session-test-cluster-test.cloudapps.example.com/session-test/count count: 3, host: session-test-1-vldx2
Podのレプリカを2に増やしても、OpenShiftのRouterはデフォルトでソースIPベースのスティッキーセッションが有効化されているため、同じPodにアクセスし続けます。これではセッションレプリケーションが動いているかわからないため、レプリカ数を2に増やした後、元々アクセスしていたPodを削除してみます。
$ oc scale dc session-test --replicas=2 $ oc delete pod session-test-1-vldx2 $ curl --insecure -b cookie.txt https://session-test-cluster-test.cloudapps.example.com/session-test/count count: 4, host: session-test-1-fhvjs
別のホストにアクセスしましたが、HTTPセッションは継続しています。セッションレプリケーションがなければ、Podをdeleteした時点でセッションは失われていたでしょう。
OpenShiftのデフォルトではマルチキャストは飛びません。テンプレートの中ではStatefulSetを使ってホスト名を固定し、クラスタメンバを静的に定義することもしていません。テンプレートでデプロイすると、jgroupsのDNS_PINGの仕組みを使ってクラスタメンバの探索を行なっています。
JGroupsのDNS_PING機能
JGroupsにはk8s向けのクラスタディスカバリの実装として、KUBE_PINGとDNS_PINGの2種類の機能があります。コンテナイメージとしてはKUBE_PINGがデフォルトでstandalone-openshift.xmlに設定されていますが、テンプレートを利用するとコンテナ起動時にデフォルトでDNS_PINGに上書きします。
/opt/eap/standalone/configuration/standalone-openshift.xmlには、以下のようにJGroupsサブシステムの設定にopenshift.DNS_PINGが含まれます。
<subsystem xmlns="urn:jboss:domain:jgroups:6.0"> <channels default="ee"> <channel name="ee" stack="tcp"/> </channels> ... <stack name="tcp"> <transport type="TCP" socket-binding="jgroups-tcp"> <property name="use_ip_addrs">true</property> </transport> <protocol type="openshift.DNS_PING" ></protocol> <protocol type="MERGE3"/>
KUBE_PINGはざっくり言うと、k8sのAPIサーバに同じラベルを持つPodのリストを問い合わせてクラスタメンバを見つけています。OpenShift上でKUBE_PINGを動作させようとすると、 アプリケーションからk8sのREST APIへのアクセスを許可する設定が必要ですが、DNS_PINGではOpenShift側の設定は不要なためシンプルに使えます。
DNS_PINGの仕組み
ここまで長くなりましたが、ここからが今日のタイトルの話です。
DNS_PINGはOpenShift上のDNSサーバのSRVレコードの仕組みを使って、クラスタメンバを見つけています。実際にレコードを内容を見てみると想像が付きやすいので、OpenShift上の任意のノードにSSHログインしてnslookupしてみます。
$ nslookup -type=SRV _tcp.session-test-ping.cluster-test.svc.cluster.local Server: 192.168.122.2 Address: 192.168.122.2#53 _tcp.session-test-ping.cluster-test.svc.cluster.local service = 10 50 8888 fd149e20.session-test-ping.cluster-test.svc.cluster.local. _tcp.session-test-ping.cluster-test.svc.cluster.local service = 10 50 8888 14a2d9.session-test-ping.cluster-test.svc.cluster.local.
ホスト名のようなものが出てくるため、pingしてみます。
$ ping fd149e20.session-test-ping.cluster-test.svc.cluster.local. PING fd149e20.session-test-ping.cluster-test.svc.cluster.local (10.130.0.11) 56(84) bytes of data. 64 bytes from 10.130.0.11 (10.130.0.11): icmp_seq=1 ttl=64 time=1.51 ms
oc get pod -o wide
で確認すると、SRVレコードで取得できたFQDNfd149e20.session-test-ping.cluster-test.svc.cluster.local.
はPodのIPアドレス10.130.0.11
とわかります。
$ oc get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE session-test-1-build 0/1 Completed 0 1h 10.130.0.9 node01.openshift.example.com <none> session-test-1-vldx2 1/1 Running 0 1h 10.130.0.11 node01.openshift.example.com <none> session-test-1-xmqpc 1/1 Running 0 41m 10.130.0.12 node01.openshift.example.com <none>
k8sの機能として、ロードバランシング用のCluster IPを持たない(clusterIP: none) Headless Serviceがデプロイされた場合、Serviceに関連づけられたPodのホスト名とServiceが公開しているポート番号を、k8s内部のDNSのSRVレコードに登録する仕組み*3があります。
OpenShiftのテンプレートでは、EAP-CD(eap-cd-basic-s2i)向けだけに止まらず、EAP64やEAP71のイメージにおいても、HTTPアクセスのためのServiceとは別に、以下のようなクラスタディスカバリ向けのServiceを登録しています。
$ oc get svc session-test-ping -o yaml apiVersion: v1 kind: Service metadata: ... spec: clusterIP: None ports: - name: ping port: 8888 protocol: TCP targetPort: 8888 selector: deploymentConfig: session-test sessionAffinity: None type: ClusterIP
もう一度nslookupの例に戻ります。
$ nslookup -type=SRV _tcp.session-test-ping.cluster-test.svc.cluster.local ... _tcp.session-test-ping.cluster-test.svc.cluster.local service = 10 50 8888 fd149e20.session-test-ping.cluster-test.svc.cluster.local. _tcp.session-test-ping.cluster-test.svc.cluster.local service = 10 50 8888 14a2d9.session-test-ping.cluster-test.svc.cluster.local.
nslookupのクエリ対象である、_tcp.session-test-ping.cluster-test.svc.cluster.local
は、前述のService session-test-ping
の追加によって登録されたDNSレコードです。k8sのServiceを追加すると、今のような命名規則でSRVレコードが追加されます。
<Serviceのprotocol>.<Service名>.<プロジェクト名(namespace)>.svc.cluster.local
SRVレコードは以下のような情報を含んでいます。PriorityとWeightはSRVレコードを読み取るクライアントが、負荷分散の重み付けのために使う情報です。DNS_PINGは負荷分散ではなく単純にクラスタディスカバリにしかSRVを使っていないため、現時点での実装では重み付け情報は使っていません。SRVレコードのポート番号も現時点での実装では使っていません。クラスタ間通信にはデフォルトで7600ポート(TCP)が使われます。
service = 10 50 8888 fd149e20.session-test-ping.cluster-test.svc.cluster.local. = (Priority) (Weight) (ポート番号) (ホスト名)
このようにJGroupsはSRVレコードからクラスタメンバの情報を取得しています。一度メンバの探索ができたら、その後FDとFD_ALLで定期的にヘルスチェックを行い障害検知を行うのは、従来のTCPユニキャストベースのクラスタと同じです。
このDNS_PINGのソースコードは以下にあります。
GitHub - jboss-openshift/openshift-ping: JGroups PING protocols for OpenShift
今回のテンプレートeap-cd-basic-s2iのように、テンプレートでは必要な関連リソースを一緒にデプロイしてくれるため、慣れないと中身で何やっているかわからない不安はありますが、テンプレートを使う方がハマりにくいです。
おそらく初めてEAPをOpenShiftにデプロイするときに、テンプレートなしでJGroups向けのHeadless Serviceのデプロイに思い当たる人はなかなかいないと思います。プロジェクト固有のテンプレートが必要な場合、まずは既存のテンプレートで慣れてから、テンプレート作成に着手した方が近道だと思います。
まとめ
- JBoss EAP CD for OpenShiftは従来のEAPと比較してインクリメンタルなモデルでリリースしている、コンテナイメージ形式リリースのバイナリです。今のところTechnology Previewのため評価検証向けです。
- JBoss EAP CD for OpenShiftの最新版をOpenShiftで利用するためには前述の通りImageStreamとTemplateの更新をします
- マルチキャストが使えない環境のために、DNS_PINGと呼ばれる、Headless ServiceによるSRVレコードの参照により、クラスタを構成しています
- 慣れないうちはテンプレートを使うのがおすすめです
明日は@orimanabuさんです、お楽しみに!
*1:OpenShiftの場合デフォルトから設定変えるとマルチキャストは使える Managing Networking | Cluster Administration | OpenShift Container Platform 3.11
*2:Technology Previewの詳細は Technology Preview Features Support Scope - Red Hat Customer Portal を参照
OpenShift Origin 3.9のインストール
OpenShift Origin 3.9のセットアップ手順メモ。
前提条件は以下の通り。
- OpenShift用DNSサーバ(dnsmasq)、OpenShift Master 1台、 Node 1台
- VirtualBoxのブリッジネットワーク上に構成
- ゲストマシンのOSは全てCentOS7
DNSサーバのセットアップ
・dnsmasqのインストール
# yum install -y dnsmasq
・/etc/hostsのの追記
dnsmasqは/etc/hostsを設定ファイルとして読み込み、名前解決を行う。
192.168.11.10 dns dns.openshift.example.com 192.168.11.11 master master.openshift.example.com 192.168.11.12 node01 node01.openshift.example.com
・/etc/dnsmasq.confの編集
DNSクエリ要求に応答するドメインの設定(local=)、Route用のワイルドカードDNS(address=)、DNSリクエスト待ち受けのネットワークインタフェース(interface=)、DHCPサーバ機能の無効化(no-dhcp-interface=)を設定。
# diff -u /etc/dnsmasq.conf.org /etc/dnsmasq.conf --- /etc/dnsmasq.conf.org 2018-05-26 12:08:03.447080188 +0900 +++ /etc/dnsmasq.conf 2018-05-26 15:02:48.609040878 +0900 @@ -72,11 +72,13 @@ # Add local-only domains here, queries in these domains are answered # from /etc/hosts or DHCP only. #local=/localnet/ +local=/openshift.example.com/ # Add domains which you want to force to an IP address here. # The example below send any host in double-click.net to a local # web-server. #address=/double-click.net/127.0.0.1 +address=/cloudapps.example.com/192.168.11.11 # --address (and --server) work with IPv6 addresses too. #address=/www.thekelleys.org.uk/fe80::20d:60ff:fe36:f83 @@ -103,7 +105,7 @@ # specified interfaces (and the loopback) give the name of the # interface (eg eth0) here. # Repeat the line for more than one interface. -#interface= +interface=enp0s3 # Or you can specify which interface _not_ to listen on #except-interface= # Or which to listen on by address (remember to include 127.0.0.1 if @@ -112,7 +114,7 @@ # If you want dnsmasq to provide only DNS service on an interface, # configure it as shown above, and then use the following line to # disable DHCP and TFTP on it. -#no-dhcp-interface= +no-dhcp-interface=enp0s3 # On systems which support it, dnsmasq binds the wildcard address, # even when it is listening on only some interfaces. It then discards
・DNSサーバの起動
ここではあくまでローカルの実験用環境のため、DNSリクエストのポート53へのアクセス許可はfirewalldを切って対処。
# systemctl stop firewalld # systemctl disable firewalld # systemctl enable dnsmasq # systemctl start dnsmasq
・DNSサーバのネットワーク設定
DNS1はOpenShift用に起動した内部ネットワーク用のdnsmasqサーバ。DNS2はWebアクセス用のDNSサーバ。
# vim /etc/sysconfig/network-scripts/ifcfg-enp0s3 TYPE=Ethernet BOOTPROTO=none NAME=enp0s3 UUID=4590c759-b0cd-4dc2-9f43-779201c6593c DEVICE=enp0s3 ONBOOT=yes IPADDR=192.168.11.10 PREFIX=24 GATEWAY=192.168.1.1 DNS1=127.0.0.1 DNS2=192.168.11.1 # hostnamectl set-hostname dns.openshift.example.com # systemctl restart network
Masterのセットアップ
・Masterサーバのネットワーク設定
# vim /etc/sysconfig/network-scripts/ifcfg-enp0s3 TYPE=Ethernet BOOTPROTO=none NAME=enp0s3 UUID=4590c759-b0cd-4dc2-9f43-779201c6593c DEVICE=enp0s3 ONBOOT=yes IPADDR=192.168.11.11 PREFIX=24 GATEWAY=192.168.11.1 DNS1=192.168.11.10 # hostnamectl set-hostname master.openshift.example.com # systemctl restart network
・必要パッケージのインストール
# yum install -y wget git net-tools bind-utils yum-utils iptables-services bridge-utils bash-completion kexec-tools sos psacct unzip # yum update # systemctl reboot
OpenShift公式ドキュメントのHost Preparation Installing Base Packagesの通りにインストールすると、後述のAnsibleによるインストール時に以下のエラーが発生したため、unzipのインストールを追加している。
TASK [etcd : Unarchive cert tarball] ************************************************************************************** fatal: [master.openshift.example.com]: FAILED! => {"changed": false, "msg": "Failed to find handler for \"~None/.ansible/tmp/ansible-tmp-1527315221.2-254636300880053/source\". Make sure the required command to extract the file is installed. Command \"unzip\" not found. Command \"/usr/bin/gtar\" could not handle archive."}
・Advanced Instllationに必要なパッケージのインストール
# yum install -y https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm # sed -i -e "s/^enabled=1/enabled=0/" /etc/yum.repos.d/epel.repo # yum -y --enablerepo=epel install ansible pyOpenSSL
・OpenShift3.9のダウンロード
# cd ~ # git clone https://github.com/openshift/openshift-ansible # cd openshift-ansible # git checkout release-3.9
・Dockerのインストール
# yum install -y docker-1.13.1 # systemctl enable docker # systemctl start docker
Nodeのセットアップ
・Nodeサーバのネットワーク設定
# vim /etc/sysconfig/network-scripts/ifcfg-enp0s3 TYPE=Ethernet BOOTPROTO=none NAME=enp0s3 UUID=4590c759-b0cd-4dc2-9f43-779201c6593c DEVICE=enp0s3 ONBOOT=yes IPADDR=192.168.11.12 PREFIX=24 GATEWAY=192.168.11.1 DNS1=192.168.11.10 # hostnamectl set-hostname node01.openshift.example.com # systemctl restart network
・必要パッケージのインストール
Masterと同じパッケージをインストール
# yum install -y wget git net-tools bind-utils yum-utils iptables-services bridge-utils bash-completion kexec-tools sos psacct unzip # yum update # systemctl reboot
・Dockerのインストール
# yum install -y docker-1.13.1 # systemctl enable docker # systemctl start docker
OpenShift Originのインストール
Masterサーバ上で以下を実行する。
・SSHログイン時の対話型パスワード入力の抑止
# ssh-keygen # for host in master.openshift.example.com node01.openshift.example.com; do ssh-copy-id -i ~/.ssh/id_rsa.pub $host; done
・/etc/ansible/hostsの設定
[OSEv3:children] masters nodes etcd [OSEv3:vars] ansible_ssh_user=root openshift_deployment_type=origin openshift_master_default_subdomain=cloudapps.example.com openshift_disable_check=memory_availability,disk_availability [masters] master.openshift.example.com [etcd] master.openshift.example.com [nodes] master.openshift.example.com openshift_node_labels="{'region': 'infra', 'zone': 'default'}" node01.openshift.example.com openshift_node_labels="{'region': 'primary', 'zone': 'default'}"
・OpenShiftインストールの実行
# ansible-playbook -i /etc/ansible/hosts ~/openshift-ansible/playbooks/prerequisites.yml # ansible-playbook -i /etc/ansible/hosts ~/openshift-ansible/playbooks/deploy_cluster.yml
インストール後の初期セットアップ
後日追記予定