※当サイトの記事には、広告・プロモーションが含まれます。

Spring Bootのデプロイ、結局どうするのが良いのかよく分からん問題

www.itmedia.co.jp

⇧ 高齢者の車の事故の問題と、車が無いと生活が難しい地域の問題、自動運転が実現したら、解決できそうですかね。

Spring Bootで可能なデプロイの方法の全量が曖昧...

公式のドキュメントによると、

spring.pleiades.io

Spring Boot の柔軟なパッケージオプションは、アプリケーションのデプロイに関して多くの選択肢を提供します。Spring Boot アプリケーションは、さまざまなクラウドプラットフォーム、仮想 / 実マシンにデプロイしたり、Unix システムで完全に実行可能にすることができます。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/deployment.html

⇧ う~む、多くの選択肢を提供します、で終わっても困るわけで、開発者が知りたいのは、その多くの選択肢の全量を把握したいのだよね...

何故、ここで、選択肢の全量を一覧として掲載しないのか甚だ疑問なのだが...

何て言うか、IT系のドキュメントって、異業種だったら、『ちょっと責任者を呼んでもらえますか』ってようなツッコミどころが満載なのどうにかならんのかね...

spring.pleiades.io

実行可能 jar( "fat jar" と呼ばれることもあります)は、コードの実行に必要なすべての jar 依存関係とともに、コンパイルされたクラスを含むアーカイブです。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/getting-started.html

実行可能 jar および Java

Java は、ネストされた jar ファイル(jar 内に含まれる jar ファイル)をロードする標準的な方法を提供しません。自己完結タイプのアプリケーションを配布しようとしている場合、これは問題になる可能性があります。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/getting-started.html

この問題を解決するために、多くの開発者は "uber" jar を使用します。uber jar は、すべてのアプリケーションの依存関係からのすべてのクラスを単一のアーカイブにパッケージ化します。このアプローチの問題は、アプリケーションにどのライブラリが含まれているかを確認しにくくなることです。また、複数の jar で同じファイル名(ただし、異なるコンテンツ)が使用されている場合、問題が発生する可能性があります。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/getting-started.html

⇧ で、Spring Bootのドキュメントの説明だと、jarの種類がいろいろ出てくるのだけど、Spring Bootで言及しているjarの種類の全量が分からないので何とも言えないのですが、一般的なjarについては、

stackoverflow.com

They're just ways of packaging the java apps.

Skinny – Contains ONLY the bits you literally type into your code editor, and NOTHING else.

Thin – Contains all of the above PLUS the app’s direct dependencies of your app (db drivers, utility libraries, etc).

Hollow – The inverse of Thin – Contains only the bits needed to run your app but does NOT contain the app itself. Basically a pre-packaged “app server” to which you can later deploy your app, in the same style as traditional Java EE app servers, but with important differences.

Fat/Uber – Contains the bit you literally write yourself PLUS the direct dependencies of your app PLUS the bits needed to run your app “on its own”.

Source: Article from Dzone

https://stackoverflow.com/questions/54616030/difference-between-jar-fat-jar-executable-jar

⇧ とのことらしいのだけど、この時点で、Spring BootのドキュメントとStackoverflowの内容とでjarについての概念が噛み合ってないような...

Spring Bootのドキュメントがハッキリしないってのが諸悪の根源な気もするが...

そして、関係ないかも知らんが、

docs.spring.io

The core class used to support loading nested jars is org.springframework.boot.loader.jar.JarFile. It lets you load jar content from a standard jar file or from nested child jar data. 

https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html#appendix.executable-jar

⇧ 最新のドキュメントに「org.springframework.boot.loader」パッケージの記載があるのに、

docs.spring.io

⇧ 3.0.0 以降のAPIドキュメントに「org.springframework.boot.loader」パッケージの記載が無くなってるんよね...

ドキュメントで混乱させてくれるの、どうにかして欲しい...

この件に関しては、質問したら回答がもらえて、

spring.io

⇧ Spring Boot が内部的に利用しているということらしい。何故、APIドキュメントから消したのかは謎です。

確かに、

github.com

⇧ build.gradleのタスクとして用意されている「bootJar」で利用してそうなクラスで、

private static final String LAUNCHER = "org.springframework.boot.loader.JarLauncher";

⇧「org.springframework.boot.loader」パッケージを参照してるっぽいし...

ブラックボックスな部分は深まるばかり...

Spring Bootのデプロイ、結局どうするのが良いのかよく分からん問題

