本ハンズオンで、次の図のような簡易家計簿のAPIサーバーをSpring WebFlux.fnを使って実装します。 あえてSpring BootもDependency Injectionも使わないシンプルなWebアプリとして実装します。
ハンズオンコンテンツ
- はじめに
- 簡易家計簿Moneygerプロジェクトの作成
- YAVIによるValidationの実装
- R2DBCによるデータベースアクセス 👈
- Web UIの追加
- 例外ハンドリングの改善
- 収入APIの実装
- Spring Bootアプリに変換
- GraalVMのSubstrateVMでNative Imageにコンパイル
目次
R2DBCによるデータベースアクセス
次はR2DBCを用いてExpenditureBuilderRepository
のデータベース実装を作成します。
pom.xml
に次のdependency
を追加してください。
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-postgresql</artifactId>
</dependency>
R2dbcExpenditureRepository
を作成して次の内容を記述してください。
TODO部分を実装してください。動作を確認するためのテストコードは以下に続きます。TODOを実装する前にテストを実行してくだい。
package com.example.expenditure;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.data.r2dbc.query.Criteria.where;
public class R2dbcExpenditureRepository implements ExpenditureRepository {
private final DatabaseClient databaseClient;
private final TransactionalOperator tx;
public R2dbcExpenditureRepository(DatabaseClient databaseClient, TransactionalOperator tx) {
this.databaseClient = databaseClient;
this.tx = tx;
}
@Override
public Flux<Expenditure> findAll() {
return this.databaseClient.select().from(Expenditure.class)
.as(Expenditure.class)
.all();
}
@Override
public Mono<Expenditure> findById(Integer expenditureId) {
// TODO
// "expenditure_id"が引数のexpenditureIdに一致する1件のExpenditureを返す
return Mono.empty();
}
@Override
public Mono<Expenditure> save(Expenditure expenditure) {
return this.databaseClient.insert().into(Expenditure.class)
.using(expenditure)
.fetch()
.one()
.map(map -> new ExpenditureBuilder(expenditure)
.withExpenditureId((Integer) map.get("expenditure_id"))
.build())
.as(this.tx::transactional);
}
@Override
public Mono<Void> deleteById(Integer expenditureId) {
return this.databaseClient.delete().from(Expenditure.class)
.matching(where("expenditure_id").is(expenditureId))
.then()
.as(this.tx::transactional);
}
}
App.java
に次のメソッドを追加してください。
static ConnectionFactory connectionFactory() {
// postgresql://username:password@hostname:5432/dbname
String databaseUrl = Optional.ofNullable(System.getenv("DATABASE_URL")).orElse("h2:file:///./target/demo?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
return ConnectionFactories.get("r2dbc:" + databaseUrl);
}
public static Mono<Void> initializeDatabase(String name, DatabaseClient databaseClient) {
if ("H2".equals(name)) {
return databaseClient.execute("CREATE TABLE IF NOT EXISTS expenditure (expenditure_id INT PRIMARY KEY AUTO_INCREMENT, expenditure_name VARCHAR(255), unit_price INT NOT NULL, quantity INT NOT NULL, expenditure_date DATE NOT NULL)")
.then();
} else if ("PostgreSQL".equals(name)) {
return databaseClient.execute("CREATE TABLE IF NOT EXISTS expenditure (expenditure_id SERIAL PRIMARY KEY, expenditure_name VARCHAR(255), unit_price INT NOT NULL, quantity INT NOT NULL, expenditure_date DATE NOT NULL)")
.then();
}
return Mono.error(new IllegalStateException(name + " is not supported."));
}
また、routes
メソッドも次のように変更してください。
static RouterFunction<ServerResponse> routes() {
final ConnectionFactory connectionFactory = connectionFactory();
final DatabaseClient databaseClient = DatabaseClient.builder()
.connectionFactory(connectionFactory)
.build();
final TransactionalOperator transactionalOperator = TransactionalOperator.create(new R2dbcTransactionManager(connectionFactory));
initializeDatabase(connectionFactory.getMetadata().getName(), databaseClient).subscribe();
return new ExpenditureHandler(new R2dbcExpenditureRepository(databaseClient, transactionalOperator)).routes();
}
src/test/java/com/example/expenditure
にR2dbcExpenditureRepositoryTest
を作成して、次のテストコードを記述してください。
package com.example.expenditure;
import com.example.App;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.r2dbc.connectionfactory.R2dbcTransactionManager;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class R2dbcExpenditureRepositoryTest {
R2dbcExpenditureRepository expenditureRepository;
DatabaseClient databaseClient;
TransactionalOperator transactionalOperator;
private List<Expenditure> fixtures = Arrays.asList(
new ExpenditureBuilder()
.withExpenditureName("本")
.withUnitPrice(2000)
.withQuantity(1)
.withExpenditureDate(LocalDate.of(2019, 4, 1))
.build(),
new ExpenditureBuilder()
.withExpenditureName("コーヒー")
.withUnitPrice(300)
.withQuantity(2)
.withExpenditureDate(LocalDate.of(2019, 4, 2))
.build());
@BeforeAll
void init() {
final ConnectionFactory connectionFactory = ConnectionFactories.get("r2dbc:h2:mem:///test?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE");
this.databaseClient = DatabaseClient.builder()
.connectionFactory(connectionFactory)
.build();
this.transactionalOperator = TransactionalOperator.create(new R2dbcTransactionManager(connectionFactory));
this.expenditureRepository = new R2dbcExpenditureRepository(this.databaseClient, transactionalOperator);
App.initializeDatabase("H2", this.databaseClient).block();
}
@BeforeEach
void each() throws Exception {
this.databaseClient.execute("TRUNCATE TABLE expenditure")
.then()
.thenMany(Flux.fromIterable(this.fixtures)
.flatMap(expenditure -> this.databaseClient.insert()
.into(Expenditure.class)
.using(expenditure)
.then())
.as(transactionalOperator::transactional))
.blockLast();
}
@Test
void findAll() {
StepVerifier.create(this.expenditureRepository.findAll())
.consumeNextWith(expenditure -> {
assertThat(expenditure.getExpenditureId()).isNotNull();
assertThat(expenditure.getExpenditureName()).isEqualTo("本");
assertThat(expenditure.getUnitPrice()).isEqualTo(2000);
assertThat(expenditure.getQuantity()).isEqualTo(1);
assertThat(expenditure.getExpenditureDate()).isEqualTo(LocalDate.of(2019, 4, 1));
})
.consumeNextWith(expenditure -> {
assertThat(expenditure.getExpenditureId()).isNotNull();
assertThat(expenditure.getExpenditureName()).isEqualTo("コーヒー");
assertThat(expenditure.getUnitPrice()).isEqualTo(300);
assertThat(expenditure.getQuantity()).isEqualTo(2);
assertThat(expenditure.getExpenditureDate()).isEqualTo(LocalDate.of(2019, 4, 2));
})
.verifyComplete();
}
@Test
void findById() {
Integer expenditureId = this.databaseClient.execute("SELECT expenditure_id FROM expenditure WHERE expenditure_name = :expenditure_name")
.bind("expenditure_name", "本")
.map((row, rowMetadata) -> row.get("expenditure_id", Integer.class))
.one()
.block();
StepVerifier.create(this.expenditureRepository.findById(expenditureId))
.consumeNextWith(expenditure -> {
assertThat(expenditure.getExpenditureId()).isNotNull();
assertThat(expenditure.getExpenditureName()).isEqualTo("本");
assertThat(expenditure.getUnitPrice()).isEqualTo(2000);
assertThat(expenditure.getQuantity()).isEqualTo(1);
assertThat(expenditure.getExpenditureDate()).isEqualTo(LocalDate.of(2019, 4, 1));
})
.verifyComplete();
}
@Test
void findById_Empty() {
Integer latestId = this.databaseClient.execute("SELECT MAX(expenditure_id) AS max FROM expenditure")
.map((row, rowMetadata) -> row.get("max", Integer.class))
.one()
.block();
StepVerifier.create(this.expenditureRepository.findById(latestId + 1))
.verifyComplete();
}
@Test
void save() {
Integer latestId = this.databaseClient.execute("SELECT MAX(expenditure_id) AS max FROM expenditure")
.map((row, rowMetadata) -> row.get("max", Integer.class))
.one()
.block();
Expenditure create = new ExpenditureBuilder()
.withExpenditureName("ビール")
.withUnitPrice(250)
.withQuantity(1)
.withExpenditureDate(LocalDate.of(2019, 4, 3))
.build();
StepVerifier.create(this.expenditureRepository.save(create))
.consumeNextWith(expenditure -> {
assertThat(expenditure.getExpenditureId()).isGreaterThan(latestId);
assertThat(expenditure.getExpenditureName()).isEqualTo("ビール");
assertThat(expenditure.getUnitPrice()).isEqualTo(250);
assertThat(expenditure.getQuantity()).isEqualTo(1);
assertThat(expenditure.getExpenditureDate()).isEqualTo(LocalDate.of(2019, 4, 3));
})
.verifyComplete();
}
@Test
void deleteById() {
Integer expenditureId = this.databaseClient.execute("SELECT expenditure_id FROM expenditure WHERE expenditure_name = :expenditure_name")
.bind("expenditure_name", "本")
.map((row, rowMetadata) -> row.get("expenditure_id", Integer.class))
.one()
.block();
StepVerifier.create(this.expenditureRepository.deleteById(expenditureId))
.verifyComplete();
StepVerifier.create(this.expenditureRepository.findById(expenditureId))
.verifyComplete();
}
}
TODOを実装しないでテストを実行すると次のようにfindById
テストが失敗します。
TODOを実装して、全てのテストが成功したら、App
クラスのmain
メソッドを実行して、次のリクエストを送り、正しくレスポンスが返ることを確認してください。
$ curl localhost:8080/expenditures -d "{\"expenditureName\":\"コーヒー\",\"unitPrice\":300,\"quantity\":1,\"expenditureDate\":\"2019-06-03\"}" -H "Content-Type: application/json"
{"expenditureId":1,"expenditureName":"コーヒー","unitPrice":300,"quantity":1,"expenditureDate":"2019-06-03"}
$ curl localhost:8080/expenditures
[{"expenditureId":1,"expenditureName":"コーヒー","unitPrice":300,"quantity":1,"expenditureDate":"2019-06-03"}]
$ curl localhost:8080/expenditures/1
{"expenditureId":1,"expenditureName":"コーヒー","unitPrice":300,"quantity":1,"expenditureDate":"2019-06-03"}
$ curl -XDELETE localhost:8080/expenditures/1
$ curl localhost:8080/expenditures
[]
SQLログを確認したい場合は
logback.xml
に次の設定を追加してください。<logger name="org.springframework.data.r2dbc.core.DefaultDatabaseClient" level="DEBUG" />
R2dbcExpenditureRepository
の正解例
package com.example.expenditure;
import org.springframework.data.r2dbc.core.DatabaseClient;
import org.springframework.transaction.reactive.TransactionalOperator;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.data.r2dbc.query.Criteria.where;
public class R2dbcExpenditureRepository implements ExpenditureRepository {
private final DatabaseClient databaseClient;
private final TransactionalOperator tx;
public R2dbcExpenditureRepository(DatabaseClient databaseClient, TransactionalOperator tx) {
this.databaseClient = databaseClient;
this.tx = tx;
}
@Override
public Flux<Expenditure> findAll() {
return this.databaseClient.select().from(Expenditure.class)
.as(Expenditure.class)
.all();
}
@Override
public Mono<Expenditure> findById(Integer expenditureId) {
return this.databaseClient.select().from(Expenditure.class)
.matching(where("expenditure_id").is(expenditureId))
.as(Expenditure.class)
.one();
}
@Override
public Mono<Expenditure> save(Expenditure expenditure) {
return this.databaseClient.insert().into(Expenditure.class)
.using(expenditure)
.fetch()
.one()
.map(map -> new ExpenditureBuilder(expenditure)
.withExpenditureId((Integer) map.get("expenditure_id"))
.build())
.as(this.tx::transactional);
}
@Override
public Mono<Void> deleteById(Integer expenditureId) {
return this.databaseClient.delete().from(Expenditure.class)
.matching(where("expenditure_id").is(expenditureId))
.then()
.as(this.tx::transactional);
}
}
Cloud Foundryへのデプロイ
Pivotal Web ServicesでPostgreSQLサービスをプロビジョニングします。cf create-service
コマンドでmoneyger-db
インスタンスを作成してください。
cf create-service elephantsql turtle moneyger-db
manifest.yml
も更新してください。
applications:
- name: moneyger
path: target/moneyger-1.0.0-SNAPSHOT.jar
memory: 128m
env:
JAVA_OPTS: '-XX:ReservedCodeCacheSize=22M -XX:MaxDirectMemorySize=22M -XX:MaxMetaspaceSize=54M -Xss512K'
JBP_CONFIG_OPEN_JDK_JRE: '[memory_calculator: {stack_threads: 30}]'
services:
- moneyger-db
ビルドしてcf push
してください。
./mvnw clean package -DskipTests=true
cf push
次のリクエストを送り、正しくレスポンスが返ることを確認してください。
curl https://moneyger-<CHANGE ME>.cfapps.io/expenditures -d "{\"expenditureName\":\"コーヒー\",\"unitPrice\":300,\"quantity\":1,\"expenditureDate\":\"2019-06-03\"}" -H "Content-Type: application/json"
cf restart moneyger
curl https://moneyger-<CHANGE ME>.cfapps.io/expenditures/1
"Kubernetesへのデプロイ"をスキップする場合は、こちらへ進んでください。
Kubernetesへのデプロイ
Cloud Foundryを使う人はこのセクションはスキップしてください。
Dockerイメージの作成
まずはDockerイメージをビルド&プッシュします。
$ pack build <image-name> --builder cloudfoundry/cnb:bionic --publish
# 例: pack build making/moneyger --builder cloudfoundry/cnb:bionic --publish
または
$ ./mvnw clean package -DskipTests=true
$ pack build <image-name> -p target/moneyger-1.0.0-SNAPSHOT.jar --builder cloudfoundry/cnb:bionic --publish
# 例: pack build making/moneyger -p target/moneyger-1.0.0-SNAPSHOT.jar --builder cloudfoundry/cnb:bionic --publish
を実行してください。
PostgreSQLのデプロイ
PostgreSQLをKubernetesにデプロイするためのマニフェストファイルを作成します。
moneyger-db.yml
を作成して、次の内容を記述してください。
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: moneyger-db
namespace: moneyger
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1G
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: moneyger-db
namespace: moneyger
spec:
selector:
matchLabels:
app: moneyger-db
strategy:
type: Recreate
template:
metadata:
labels:
app: moneyger-db
spec:
initContainers:
- name: remove-lost-found
image: busybox
command:
- sh
- -c
- |
rm -fr /var/lib/postgresql/data/lost+found
volumeMounts:
- name: moneyger-db
mountPath: /var/lib/postgresql/data
containers:
- image: postgres:11.5
name: postgres
env:
- name: POSTGRES_INITDB_ARGS
value: "--encoding=UTF-8 --locale=C"
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: moneyger-db
key: postgres-db
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: moneyger-db
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: moneyger-db
key: postgres-password
ports:
- containerPort: 5432
name: moneyger-db
volumeMounts:
- name: moneyger-db
mountPath: /var/lib/postgresql/data
volumes:
- name: moneyger-db
persistentVolumeClaim:
claimName: moneyger-db
---
apiVersion: v1
kind: Service
metadata:
name: moneyger-db
namespace: moneyger
spec:
ports:
- port: 5432
selector:
app: moneyger-db
clusterIP: None
PostgreSQLのユーザー情報をSecret
に作成するためのマニフェストファイルを作成します。次のコマンドを実行してください。
kubectl -n moneyger create secret generic moneyger-db \
--dry-run -o yaml \
--from-literal postgres-user=moneyger \
--from-literal postgres-password=moneyger \
--from-literal postgres-db=moneyger \
> moneyger-db-secret.yml
次のコマンドでPostgreSQLをデプロイします。
kubectl apply -f moneyger-db.yml -f moneyger-db-secret.yml
次のコマンドを実行し、moneyger-db-*****
という名前のPodのSTATUS
がRuning
になっていることを確認してください。
$ kubectl get -n moneyger all
NAME READY STATUS RESTARTS AGE
pod/moneyger-59c9b56d7-ddkdn 1/1 Running 0 5h3m
pod/moneyger-db-77f767f6d5-nfwc8 1/1 Running 0 34m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/moneyger LoadBalancer 10.100.200.144 a812e3e18c7e511e99cb7066f53a5a1a-1913282802.ap-northeast-1.elb.amazonaws.com 8080:31684/TCP 5h3m
service/moneyger-db ClusterIP None <none> 5432/TCP 34m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/moneyger 1/1 1 1 5h3m
deployment.apps/moneyger-db 1/1 1 1 34m
NAME DESIRED CURRENT READY AGE
replicaset.apps/moneyger-5779d966cb 1 1 1 5h3m
replicaset.apps/moneyger-db-77f767f6d5 1 1 1 34m
次のコマンドを実行してPostgreSQLの動作確認をしてください。
$ kubectl run -it --rm --image=postgres:11.5 --generator=run-pod/v1 --restart=Never --env="PGPASSWORD=moneyger" psql -- psql -h moneyger-db.moneyger.svc.cluster.local -U moneyger -d moneyger -c '\l'
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
-----------+----------+----------+---------+-------+-----------------------
moneyger | moneyger | UTF8 | C | C |
postgres | moneyger | UTF8 | C | C |
template0 | moneyger | UTF8 | C | C | =c/moneyger +
| | | | | moneyger=CTc/moneyger
template1 | moneyger | UTF8 | C | C | =c/moneyger +
| | | | | moneyger=CTc/moneyger
Moneygerのアップデート
moneyger.yml
を次のように更新します。PostgreSQLの接続情報を環境変数に設定します。
apiVersion: v1
kind: Namespace
metadata:
name: moneyger
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: moneyger
namespace: moneyger
spec:
replicas: 1
selector:
matchLabels:
app: moneyger
template:
metadata:
labels:
app: moneyger
spec:
containers:
- image: <image-name>:latest
# 例:
# image: making/moneyger:latest
name: moneyger
ports:
- containerPort: 8080
env:
- name: _JAVA_OPTIONS
value: "-Xmx15m -XX:ReservedCodeCacheSize=22M -XX:MaxDirectMemorySize=22M -XX:MaxMetaspaceSize=54M -Xss512K"
## ここから追加
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: moneyger-db
key: postgres-db
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: moneyger-db
key: postgres-user
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: moneyger-db
key: postgres-password
- name: DATABASE_URL
value: "postgresql://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@moneyger-db.moneyger.svc.cluster.local:5432/$(POSTGRES_DB)"
## ここまで追加
resources:
limits:
memory: "128Mi"
requests:
memory: "128Mi"
readinessProbe:
httpGet:
path: /expenditures
port: 8080
scheme: HTTP
initialDelaySeconds: 5
timeoutSeconds: 3
failureThreshold: 3
periodSeconds: 5
---
kind: Service
apiVersion: v1
metadata:
name: moneyger
namespace: moneyger
spec:
type: LoadBalancer
selector:
app: moneyger
ports:
- protocol: TCP
port: 8080
次のコマンドで変更を反映してください。
kubectl apply -f moneyger.yml
Connection Poolの設定
今回Pivotal Web Servicesで使用したPostgreSQLサービスは無料プランを使用しており、同時接続数が4という制限があります。 現在のコードでは5コネクション以上同時に接続するとエラーが発生します。
Connection Poolを設定し、最大Pool数を4にすることでエラーを防ぎます。
pom.xml
に次のdependency
を追加してください。
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-pool</artifactId>
</dependency>
App.java
に次のメソッドを追加してください。
static ConnectionPool connectionPool(ConnectionFactory connectionFactory) {
return new ConnectionPool(ConnectionPoolConfiguration.builder(connectionFactory)
.initialSize(4)
.maxSize(4)
.maxIdleTime(Duration.ofSeconds(3))
.validationQuery("SELECT 1")
.build());
}
routes
メソッドの次の箇所を、
static RouterFunction<ServerResponse> routes() {
final ConnectionFactory connectionFactory = connectionFactory();
// ...
}
次のように変更してください。
static RouterFunction<ServerResponse> routes() {
final ConnectionFactory connectionFactory = connectionPool(connectionFactory());
// ...
}
Cloud Foundryの場合は、ビルドしてcf push
してください。
./mvnw clean package -DskipTests=true
cf push
wrk
コマンドで負荷をかけてもエラーが発生しないことが確認できます。
wrk -t32 -c100 -d60s --latency --timeout 30s https://moneyger-<CHANGE ME>.cfapps.io/expenditures
Running 1m test @ https://moneyger-chatty-zebra.cfapps.io/expenditures
32 threads and 100 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 266.43ms 121.88ms 1.41s 88.77%
Req/Sec 12.60 5.94 30.00 57.44%
Latency Distribution
50% 223.84ms
75% 292.53ms
90% 404.77ms
99% 745.04ms
21965 requests in 1.00m, 6.37MB read
Requests/sec: 365.52
Transfer/sec: 108.51KB
Kubernetesの場合は、
$ pack build <image-name> --builder cloudfoundry/cnb:bionic --publish
# 例: pack build making/moneyger --builder cloudfoundry/cnb:bionic --publish
または
$ ./mvnw clean package -DskipTests=true
$ pack build <image-name> -p target/moneyger-1.0.0-SNAPSHOT.jar --builder cloudfoundry/cnb:bionic --publish
# 例: pack build making/moneyger -p target/moneyger-1.0.0-SNAPSHOT.jar --builder cloudfoundry/cnb:bionic --publish
を実行してDockerイメージのビルド&プッシュを実行してください。
moneyger.yml
で使用するDockerイメージを確実に更新するために、
image: <image-name>:latest
の部分を
image: <image-name>@sha256:<image-digest>
形式にしてください。
<image-digest>
の値はpack
コマンドの出力から確認できます。
例えば、次の出力の場合、image
に設定する値はmaking/moneyger@sha256:c0bac5367237f5c1a7989ca7bc21c574d890829f162caace81fc72897221e2f7
です。
===> EXPORTING
[exporter] Reusing layers from image 'index.docker.io/making/moneyger@sha256:b1c8bd34de9eb1993e836191908e1cb6468d08fe9ac1546bec4ce5712b932243'
[exporter] Exporting layer 'app' with SHA sha256:bad05a602ca8572ebf90aff7405c7b8ca12ca47df74ccbca381034f2da1026ef
[exporter] Exporting layer 'config' with SHA sha256:332c6f162dbfa1df40280262e08279deb0ec17876cdfd86bdd84d188110e3c77
[exporter] Reusing layer 'launcher' with SHA sha256:2187c4179a3ddaae0e4ad2612c576b3b594927ba15dd610bbf720197209ceaa6
[exporter] Reusing layer 'org.cloudfoundry.openjdk:openjdk-jre' with SHA sha256:9c84525dcbc758ce1754cce9b8f4d59f5ea6cf103a6c47043d900cad838052da
[exporter] Reusing layer 'org.cloudfoundry.jvmapplication:executable-jar' with SHA sha256:3d9310c8403c8710b6adcd40999547d6dc790513c64bba6abc7a338b429c35d2
[exporter] Exporting layer 'org.cloudfoundry.springboot:spring-boot' with SHA sha256:f8df93a1fec49ede0909fda3631f3a08d04a6ae947494deae4c73b98266ec2f3
[exporter] Reusing layer 'org.cloudfoundry.springautoreconfiguration:auto-reconfiguration' with SHA sha256:f61d2b65c75f9f5f2f2185fccb0be37ec39535bf89975c1632291f5116720479
[exporter] *** Images:
[exporter] index.docker.io/making/moneyger:latest - succeeded
[exporter]
[exporter] *** Digest: sha256:c0bac5367237f5c1a7989ca7bc21c574d890829f162caace81fc72897221e2f7
moneyger.yml
を更新したら次のコマンドで更新を反映してください。
kubectl apply -f moneyger.yml
kbld
を使うと、<image-digest>
を更新する作業を次のコマンドで自動化できます。
kbld -f moneyger.yml | kubectl apply -f -
次の章でも利用するのでこちらからバイナリをダウンロードしてkbld
をインストールしてください。