CLOVER🍀

That was when it all began.

OKD/Minishift上で、マルチモジュール構成のMavenプロジェクトをデプロイする

これは、なにをしたくて書いたもの?

  • JavaアプリケーションをMavenのマルチモジュール構成で作った時に、「どうやってデプロイするんだっけ?」と思ったので
  • S2Iビルドする時に、なにか設定とかがいるのではと思って調べてみようと

というわけで、OKD/Minishift上にマルチモジュール構成のMavenプロジェクトをデプロイしてみます。

お題

OKD/Minishift上に、ごくごく簡単な2つのMavenプロジェクトをデプロイしてみます。

2つというのは、

という感じで。

ほぼ答えが書いていますが、参考にしたのはこちらのエントリ。

Maven Multi-Module Projects and OpenShift – Red Hat OpenShift Blog

環境

今回の環境は、こちら。

$ minishift version
minishift v1.24.0+8a904d0


$ oc version
oc v3.10.0+dd10d17
kubernetes v1.10.0+b81c8f8
features: Basic-Auth GSSAPI Kerberos SPNEGO

Server https://192.168.42.24:8443
openshift v3.10.0+e3465d0-44
kubernetes v1.10.0+b81c8f8

マルチモジュール構成のWARなアプリケーションをデプロイする

それでは、最初はWARファイルから。

こんな感じのMavenプロジェクトを用意。

$ find pom.xml library web -type f
pom.xml
library/pom.xml
library/src/main/resources/META-INF/beans.xml
library/src/main/java/org/littlewings/openshift/MessageService.java
web/web.iml
web/src/main/java/org/littlewings/openshift/JaxrsActivator.java
web/src/main/java/org/littlewings/openshift/MessageResource.java

「web」がWARを構成するプロジェクトで、JAX-RSに関連するクラスを置いています。「library」は「web」から
参照されるプロジェクトで、CDI管理Beanを置いています。

内容は、こんな感じ。

library

pom.xml(の一部)
※依存関係がちょっと雑です

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

CDI管理Bean。
library/src/main/java/org/littlewings/openshift/MessageService.java

package org.littlewings.openshift;

import javax.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class MessageService {
    public String get() {
        return "Hello World!!";
    }
}

CDI有効化のための、beans.xml
library/src/main/resources/META-INF/beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="annotated">
</beans>

web

WARファイル側。

pom.xml(の抜粋)。

    <artifactId>web</artifactId>
    <packaging>war</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </properties>

    <dependencies>
        <dependency>
            <groupId>xxx.yyy</groupId>
            <artifactId>library</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-web-api</artifactId>
            <version>7.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>ROOT</finalName>
    </build>

JAX-RS関連のクラス。
リソースクラス。
web/src/main/java/org/littlewings/openshift/MessageResource.java

package org.littlewings.openshift;

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@ApplicationScoped
@Path("message")
public class MessageResource {
    @Inject
    MessageService messageService;

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String get() {
        return messageService.get();
    }
}

JAX-RSの有効化。
web/src/main/java/org/littlewings/openshift/JaxrsActivator.java

package org.littlewings.openshift;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
}
トップレベルのプロジェクト

トップレベルのプロジェクトは、pom.xmlにモジュールのリストがあるだけです。
pom.xml(の抜粋)。

    <packaging>pom</packaging>

    <modules>
        <module>library</module>
        <module>web</module>
    </modules>

このプロジェクトを「http://〜/maven-multi-project-war.git」という感じでGitリポジトリに登録しておきます。

デプロイする

では、このWebアプリケーションをOKDにデプロイしてみます。

このようなマルチモジュールな構成のMavenプロジェクトをデプロイするには、環境変数「ARTIFACT_DIR」をビルド時に
指定します。

ビルド時に指定するので、いきなり「oc new-app」ではなく、「oc new-build」で始めてみます。

$ oc new-build openshift/wildfly:12.0~http://〜/maven-multi-project-war.git -e ARTIFACT_DIR=web/target

