IK.AM

@making's tech note


Spring WebFlux.fnハンズオン - 4. R2DBCによるデータベースアクセス


本ハンズオンで、次の図のような簡易家計簿のAPIサーバーをSpring WebFlux.fnを使って実装します。 あえてSpring BootもDependency Injectionも使わないシンプルなWebアプリとして実装します。

ハンズオンコンテンツ

  1. はじめに
  2. 簡易家計簿Moneygerプロジェクトの作成
  3. YAVIによるValidationの実装
  4. R2DBCによるデータベースアクセス 👈
  5. Web UIの追加
  6. 例外ハンドリングの改善
  7. 収入APIの実装
  8. Spring Bootアプリに変換
  9. 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/expenditureR2dbcExpenditureRepositoryTestを作成して、次のテストコードを記述してください。

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テストが失敗します。

image

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のSTATUSRuning になっていることを確認してください。

$ 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をインストールしてください。


✒️️ Edit  ⏰ History  🗑 Delete