Spring Bootのデプロイについてのドキュメントがカオス過ぎて、何とも言えんのだけど、ドキュメントによりますと、

spring.pleiades.io

spring-boot-loader モジュールにより、Spring Boot は実行可能な jar および war ファイルをサポートできます。Maven プラグインまたは Gradle プラグインを使用する場合、実行可能な jar は自動的に生成されるため、通常、それらがどのように機能するかの詳細を知る必要はありません。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/executable-jar.html

別のビルドシステムから実行可能 jar を作成する必要がある場合、または基礎となるテクノロジに興味がある場合は、この付録で背景を説明します。

https://spring.pleiades.io/spring-boot/docs/current/reference/html/executable-jar.html

⇧ とあるので、おそらく、

    • jar
      • 実行可能jar(Spring Bootの組み込みTomcatあり)
      • 実行可能jar(Spring Bootの組み込みTomcatなし)
    • war

のいずれかのファイルを、GradleやMavenなどのビルドツールでビルドして作成して、ビルドしたファイルを配置し起動する感じになるんかね?

このあたりは、実際の開発現場で確認するしかない感じだとは思うのだけど、Spring Bootのドキュメントでデプロイの選択肢の全量を掲載してくれていないせいで、確認しようにも確認の仕方に困るよね...

Dockerコンテナにデプロイする場合は、jarにビルドしてデプロイする感じか

Spring Bootの公式のドキュメントによると、

spring.pleiades.io

⇧ jarにビルドする方法が中心に取り上げられており、そもそも、warについては記載がないことから、Dockerコンテナのような環境にデプロイする際は、jarファイルの形にしてデプロイするのが一般的なんですかね?

Dockerのブログの方でも、

www.docker.com

⇧ jarにビルドしてデプロイする方式を採用してるっぽい。

Gradle(EclipseでのGradle タスク)でjarをビルドし、Dockerコンテナにデプロイしてみる

正直、Spring Boot のデプロイについて、どうするのが一番良いのか分からんのですが、とりあえず、Dockerコンテナにデプロイしてみる。

デプロイの手順としては、

  1. Gradleでビルドしてjarファイル作成
  2. 生成したjarファイルを元に、Dockerイメージ作成
  3. 生成したDockerイメージを元に、Dockerコンテナ作成・起動

という感じになるんかな。

で、デプロイしたものの動作確認してみたら何かしら不具合が見つかって、一旦「切り戻し」したい時があると思います。

「切り戻し」の方針としては、

note.com

christina04.hatenablog.com

system-architect-from-backend.com

⇧ 開発現場によって異なるようですが、Javaの開発でDockerのようなコンテナ技術を利用している環境では、アプリケーションのデプロイで管理すべき対象としては、

  • jarファイル
  • Dockerイメージ

の2つがある模様。データベースとかは、クラウドを使ってる場合は、マネージドデータベースを利用していると思うので、別に管理する感じになるかと。

で、話をアプリケーションのデプロイの「切り戻し」に戻すと、「jarファイル名」と「Dockerイメージ名」の関連が分かりやすいようにしておいた方が良さそう。

Gitでソースコードを管理してる場合は、『「コミット番号」+「日付」』みたいな名前にすれば良いんかな?

AWSの「Amazon Elastic Container Service (Amazon ECS)」を使ってる場合なんかは、

docs.aws.amazon.com

ローリング更新デプロイには、デプロイの失敗をすばやく特定し、失敗したデプロイを最後に動作していたデプロイにロールバックすることの選択を可能にする 2 つのメソッドがあります。

メソッドはどちらか一方でも、両方を同時にでも使用できます。両方のメソッドを同時に使用する場合、いずれかの失敗処理メソッドの失敗の基準が満たされ次第、デプロイは失敗に設定されます。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/deployment-type-ecs.html

⇧ 良しなに「切り戻し」をしてくれるらしい。

仮想マシンにデプロイする場合でも同様の「切り戻し」を良しなにしてくれるサービスが存在するかは不明。

とは言え、「切り戻し」を良しなにしてもらえると、開発時の精神的負担が減るから開発者のモチベーションは上がりますよね、トライ&エラーの試行錯誤に取り組みやすくなりますし。

やはり、開発に専念したい、可能であれば、煩わしい保守・運用に時間を取られたくないって思ってしまうのは、凡人ゆえだからなのだろうか...

強強エンジニアなら、保守・運用しながら片手間で開発ってのも苦にならないのかもしらんけど...