ImageStreamとしてWildfFlyを指定し、環境変数「ARTIFACT_DIR」にはwebモジュールのtargetディレクトリ、
つまり最終的に使われるアーティファクトが生成されるディレクトリを指定します。

環境変数「ARTIFACT_DIR」で適切なディレクトリを指定しなかった場合は、この後にDeploymentConfigを
作成してデプロイした時に、WildFlyになにもデプロイされません…。

ビルドが終わったら、「oc new-app」を実行してデプロイして、Routeもexposeします。

$ oc new-app maven-multi-project-war
$ oc expose svc/maven-multi-project-war

確認。

$ curl maven-multi-project-war-myproject.xxx.xxx.xx.xx.nip.io/message
Hello World!!

OKです。

ちなみに、ビルドするモジュールを絞りたい場合は、環境変数MAVEN_ARGS_APPEND」を併用します。

$ oc new-build openshift/wildfly:12.0~http://〜/maven-multi-project-war.git -e ARTIFACT_DIR=web/target -e MAVEN_ARGS_APPEND='-pl web -am'

これで、Maven実行時には以下のようなコマンドになります。

$ mvn package -Popenshift -DskipTests -B -s /opt/app-root/src/.m2/settings.xml -pl web -am

ビルドしたいモジュールと、関連するモジュールがビルドされる感じになりますね。

YAMLで書くと

今回のBuildConfigをYAMLで書いた場合の、該当の箇所を抜粋すると、こうですね。

apiVersion: v1
kind: BuildConfig

## 省略...

spec:

## 省略...

  strategy:
    sourceStrategy:
      env:
      - name: ARTIFACT_DIR
        value: web/target
      - name: MAVEN_ARGS_APPEND
        value: -pl web -am

## 省略...

という感じになります。

マルチモジュール構成のUber JARなアプリケーションをデプロイする

続いて、マルチモジュール構成なUber JARなプロジェクトをデプロイします。

こちらは、こんな構成。

$ find pom.xml library launcher -type f
pom.xml
library/pom.xml
library/src/main/java/org/littlewings/openshift/MessageService.java
launcher/pom.xml
launcher/src/main/java/org/littlewings/openshift/App.java

あらシンプル。内容は、Spring BootアプリケーションでUber JARになる方が「launcher」モジュールです。

OpenJDKのImageStreamを入れる

中身に入る前に、OKDにOpenJDKのImageStreamを突っ込んでおきます。

$ oc create -f https://raw.githubusercontent.com/jboss-openshift/application-templates/ose-v1.4.15/openjdk/openjdk18-image-stream.json -n openshift --as system:admin

Uber JARの実行には、こちらを使用します。

Minishiftの、「admin-user」addonは有効にしています。

$ minishift addon list
- admin-user             : enabled  P(0)
- anyuid             : disabled P(0)
- che                : disabled P(0)
- htpasswd-identity-provider     : disabled P(0)
- registry-route         : disabled P(0)
- xpaas              : disabled P(0)

では、中身の方へ。

library

pom.xml(の一部)。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

いやぁ、こちらも依存関係が雑です…。

Serviceクラス。
library/src/main/java/org/littlewings/openshift/MessageService.java

package org.littlewings.openshift;

import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;

@Service
public class MessageService {
    public Mono<String> get() {
        return Mono.just("Hello World!!");
    }
}
launcher

lancher側。
pom.xml(の一部)。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.0.5.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>xxx.yyy</groupId>
            <artifactId>library</artifactId>
            <version>${project.version}</version>
        </dependency>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.0.5.RELEASE</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

起動クラス兼RestController。
launcher/src/main/java/org/littlewings/openshift/App.java

package org.littlewings.openshift;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@SpringBootApplication
@RestController
public class App {
    MessageService messageService;

    public App(MessageService messageService) {
        this.messageService = messageService;
    }

    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }

    @GetMapping("message")
    public Mono<String> message() {
        return messageService.get();
    }
}
トップレベルのプロジェクト

トップレベルのプロジェクトは、pom.xmlにモジュールのリストがあるだけです。
pom.xml(の一部)。

    <packaging>pom</packaging>

    <modules>
        <module>library</module>
        <module>launcher</module>
    </modules>

WARプロジェクトの時と、同じようなものですね。

このプロジェクトを「http://〜/maven-multi-project-jar.git」という感じでGitリポジトリに登録しておきます。

デプロイする

では、このUber JARなアプリケーションをOKDにデプロイしてみます。

といっても、ここから先の指定は、先程のWebアプリケーションの時と似たようなものです。

「oc new-build」で、環境変数「ARTIFACT_DIR」を「launcher/target」ディレクトリに指定します。

$ oc new-build openshift/redhat-openjdk18-openshift:1.4~http://〜/maven-multi-project-jar.git -e ARTIFACT_DIR=launcher/target

ビルドが終わったら、DeploymentConfigとRouteの作成。

$ oc new-app maven-multi-project-jar
$ oc expose svc/maven-multi-project-jar

確認。

$ curl maven-multi-project-jar-myproject.xxx.xxx.xx.xx.nip.io/message
Hello World!!

OKですね。

もちろん、「oc new-build」時に環境変数MAVEN_ARGS_APPEND」を指定できるのも同じです。

$ oc new-build openshift/redhat-openjdk18-openshift:1.4~http://〜/maven-multi-project-jar.git -e ARTIFACT_DIR=launcher/target -e MAVEN_ARGS_APPEND='-pl launcher -am'

実行されるMavenのコマンドは、このようになりました。

$ mvn -Dmaven.repo.local=/tmp/artifacts/m2 -s /tmp/artifacts/configuration/settings.xml -e -Popenshift -DskipTests -Dcom.redhat.xpaas.repo.redhatga -Dfabric8.skip=true package --batch-mode -Djava.net.preferIPv4Stack=true -pl launcher -am

ちなみに、このようなマルチモジュール構成で環境変数「ARTIFACT_DIR」を指定しなかった場合は、
デプロイ対象が見つからずにエラーになります。

Copying Maven artifacts from /tmp/src/target to /deployments ...
Running: cp *.jar /deployments
/usr/local/s2i/assemble: line 71: cd: /tmp/src/target: No such file or directory
cp: cannot stat '*.jar': No such file or directory
Aborting due to error code 1 for copying artifacts from /tmp/src/target to /deployments
error: build error: non-zero (13) exit code from registry.access.redhat.com/redhat-openjdk-18/openjdk18-openshift@sha256:dc84fed0f6f40975a2277c126438c8aa15c70eeac75981dbaa4b6b853eff61a6

ここは、WARの時とは異なりますね。といっても、WARの時はデプロイが空振りするので、結局それでは
ダメなのですけどね。

YAMLで書くと

今回のBuildConfigをYAMLで書いた場合の、該当の箇所を抜粋すると、こうですね。

apiVersion: v1
kind: BuildConfig
metadata:

## 省略...

spec:

## 省略...

  strategy:
    sourceStrategy:
      env:
      - name: ARTIFACT_DIR
        value: launcher/target
      - name: MAVEN_ARGS_APPEND
        value: -pl launcher -am

## 省略...

オマケ

ビルドのこういった環境変数を扱っている箇所を、ちょっと探しておきました。

WildFly
ARTIFACT_DIR
https://github.com/openshift-s2i/s2i-wildfly/blob/720a8d33b6af1cdc39d28333d44df7adb6bcda9b/12.0/s2i/bin/assemble#L220

MAVEN_ARGS_APPEND(とMAVEN_ARGS)
https://github.com/openshift-s2i/s2i-wildfly/blob/720a8d33b6af1cdc39d28333d44df7adb6bcda9b/12.0/s2i/bin/assemble#L255-L277

S2Iのスクリプトを確認。

$ oc get istag/wildfly:12.0 -n openshift -o yaml

YAMLを抜粋。