話が脱線しましたが、ローカル環境でDcokerコンテナにjar(「Spring Boot」プロジェクト)をデプロイしてみようと思います。

「Spring Boot」プロジェクトは、

ts0818.hatenablog.com

⇧ 上記の記事の時のものを利用します。

まずは、jarファイルをビルドですが、Eclipse以外の「統合開発環境IDE:Integrated Development Environment)」がGUI上のGradleビルドに対応しているかは分かりませんが、Eclipseの場合、

spring.io

⇧ build.gradleに「Gradleタスク」を追加することでGUI上でjarファイルが作成できるようです。

何やら、

bottoms-programming.com

⇧ Gradleのタスクで「bootJar」と「jar」で生成されるjarは異なるらしく、Spring Boot の場合は、「bootJar」でjarファイルを作成しとくのが良さ気らしい。

Gitでソースコードを管理している場合、

stackoverflow.com

⇧ commitの値を取得できるようです。

日付も、

qiita.com

⇧ 取得できそうです。

そして、ネットの情報を見た感じ、「bootJar」だと、

irof.hateblo.jp

⇧ 上記サイト様の説明にある、「spring.profiles.active」での切り替えが、build.gradleで不可能っぽい。

qiita.com

⇧ 上記サイト様によりますと、リネームして対応してらっしゃいました。

今回は、読み込んで欲しくないファイルを除外する方法を取りました。

そして、

think-memo.com

⇧ mainクラスを明示的に指定しておいた方が良い模様。

最終的に、jarをビルドする際の設定は、以下のようになりました。Spring Bootが参照するプロパティファイルを、

  • 開発環境用
    /geometry/src/main/resources/application.properties
  • ステージング環境用
    /geometry/src/main/resources/application-stg.properties
  • 本番環境用
    /geometry/src/main/resources/application-prod.properties

のような感じで用意しておきます。「ステージング環境用」「本番環境用」は空ファイルになってます。

■/geometry/gradle.properties

## gradle.properties
# 共通
propertyFile.basePath=src/main/resources
archive.version=0.1.0
output.jar.base.dir=build/libs

# 開発環境
propertyFile.dev=application.properties
output.jar.dev=dev

# ステージング環境
propertyFile.stg=application-stg.properties
output.jar.stg=stg

# 本番環境
propertyFile.prod=application-prod.properties
output.jar.prod=prod    

■/geometry/build.gradle

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.2'
	id 'io.spring.dependency-management' version '1.1.2'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	// https://mvnrepository.com/artifact/net.postgis/postgis-jdbc
    implementation group: 'net.postgis', name: 'postgis-jdbc', version: '2021.1.0'
	
	annotationProcessor 'org.projectlombok:lombok'
	// https://mvnrepository.com/artifact/org.hibernate.orm/hibernate-spatial
    implementation group: 'org.hibernate.orm', name: 'hibernate-spatial', version: '6.3.0.CR1'
    // https://mvnrepository.com/artifact/com.graphhopper.external/jackson-datatype-jts
    implementation group: 'com.graphhopper.external', name: 'jackson-datatype-jts', version: '2.14'
    // https://mvnrepository.com/artifact/org.locationtech.jts/jts-core
    //simplementation group: 'org.locationtech.jts', name: 'jts-core', version: '1.19.0'
    implementation 'org.locationtech.jts.io:jts-io-common:1.18.2'
    // https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-annotations
    implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: '2.13.0'
    
 	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

//ext {
//    env = ''
//}
tasks.named('test') {
	useJUnitPlatform()
}

//jar.archiveBaseName = getArchiveBaseName() + "_" + getTimestamp()
//jar.archiveVersion =  project.properties['archive.version']

task cleanUp(type: Delete) {
  doFirst {
    delete project.properties['output.jar.base.dir'] + "/"
  }  
}

// 開発環境のjarファイルを作成するGradleタスク
task buildJarForDev(type: Jar, dependsOn: 'setInfoForBuildJar') {
    group 'build' // 既存の「Gradle タスク」タブに表示されるディレクトリ
    description '開発環境のjarファイルを作成します。' // タスクの説明
    
    dependsOn 'cleanUp'
//    from (project.properties['propertyFile.basePath']) {
//      include project.properties['propertyFile.dev']
//    }
    //bootJar.systemProperties=project.properties['propertyFile.dev']
    //bootJar.systemProperty 'spring.profiles.active', 'dev'
    doLast {
      setEnvironment(project.properties['output.jar.dev'])
    }
    finalizedBy bootJar

}

// ステージング環境のjarファイルを作成するGradleタスク
task buildJarForStg(type: Jar, dependsOn: 'setInfoForBuildJar') {
    group 'build' // 既存の「Gradle タスク」タブに表示されるディレクトリ
    description 'ステージング環境のjarファイルを作成します。' // タスクの説明

    dependsOn 'cleanUp'
//    from (project.properties['propertyFile.basePath']) {
//      include project.properties['propertyFile.stg']
//    }
    //bootJar.systemProperties=project.properties['propertyFile.stg']
    //bootJar.systemProperty 'spring.profiles.active', 'stg'
    doLast {
      setEnvironment(project.properties['output.jar.stg'])
    }
    finalizedBy bootJar

}

// 本番環境のjarファイルを作成するGradleタスク
task buildJarForProd(type: Jar, dependsOn: 'setInfoForBuildJar') {
    group 'build' // 既存の「Gradle タスク」タブに表示されるディレクトリ
    description '本番環境のjarファイルを作成します。' // タスクの説明
    
    dependsOn 'cleanUp'
//    from (project.properties['propertyFile.basePath']) {
//      include project.properties['propertyFile.prod']
//    }
    //bootJar.systemProperties=project.properties['propertyFile.prod']
    //bootJar.systemProperty 'spring.profiles.active', 'prod'
    doLast {
      setEnvironment(project.properties['output.jar.prod'])
    }
    finalizedBy bootJar

}

// 環境ごとにプロパティファイルを変更する
def setEnvironment(env) {
  def baseOutputDir=project.properties['output.jar.base.dir'] + "/"
  print 'env: ' + env
  switch (env) {
    case 'dev': // 開発環境
      bootJar.exclude(
        project.properties['propertyFile.stg']
        ,project.properties['propertyFile.prod']
      )
      baseOutputDir += project.properties['output.jar.dev']
      break
    case 'stg': // ステージング環境
      bootJar.exclude(
        project.properties['propertyFile.prod']
      )
      baseOutputDir += project.properties['output.jar.stg']
      break
    case 'prod': // 本番環境
      bootJar.exclude(
        project.properties['propertyFile.stg']
      )
      baseOutputDir += project.properties['output.jar.prod']
      break
    default:
      break
  }
  bootJar.destinationDirectory=file(baseOutputDir)
}


task setInfoForBuildJar() {
    doFirst {
        bootJar.archiveBaseName = getArchiveBaseName() + "_" + getTimestamp()
        bootJar.archiveVersion =  project.properties['archive.version']
        bootJar.mainClass = 'com.example.demo.GeometryApplication'
    }
}

def getArchiveBaseName() {
  // Gitのコミットのハッシュ値にしたいところだが、今回は固定値
  return 'gs-gradle';
}

// 現在時刻を取得する
def getTimestamp() {
    // 年月日_時分秒_ミリ秒
    return new Date().format('yyyyMMdd_HHmmss_SSS')
}
    

⇧ 何か、意外に環境毎にプロパティファイルを変えてSpring Boot のjarをビルドするって情報が無くて、滅茶苦茶に時間がかかったんだが...

正直、正しい設定が分からん...

何て言うか、Spring Boot の公式のドキュメントで頑張って欲しい気はするんだけどね...

Eclipseの「Gradle タスク」タブを確認すると、

⇧ build.gradleで定義したタスクが、Gradleのタスクとして追加されているので、右クリックし「Gradle タスクの実行」を選択。

無事、Gradleタスクの実行が完了したようです。

指定したフォルダに、

⇧ jarファイルが作成されました。

続いて、作成したjarファイルを元に、Dockerイメージの作成に進みます。

Dockerfileなどについては、

ts0818.hatenablog.com

⇧ 上記の記事の時のものを流用していきます。

ビルドしたjarファイルを、Dockerfileと同じフォルダに配置しておきます。

Dockerfileを編集していきますが、

spring.pleiades.io

従来の VM デプロイと同様に、プロセスは root 権限で実行しないでください。代わりに、イメージには、アプリケーションを実行する root 以外のユーザーが含まれている必要があります。

https://spring.pleiades.io/guides/topicals/spring-boot-docker/

⇧ ということらしく、ユーザーを追加する必要があるらしい。

qiita.com

⇧ パスワードは、ハッシュ化したものを指定する必要があるらしい。

■C:\Users\Toshinobu\Desktop\soft_work\rancher-desktop\DockerFile