apiVersion: image.openshift.io/v1
generation: 2
image:
  dockerImageLayers:

## 省略...

  dockerImageMetadata:

## 省略...

    Config:
      Cmd:
      - /bin/sh
      - -c
      - $STI_SCRIPTS_PATH/usage
      Entrypoint:
      - container-entrypoint

container-entrypointを見よ、と。

「oc rsh」か「oc debug」で確認。

$ oc rsh dc/maven-multi-project-war
## または
$ oc debug dc/maven-multi-project-war

で、「container-entrypointo」は、と。

sh-4.2$ which container-entrypoint
/usr/bin/container-entrypoint

sh-4.2$ cat $(which container-entrypoint)
#!/bin/bash
exec "$@"

うん、わからん…。

Dockerfileを見てみます。
https://github.com/openshift-s2i/s2i-wildfly/blob/master/12.0/Dockerfile

STI_SCRIPTS_PATH」を見ればよいみたいです。

# Copy the S2I scripts from the specific language image to $STI_SCRIPTS_PATH
COPY ./s2i/bin/ $STI_SCRIPTS_PATH

そういえば、さっきのYAMLにも書いてありましたね。

    Config:
      Cmd:
      - /bin/sh
      - -c
      - $STI_SCRIPTS_PATH/usage
      Entrypoint:
      - container-entrypoint

「/usr/libexec/s2i」らしいです。

sh-4.2$ env | grep STI_SCRIPTS_PATH
STI_SCRIPTS_PATH=/usr/libexec/s2i

では、assembleを確認。

sh-4.2$ view $STI_SCRIPTS_PATH/assemble

MAVEN_ARGS_APPEND」や

  # Append user provided args
  if [ -n "$MAVEN_ARGS_APPEND" ]; then
    export MAVEN_ARGS="$MAVEN_ARGS $MAVEN_ARGS_APPEND"
  fi

「ARTIFACT_DIR」を見てみたり。

# the subdirectory within LOCAL_SOURCE_DIR from where we should copy build
# artifacts (*.war, *.jar)
ARTIFACT_DIR=${ARTIFACT_DIR:-target}

OpenJDK
S2Iのスクリプトを確認。

$ oc get istag/redhat-openjdk18-openshift:1.4 -o yaml

YAMLの抜粋。

apiVersion: image.openshift.io/v1
generation: 2
image:
  dockerImageLayers:

## 省略...

  dockerImageManifestMediaType: application/vnd.docker.distribution.manifest.v2+json
  dockerImageMetadata:
    Architecture: amd64
    Config:
      Cmd:
      - /usr/local/s2i/run
      Env:
      - PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/s2i

「oc rsh」、もしくは「oc debug」でコンテナ内に入って、確認。

$ oc rsh dc/maven-multi-project-jar
## もしくは
$ oc debug dc/maven-multi-project-jar

$ sh-4.2$ view /usr/local/s2i/assemble

スクリプトの一部を見てみます。

sh-4.2$ view /usr/local/s2i/assemble

こんな感じ。

function build_maven() {
  # Where artifacts are created during build
  local build_dir=$1

  # Where to put the artifacts
  local app_dir=$2

  local jvm_option_file=/opt/run-java/java-default-options
  if [ -z "${MAVEN_OPTS}" -a -x "$jvm_option_file" ] ; then
    export MAVEN_OPTS="$($jvm_option_file)"
    echo "Setting MAVEN_OPTS to ${MAVEN_OPTS}"
  fi
  # Default args: no tests, if a module is specified, only build this module
  local maven_args=${MAVEN_ARGS:--e -Popenshift -DskipTests -Dcom.redhat.xpaas.repo.redhatga -Dfabric8.skip=true package}

  # Use batch mode (CLOUD-579)
  echo "Found pom.xml ... "
  local mvn_cmd="${maven_env_args} ${maven_args} --batch-mode -Djava.net.preferIPv4Stack=true ${MAVEN_ARGS_APPEND}"
  echo "Running 'mvn ${mvn_cmd}'"