#FROM redhat/ubi8-init:latest
#FROM registry.access.redhat.com/ubi8-init:latest
#FROM registry.access.redhat.com/ubi9-init:latest
FROM eclipse-temurin:17-jdk-ubi9-minimal@sha256:2d5b228568afdccb76f87f969ec915826fd81f6c4502ff482694052cfbb84f43

#RUN yum update -y --disableplugin=subscription-manager \
# && yum clean all \
# && yum install -y temurin-17-jdk \
# && yum install -y curl \
# && yum install -y zip \
# && yum install -y git
 
RUN microdnf update -y --disableplugin=subscription-manager \
 && microdnf install dnf -y \
 && dnf update -y \
 && dnf clean all -y \
 && dnf install -y zip \
 && dnf install -y git \
# && dnf install -y find
 && dnf install -y findutils \
 && dnf install -y sudo \
# && dnf install -y iproute2 \
 && dnf install -y iproute \
 && dnf install -y bind-utils \
# && dnf install -y traceroute
# && dnf install -y vim-enhanced
# && dnf install vim-minimal
 && dnf install -y vim

RUN curl -s "https://get.sdkman.io" | bash
RUN echo ". $HOME/.sdkman/bin/sdkman-init.sh; sdk install gradle" | bash

RUN groupadd dev_web && useradd dev_web -g dev_web -p $(perl -e 'print crypt("password", "\$6\$salt03")') 
RUN usermod -aG wheel dev_web
USER dev_web

WORKDIR /usr/project

VOLUME /tmp

ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

#RUN sudo groupadd docker
#RUN sudo adduser $USER docker
#RUN sudo chown root:docker /var/run/docker.sock
#RUN sudo chmod g+w /var/run/docker.sock
#RUN newgrp docker

#RUN sudo chown root:docker /var/run/docker.sock
#RUN sudo chmod g+w /var/run/docker.sock

⇧ とりあえず、Dockerfileを編集。

「Rancher Desktop」を起動。Dockerが使えるのであれば、何でも良いかと。

Windows特有の問題なのか分からんが、

⇧ エラーが出るんだが、Dockerの起動自体はできてるらしい。

⇧ 警告が出てるのだが、

forums.docker.com

⇧「WSL 2(Windows SubSystem for Linux 2)」側の問題らしい...相変わらず、Windowsさん、次から次へと問題を引き起こしてくれる...

stackoverflow.com

⇧「blkio」という機能を使わないのであれば、無視しても問題ないらしいので、無視することにしました。

Dockerfileのある場所に移動して、Dockerイメージを作成で。

docker build --build-arg JAR_FILE=[利用したいjarファイルのパス] -t [イメージID]:[タグ] .

実際に、実行したコマンドは以下。

cd C:\Users\Toshinobu\Desktop\soft_work\rancher-desktop
docker build --build-arg JAR_FILE="gs-gradle_20230807_224948_316-0.1.0.jar" -t rhel/spring-boot:version3.1.2 .

⇧ 結局、「Dockerイメージ名」と「jarファイル名」関連付けれてないけど、このあたりは、開発現場に合わせる感じで。

そしたらば、Dockerイメージを元に、Dockerコンテナ作成・起動。

コンテナで利用されてるOSによって変わってくるとは思うだけど、

access.redhat.com

www.tohoho-web.com

⇧ 上記サイト様を参考に、「privileged」オプションを使って起動することにしました。

Dockerコンテナの「/etc/hosts」「/etc/resolv.conf」に追加する形で、Dockerコンテナを起動する必要があるっぽいので、「WSL 2(Windows SubSystem for Linux 2)」のUbuntuIPアドレスを確認。

wsl -d [WSL上のディストリビューション名] hostname -i    

確認したIPアドレスと、Avahiで設定しているホスト名を追加するように、docker runコマンドを実施する。

docker run -it --name spring-boot-app --add-host=ubuntuhost:172.24.91.141 --dns=8.8.8.8 --privileged -p 8080:8080 rhel/spring-boot:version3.1.2   

ブラウザからアクセスすると、

Dockerコンテナから「WSL 2(Windows SubSystem for Linux 2)」のUbuntuへインストールしていたPostgreSQLへ接続できて、データが取得できているようです。

う~ん、毎回、IPアドレスを確認せにゃならんのが辛いですな...

「WSL 2(Windows SubSystem for Linux 2)」を使っている以上、致し方ないということですかね...

設定周り、調べるのに時間がかかるのが辛い...

本当なら他のことに時間を使いたいんだけどな...

毎度モヤモヤ感が半端ない...

今回はこのへんで。