Akka gRPC で Hello World

プログラミング言語として Scala を使う場合、フレームワークとして Akka がよく利用されます。

また、gRPC の API は Protocol Buffers (protobuf) という Schema 定義言語で規定します。これは日本CTO協会が監修・編纂しているDX Criteriaで良いプラクティスとして SYSTEM-5-4 等でもあげられています。

この記事ではスキーマ駆動開発 (SDD) の実践として Akka gRPC を使った Hello World アプリケーションを実装します。

環境構築

開発と実行のため、次のツールのインストールが必要です。

  • sbt 1.8.2
  • scala 2.3.10
  • grpcurl 1.8.7

私が使用している mac の場合のインストール手順をこれから説明します。

Homebrew がすでにインストールされているものとして説明します。

grpcurl

Homebrew を使って grpcurl をインストールします。

brew install grpcurl

asdf

Homebrew を使って asdf をインストールします。

brew install asdf

java

asdf を使って java をインストールします。

asdf plugin add java
asdf install java temurin-17.0.5+8

デフォルトでインストールしたバージョンの java が使用されるように設定します。

asdf global java temurin-17.0.5+8

scala

asdf を使って scala をインストールします。

asdf plugin add scala
asdf install scala 2.13.10

デフォルトでインストールしたバージョンの scala が使用されるように設定します。

asdf global scala 2.3.10

sbt

asdf を使って sbt をインストールします。

asdf plugin add sbt
asdf install sbt 1.8.2

デフォルトでインストールしたバージョンの sbt が使用されるように設定します。

asdf global sbt 1.8.2

プロジェクトの生成

sbt を使ってプロジェクトを生成します。

sbt new sbt/scala-seed.g8

プロジェクト名は hello-akka-grpc です。

name [Scala Seed Project]: hello-akka-grpc

生成したプロジェクトは基本的な scala プロジェクトです。依存ライブラリや sbt plugin などの追加が必要です。

生成したプロジェクトに変更を加えたコードを GitHub リポジトリ に置いています。 ここからは、GitHub リポジトリのコードを使って説明します。

コード

gRPC は Protocol Buffers (protobuf) で定義され HTTP/2 で通信します。

protobuf の定義はさまざまな言語用のコードに変換できます。Akka gRPC の場合は、ScalaPB を使って scala のコードに変換されます。

SOASOAP の場合は XML、REST の場合は JSON、gRPC の場合は protobuf と理解すれば良さそうです。

では、早速 protobuf から記述します。src/main/protobuf/hello.proto ファイルは次の通りです。

syntax = "proto3";
package com.pigumer;

service HelloService {
  rpc Hello (HelloRequest) returns (HelloResult) {}
}

message HelloRequest {
  string name = 1;
}

message HelloResult {
  string value = 1;
}

次に、sbt を使って scala コードを生成します。

sbt protocGenerate

target/scala-2.13/akka-grpc/main に生成されたコードが出力されます。

サービスの実装

生成された HelloService トレイトを拡張してサービスを実装します。src/main/scalacom.pigumer パッケージの HelloServiceImpl クラスは次の通りです。

package com.pigumer
import akka.stream.Materializer

import scala.concurrent.Future

class HelloServiceImpl(implicit mat: Materializer) extends HelloService {

  override def hello(in: HelloRequest): Future[HelloResult] =
    Future.successful(HelloResult(s"Hello ${ in.name }"))
}

gRPC サーバを起動するためのコード

gRPC サーバを起動するコードも必要です。src/main/scalacom.pigumer パッケージの Main.scala は次の通りです。

package com.pigumer

import akka.actor.typed.scaladsl.Behaviors
import akka.actor.typed.{ActorSystem, Behavior}
import akka.http.scaladsl.Http
import com.typesafe.config.ConfigFactory

import scala.concurrent.Await
import scala.concurrent.duration.Duration

object Server {
  def setup: Behavior[Any] = Behaviors.setup { ctx =>
    implicit val system = ctx.system

    val helloService = HelloServiceHandler.withServerReflection(new HelloServiceImpl())

    Http().
      newServerAt("127.0.0.1", -1).
      bind(helloService)

    Behaviors.same
  }
}

object Main extends App {
  val conf = ConfigFactory.load()
    .withFallback(ConfigFactory.defaultApplication())

  val system = ActorSystem(Server.setup, "hello", conf)
  Await.ready(system.whenTerminated, Duration.Inf)
}

Server オブジェクトは、Akka Typed の作法でコーディングしています。

Main オブジェクトは、ActorSystem が終了するのを待つように実装しています (Await.ready(system.whenTerminated, Duration.Inf))。

sbt run を実行してください。

動作確認

grpcurl を使って動作確認ができます。

まず、サービス一覧を取得してみましょう。

grpcurl -plaintext localhost:8080 list

次のレスポンスが返ります。

com.pigumer.HelloService
grpc.reflection.v1alpha.ServerReflection

次に com.pigumer.HelloService のメソッド一覧を取得します。

grpcurl -plaintext localhost:8080 list com.pigumer.HelloService

次のレスポンスが返ります。

com.pigumer.HelloService.Hello

最後に com.pigumer.HelloService.Hello を実行します。protobuf で定義した通り、リクエストには name が必要です。grpcurl は JSON 形式でリクエストを渡すことができます。

grpcurl -plaintext -d '{"name":"World"}' localhost:8080 com.pigumer.HelloService.Hello

次のレスポンスが返ります。

{
  "value": "Hello World"
}

KeycloakのSAML2 IdPをAmazon Cognito user poolsと連携する

Keycloak は SAML の Identity Provider (IdP) としてあるいは OpenID Connect (OIDC) の OpenID Provider (OP) として使用することができます。

2層アーキテクチャ (Two-tier Architecture) や 3層アーキテクチャ (Three-tier Architecture)、またはより多層 (Multi-tier) となるマイクロサービスアーキテクチャで構築されたアプリケーションに認証認可機能を提供する場合には持参人トークン (Bearer token) を使用する OIDC が適していますが、SAML フェデレーション機能を活用することで ID やパスワードを使用する認証フローをプライベートネットワーク内に閉じることができる利点も得られます。

この記事では、SAML IdP として Keycloak を Rancher Desktop の Kubernetes 環境にインストールし、OIDC の OP として Amazon Cognito user pools をセットアップする方法を説明します。

環境

Keycloakのインストール

Keycloak は Helm チャートを使ってインストールします (ArtifactHUB)。

codecentric/helm-chartsvalues.yaml を元にした takesection/keycloak-install を clone します。

git clone https://github.com/takesection/keycloak-install.git

この記事では Rancher Desktop に同梱されている Ingress、traefik を使用するため、TLS のための証明書を用意する必要があります。

証明書を Let's Encrypt で取得する例から説明します。

certbot

Homebrew を使って certbot をインストールします。

brew install certbot

証明書生成

次のコマンドを実行します。

sudo certbot certonly --manual --preferred-challenges dns

ドメイン名を入力します。所有するドメイン名が example.com であれば、*.example.com のように入力すると便利です。

Please enter in your domain name(s) (comma and/or space separated)  (Enter 'c'
 to cancel):

ドメイン名の所有を確認するために、DNS に TXT レコードの追加が要求されます。

表示された指示に従って DNS (Route 53 等) に TXT レコードを追加します。

_acme-challenge.example.com TXT "<表示された値>"

DNS にレコードを追加してから、それがローカル環境で認識されるまで少し時間がかかるので、追加後しばらく待ってから Enter キーを押してください。

成功すると、この例であれば /etc/letsencrypt/live/example.com ディレクトリに証明書ファイルが生成されます。

生成されたファイルを keycloak-install にコピーします。

sudo cp /etc/letsencrypt/live/example.com/fullchain.pem /etc/letsencrypt/live/example.com/privkey.pem ./

tls-secret を作成します。

kubectl create secret tls tls-secret --cert=fullchain.pem --key=privkey.pem

values.ymlの編集

values.yml から local-values.yml をコピーしてください。

cp values.yml local-values.yml

local-values.yml の298行目、306行目にある ingress の設定を、証明書のドメイン名に合わせて編集します。例えば、使用するドメイン名が keycloak.example.com であれば、以下のようになります。

  # List of rules for the Ingress
  rules:
    -
      # Ingress host
      host: '{{ .Release.Name }}.example.com'
      # Paths for the host
      paths:
        - path: /
          pathType: Prefix
  # TLS configuration
  tls:
    - hosts:
        - keycloak.example.com
      secretName: "tls-secret"

Helm チャートのリポジトリを追加します。

helm repo add codecentric https://codecentric.github.io/helm-charts

keycloak をインストールします。

helm install keycloak --values=local-values.yml codecentric/keycloak  

Keycloak の初期設定

port-forward で localhost でアクセスできるようにします。

kubectl port-forward svc/keycloak-http 8080:80

ブラウザを使って http://localhost:8080/auth/ にアクセスして、管理者のユーザ名とパスワードを設定します。

Create ボタンをクリックします。

メタデータのダウンロード

kubectl get ingress で表示されたホスト名とIPアドレスを、DNS (Route 53) または /etc/hosts ファイルに設定します。

ブラウザを使って https://keycloak.example.com/auth/ にアクセスします。以降、example.com の部分は所有しているドメイン名で読み替えてください。

Administration Console をクリックして、設定したユーザ名、パスワードを入力して Sign In ボタンをクリックします。

左上の Master と表示されているところにマウスカーソルを移動して、Add realm ボタンをクリックします。

任意の Name を入力します。ここでは local と入力して Create ボタンをクリックします。

レルム Local の Endpoints にある SAML 2.0 Identity Provider Metadata のリンクをクリックして、ダウンロードします。

Cognito User pools

AWS の管理コンソールにログインして、東京 (ap-northeast-1) リージョンの Cognito サービスにアクセスし、ユーザープールの管理 ボタンをクリックします。

ユーザープールを作成する ボタンをクリックし、プール名に keycloak と入力します。そして デフォルトを確認する ボタンをクリックします。

プールの作成 ボタンをクリックします。

プールID の値をメモします。urn:amazon:cognito:sp:<プールID> を Keycloak のクライアント名の設定に後で使用します。

左側にある アプリの統合 - ドメイン名 を選択して、ドメイン名を入力し、変更の保存 ボタンをクリックします。

ドメイン名が例えば https://example.auth.ap-northeast-1.amazoncognito.com の場合、Keycloak のクライアントに設定する Endpoint は https://example.auth.ap-northeast-1.amazoncognito.com/saml2/idpresponse になります。

左側にある 全般設定 - アプリクライアント を選択し、アプリクライアントの追加 リンクをクリックします。

アプリクライアント名saml と入力して アプリクライアントの作成 ボタンをクリックします。

左側にある フェデレーション - ID プロバイダー を選択し、SAML を選択します。

メタデータドキュメントSelect file ボタンをクリックして、Keycloak の SAML 2.0 Identity Provider Metadata リンクからダウンロードしたファイルを選択します。プロバイダ名には keycloak と入力して プロバイダーの作成 ボタンをクリックします。

左側にある フェデレーション - 属性マッピング を選択し、SAML 属性の追加 リンクをクリックします。SAML 属性email と入力し、ユーザープール属性Email を選択して 変更の保存 ボタンをクリックします。Amazon Cognito user pools は email 属性が必須なためです。

Keycloak のクライアント

再び、Keycloak に戻ってクライアントを追加します。

レルム LocalConfigure - Clients を選択し Create ボタンをクリックします。

Client IDurn:amazon:cognito:sp:<プールID> の形式で入力します。

Client Protocolsaml を選択し、Save ボタンをクリックします。

Settings をデフォルトから変更する項目は次のとおりです。

  • Sign DocumentsOFF
  • Sign AssertionsON
  • Client Signature RequiredOFF
  • Force Name ID FormatON
  • Name ID Formatpersistent に設定
  • Valid Redirect URIshttps://<Cognito user pools に設定したドメイン名>/saml2/idpresponse を設定

Save ボタンをクリックします。

Mappers タブを選択し、Create ボタンをクリックします。

次のように入力し、Save ボタンをクリックします。

  • Nameemail を設定
  • Mapper TypeUser Attribute
  • User Attributeemail を設定
  • SAML Attribute Nameemail を設定

ユーザの追加

左側の Manage - Users を選択し、Add user ボタンをクリックします。

UsernameEmail 等を入力し Save ボタンをクリックします。

追加したユーザの Credentials タブを選択し、パスワードを設定して Set Password ボタンをクリックします。

Cognito User poolsのアプリクライアントの設定

AWS 管理コンソールの Cognito サービスの左側にある アプリの統合 - アプリクライアントの設定 をクリックします。

有効な ID プロバイダkeycloak を選択します。

コールバック URL は OIDC のフローのコールバック URL を設定しますが、テストのために http://localhost:8080/callback と入力します。

許可されている OAuth フロー は、これもテストのために Implicit grant をチェックします。

許可されている OAuth スコープ は、emailopenid をチェックし、変更の保存 ボタンをクリックします。

ホストされた UI を起動 リンクをクリックし、keycloak ボタンをクリックします。

Keycloak の認証画面が表示されるので、Username or emailPassword を入力して Sign In ボタンをクリックします。

サインインに成功しても、コールバック URL を処理するアプリケーションがありませんが、ブラウザの URL にはコールバック URL とフラグメント (#) の後に access_tokenid_token 等が表示されます。id_token の値を jwt.io にペーストして JWT を確認できます。

なお、コールバック URL を envoyproxy で処理することも可能です。「Envoy OAuth2 Filter を使ったログイン」の記事で説明しています。また、この記事で紹介している GitHub リポジトリ には Kubernetes 環境にデプロイするための Helm チャートも用意しています。

まとめ

SAMLOpenID Connect のような業界標準を採用することで、認証基盤に採用するプロダクトの幅が広がります。この記事では Keycloak を SAML IdP として使用する例を紹介しました。

Keycloak を SAML IdP として使用することで、WebAuthn のような新しい認証方式への対応も可能になります。

Keycloak を使用する WebAuthn のようなパスワードレスな認証方式の対応は次の記事を参照してください。

Active Directory と AD FS を使用する例は検索すると数多く見つかるでしょう。次の記事も参照してください。

OpenID Connect のトークンを処理するアプリケーション構築の記事は次のとおりです。

SAML のコールバックを Spring Boot などの Java フレームワークでハンドルできます。かなり古いリポジトリですが GitHub リポジトリ を参照してください。

参考

Envoy Proxyを使ったファイルベースの動的コンフィグレーションとk8sのライフサイクル

みらい翻訳のカレンダー | Advent Calendar 2022 - Qiita に投稿した2つの記事 (「Envoy Proxyを使ったファイルベースの動的コンフィグレーション」、「Envoy Proxyを使ったファイルベースの動的コンフィグレーション (EKS)」) では、いずれも起動前に Envoy Proxy の構成ファイルをコピーし、また複数の Pod 間で共有するイメージとしていました。

これら 2 つの記事で、Kubernetes の hostPath と Persistent Volumes (Amazon Elastic Block Store (EBS)) の使い方を理解することができます。

しかし、起動時の初期構成ファイルをあらかじめストレージにコピーすることは、運用が煩雑になる可能性があります。

また、これら 2 つの記事では Envoy Proxy のコンテナイメージに shell や mv コマンド等を含むことを想定していました。これはコンテナサイズとセキュリティの両面から問題があります。

この記事では、コンテナのライフサイクルを解説して、課題の解決方法を示します。

postStart と preStop

Kubernetes では、コンテナの起動後と停止前に任意のコマンドの実行が可能です。これを利用することで、Envoy Proxy の構成ファイルを ConfigMap や Secrets からコピーできます。

またこの場合、Envoy Proxy のコンテナイメージに shell や cp コマンド等を含んでいることが前提となります。これは課題であるコンテナサイズの縮小やセキュリティ向上に寄与しないということになります。

さらに、postStart に記述されたコマンドはコンテナの起動と同時並行で実行されます。試してみると、Envoy Proxy の方が構成ファイルのコピーより早く起動して、そのため構成ファイルが見つからず、コンテナの起動に失敗しました。 つまり、この方法は Envoy Proxy の場合には利用できませんでした。

このトピックの詳細については、Kubernetes のドキュメント「コンテナライフサイクルイベントへのハンドラー紐付け」を参照してください。

Init Containers

Kubernetes には、Pod 内で主となるコンテナを起動する前に別のコンテナを順番に起動する「Init コンテナ」機能があります。

initContainers に定義されたコンテナは上から順番に実行され、それぞれのコマンドが完了してから次のコンテナが実行され、initContainers 全てのコマンド完了後、主となるコンテナを起動します。

これを利用することで、この記事の例にある Envoy Proxy や Demo アプリケーションの起動前に必要な構成ファイルを ConfigMap や Secrets からターゲットファイルにコピーできます。

Init コンテナを使用する利点は、Kubernetes のドキュメントで説明されている通りで、主となるコンテナイメージにセットアップ用のツール等を含める必要がなくなり、コンテナサイズの縮小とセキュリティ向上に寄与します。

2 つの記事の説明で残されていた課題の解決策として活用できます。

サンプルコード

サンプルコードは、これまでと同じ GitHub リポジトリ にあります。

emptyDir

今回、構成ファイルをコピーするボリュームに emptyDir (Volumes) を使用します。emptyDir は Pod のノード割り当て時に最初に空で作成され、Pod が実行されている間存在し続けます。

initContainers のコンテナコマンドで ConfigMap に定義された動的コンフィグレーションを emptyDir ボリュームにコピーします。

これまでの記事では永続ボリュームに動的コンフィグレーションファイルをコピーしていました。このため、動的コンフィグレーションが変更された後 Pod を再起動すると変更後のルーティングで起動されました。

これ自体は問題ではありませんが、ローリングアップデートやカナリアの場合には全ての Pod の設定を同時に変更せず、徐々に変更を反映して、場合によっては変更をキャンセルします。

残念ながら永続ボリュームを使用する場合、このような制御はより複雑な運用が必要になってしまいます。

この記事で説明する方法であれば、Pod を再起動すると、ConfigMap から emptyDir に起動時にコピーするため、ConfigMap の構成で再起動されることが保証されます。

同じ Pod を 3 つにスケーリングしている場合、つまり replicas が 3 の場合の動的コンフィグレーションのローリングアップデートを考えてみます。

  1. Pod-1 の emptyDir に構成ファイルを mv して、動的コンフィグレーションを変更
  2. Pod-1 の変更が正常であることを確認
  3. Pod-2 の emptyDir に構成ファイルを mv して、動的コンフィグレーションを変更
  4. Pod-2 の変更が正常であることを確認
  5. Pod-3 の emptyDir に構成ファイルを mv して、動的コンフィグレーションを変更
  6. Pod-3 の変更が正常であることを確認
  7. 動的コンフィグレーションを定義した ConfigMap を更新

もし、途中で不具合が判明した場合は、7 の前であれば変更に失敗した Pod を再起動するだけで前の状態に戻せます。

実行環境

emptyDir はローカル環境の KubernetesAmazon Elastic Kubernetes Service (EKS) もどちらも同様に利用することができます。

EKS を利用する場合、前回の記事同様 spring-boot/eks/terraform ディレクトリで次のコマンドを実行して EKS クラスタを構築します。

terraform init
terraform plan
terraform apply

Helm チャート

Helm チャートは spring-boot/demo2 ディレクトリにあります。

Helm チャートはローカル環境の Kubernetes を使用する場合は次のコマンドでインストールします。

helm install us ./demo2
helm install jp --values=jp-values.yaml ./demo2

EKS の場合は次のコマンドでインストールします。

helm install us --values=eks-demo2-us-values.yaml ./demo2
helm install jp --values=eks-demo2-jp-values.yaml ./demo2

インストール結果は次のコマンドで確認できます。

kubectl get configMap,pod,svc

この記事のキーとなる deployment.yaml の initContainers の設定は次の通りです。

      initContainers:
        - name: init
          image: busybox
          command: ["sh", "-c", "(cp /config/cds/cds.yaml /config/dynamic/cds.yaml; cp /config/lds/lds.yaml /config/dynamic/lds.yaml)"]
          volumeMounts:
            - mountPath: /config/dynamic
              name: dynamic-config
            - mountPath: /config/cds
              name: cds
            - mountPath: /config/lds
              name: lds

Volumes の設定は次の通りです。

      volumes:
        - name: dynamic-config
          emptyDir: {}
        - name: config
          configMap:
            name: {{ include "demo2.fullname" . }}-config
        - name: cds
          configMap:
            name: {{ include "demo2.fullname" . }}-cds
        - name: lds
          configMap:
            name: {{ include "demo2.fullname" . }}-lds

この設定により、Initコンテナの BusyBox は次のようにボリュームがマウントされます。

/config     - dynamic-config           (emptyDir)
/config/cds - {{ demo2.fullname }}-cds (ConfigMap)
/config/lds - {{ demo2.fullname }}-lds (ConfigMap)

そして、cds と lds の ConfigMap は、command に書かれた cp コマンドにより dynamic-config の emptyDir ボリュームにコピーされます。

そして、Envoy Proxy のコンテナの設定は次の通りです。

        - name: {{ .Values.envoyimage.name }}
          securityContext:
            {{- toYaml .Values.securityContext | nindent 12 }}
          image: {{ .Values.envoyimage.repository }}:{{ .Values.envoyimage.tag }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
            - name: admin
              containerPort: 9901
              protocol: TCP
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
          volumeMounts:
            - readOnly: true
              mountPath: /config
              name: config
            - mountPath: /config/dynamic
              name: dynamic-config
          args:
            - "-c"
            - "/config/front-envoy.yaml"

動的コンフィグレーションの変更

EKS を使用している場合は、サービスの EXTERNAL-IP を使ってアクセスしてください。ここではローカル環境の場合の説明をします。

service/us-demo2service/jp-demo2 それぞれのサービスに外部から接続できるように port-forward を実行します。

kubectl port-forward svc/us-demo2 8080:80
kubectl port-forward svc/jp-demo2 8081:80
curl http://localhost:8080
Hello World

curl http://localhost:8080/jp
{"timestamp":"2022-12-28T06:26:45.295+00:00","path":"/jp","status":404,"error":"Not Found","message":null,"requestId":"7c8f19c4-2"}

curl http://localhost:8081
Hello Japan

では、us-demo2 の Envoy Proxy の動的コンフィグレーションを変更します。

注意: この段階ではまだ、Envoy Proxy に shell や mv コマンドが存在していることを想定しています。プロダクション環境でこのような処理を実行するためには、REST API でこの一連の処理をするコンテナを作成して、Pod に追加することをおすすめします。

まず、Pod 名を確認します。

kubectl get pod | grep us-demo2
us-demo2-598f8c6fbc-wj29t   2/2     Running   0          17m

POD_NAME 環境変数に Pod 名を設定します。

export POD_NAME=us-demo2-598f8c6fbc-wj29t

Envoy Proxy のクラスタ設定 (cds.yaml) を更新します。

kubectl cp config/cds/demo2-cds2.yaml $POD_NAME:/config/dynamic/cds2.yaml -c envoy
kubectl exec $POD_NAME -c envoy -- mv -f /config/dynamic/cds2.yaml /config/dynamic/cds.yaml

次にリスナー設定 (lds.yaml) を更新します。

kubectl cp config/lds/demo2-lds2.yaml $POD_NAME:/config/dynamic/lds2.yaml -c envoy
kubectl exec $POD_NAME -c envoy -- mv -f /config/dynamic/lds2.yaml /config/dynamic/lds.yaml

ルーティングの変更を確認します。

curl http://localhost:8080
Hello World

curl http://localhost:8080/jp
Hello Japan

まとめ

Initコンテナによる初期化、emptyDir ボリュームを利用する方法が前回までの記事による方法より運用、セキュリティ上も利点が多くなります。

ファイルベースの動的コンフィグレーションを採用する場合は、この記事の方法を参考にしてください。

参考

Envoy Proxyを使ったファイルベースの動的コンフィグレーション (EKS)

メリークリスマス !

こんにちは、ed (edward) です。

この記事は、みらい翻訳のカレンダー | Advent Calendar 2022 - Qiita の25日目です。

前回17日目はローカルPC上の Kubernetes を使ったファイルベースの動的コンフィグレーションを説明しました。

今回のこの記事では、Amazon Elastic Kubernetes Service (EKS) を使って説明します。

IaC ツールは、Terraform を使用します。コードは、GitHub リポジトリ にあります。

最終的に実現されるルーティングは前回と同じです。

なお、プロダクション環境で利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーションとk8sのライフサイクル」を参照することをおすすめします。

EKS クラスタの構築

git clone した後、spring-boot/eks/terraform ディレクトリに移動してください。そして、次の3つのコマンドを順に実行します。

terraform init
terraform plan
terraform apply

この terraform により、永続ボリュームに EBS を使う aws-ebs-csi-driverクラスタ add on に導入されます。

構築された EKS クラスタkubectl のデフォルトコンテキストに設定します。

aws eks update-kubeconfig --name envoyproxy-examples

EBS ボリュームの作成

クラスタで生成されたノードのアベイラビリティゾーンを確認します。

kubectl get node -L topology.ebs.csi.aws.com/zone

EBS ボリュームは管理コンソールまたは AWS CLI で作成します。今回使用する EBS ボリュームは最小の 1G で作成します。

aws ec2 create-volume --availability-zone ap-northeast-1d --size 1

永続ボリュームの作成 (PersistentVolume と PersistentVolumeClaim)

spring-boot/eks ディレクトリに移動します。

pv.yaml ファイルを編集して、作成した EBS ボリュームの ID を volumeHandle に設定します。

kubectl apply -f pv.yaml
kubectl apply -f pvc.yaml

EnvoyProxy の構成ファイルのコピー

ここまでの手順では EBS ボリュームは空の状態です。Envoy Proxy の初期の構成ファイルを BusyBox のイメージを使ってコピーします。

次のコマンドを実行する前に、deployment.yamltopology.ebs.csi.aws.com/zone を EBS ボリュームを作成したアベイラビリティゾーンに合わせてください。

kubectl apply -f deployment.yaml
export POD_NAME=$(kubectl get pod -o jsonpath='{.items[0].metadata.name}' -l app=app)
kubectl cp ../config $POD_NAME:/app/data/jp/config/
kubectl cp ../config $POD_NAME:/app/data/spring-boot-demo/config/
kubectl exec $POD_NAME -- ls -lR /app/data
kubectl delete -f deployment.yaml

メモ: 構成ファイルのコピーが完了した EBS ボリュームのスナップショットをここで取得しておくと、異なるアベイラビリティゾーンで同じデータが必要になった時にスナップショットから EBS ボリュームを作成できるようになるので便利です。

Helm を使ってインストール

spring-boot ディレクトリに移動します。

レガシーアプリケーションを想定したサービスのインストール

helm install spring-boot-demo --values=eks-us-values.yaml ./spring-boot-demo

kubectl get svc を実行すると、次のような出力が得られます。

NAME                     TYPE           CLUSTER-IP      EXTERNAL-IP                                                                    PORT(S)        AGE
kubernetes               ClusterIP      172.20.0.1      <none>                                                                         443/TCP        57m
spring-boot-demo         LoadBalancer   172.20.187.23   a461f11bac2c4475c80f679b35ee1457-1810793654.ap-northeast-1.elb.amazonaws.com   80:31938/TCP   2m27s
spring-boot-demo-admin   LoadBalancer   172.20.190.77   a6db6b07a227c46b8b12a7e4b4861348-1598948861.ap-northeast-1.elb.amazonaws.com   80:30818/TCP   2m27s

spring-boot-demoEXTERNAL-IP にあるドメイン名を使ってアクセスすると、Hello World が返ります。

curl http://a461f11bac2c4475c80f679b35ee1457-1810793654.ap-northeast-1.elb.amazonaws.com
Hello World

新しいサービスを想定したサービスのインストール

helm install jp --values=eks-jp-values.yaml ./spring-boot-demo

kubectl get svc を実行すると、次のような出力が得られます。

NAME                        TYPE           CLUSTER-IP      EXTERNAL-IP                                                                    PORT(S)        AGE
jp-spring-boot-demo         LoadBalancer   172.20.247.84   af7999c557c184403ab89c1c594f74af-1198057548.ap-northeast-1.elb.amazonaws.com   80:31249/TCP   55s
jp-spring-boot-demo-admin   LoadBalancer   172.20.91.61    a6566724d98db49a69605d8be2e278bb-326792365.ap-northeast-1.elb.amazonaws.com    80:32010/TCP   55s
kubernetes                  ClusterIP      172.20.0.1      <none>                                                                         443/TCP        64m
spring-boot-demo            LoadBalancer   172.20.187.23   a461f11bac2c4475c80f679b35ee1457-1810793654.ap-northeast-1.elb.amazonaws.com   80:31938/TCP   9m12s
spring-boot-demo-admin      LoadBalancer   172.20.190.77   a6db6b07a227c46b8b12a7e4b4861348-1598948861.ap-northeast-1.elb.amazonaws.com   80:30818/TCP   9m12s

jp-spring-boot-demoEXTERNAL-IP にあるドメイン名を使ってアクセスすると、Hello Japan が返ります。

curl http://af7999c557c184403ab89c1c594f74af-1198057548.ap-northeast-1.elb.amazonaws.com
Hello Japan

ルーティングの追加

ルーティングを追加する前に、spring-boot-demojp パスを確認します。

curl http://a461f11bac2c4475c80f679b35ee1457-1810793654.ap-northeast-1.elb.amazonaws.com/jp
{"timestamp":"2022-12-25T12:20:06.818+00:00","path":"/jp","status":404,"error":"Not Found","message":null,"requestId":"a705bb12-3"}

上の通り 404 エラーが返されます。

kubectl get podspring-boot-demo のポッド名を調べます。

export POD_NAME=spring-boot-demo-ccc77d49b-b6zb7
kubectl exec $POD_NAME -c envoy -- cat /var/lib/envoy/spring-boot-demo/config/cds/cds.yaml
resources:
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: us
    load_assignment:
      cluster_name: us
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081

クラスタの更新

jp-spring-boot-demo サービスを Envoy Proxy のクラスタに追加する設定ファイルをコピーします。

kubectl exec $POD_NAME -c envoy -- mv -f /var/lib/envoy/spring-boot-demo/config/cds/cds2.yaml /var/lib/envoy/spring-boot-demo/config/cds/cds.yaml

リスナーの更新

kubectl exec $POD_NAME -c envoy -- mv -f /var/lib/envoy/spring-boot-demo/config/lds/lds2.yaml /var/lib/envoy/spring-boot-demo/config/lds/lds.yaml
curl http://a461f11bac2c4475c80f679b35ee1457-1810793654.ap-northeast-1.elb.amazonaws.com/jp
Hello Japan

リソースの削除

最後に、使用したリソースを削除して課金されないようにします。

cd spring-boot
helm uninstall jp
helm uninstall spring-boot-demo
cd eks
kubectl delete -f pvc.yaml
kubectl delete -f pv.yaml
cd terraform
terraform destroy

上記コマンドを実行した後、手動で作成した EBS ボリュームを削除します。

まとめ

この記事では永続ボリュームに EBS を使用しました。Envoy Proxy の構成変更はファイルの中身の変化ではなく移動 (mv) を検出するものであるためです。

EBS の場合、同じアベイラビリティゾーンのノードにデプロイされた Pod でなければ利用できないという制約があります。この制約のない永続ボリュームとして利用できる他のサービスに Amazon Elastic File System (EFS)Amazon FSx for LustreAmazon FSx for NetApp ONTAP があります。

ところで、デジタルバッジといえば、AWS 認定資格のものが有名ですが、今年は、AWS Skill Builder のラーニングプランにあるバッジアセスメントで 80% 以上の正答率で得られるバッジがいくつか発表されています。

この記事の EBS についてのラーニングプランもあります。

以下の3つのラーニングプランそれぞれでデジタルバッジを獲得でき、3つとも取得すると、Storage Core バッジも取得できます。

最後まで読んでいただきありがとうございます。

それでは皆さん、よいお年をお迎えください。

We're hiring!

みらい翻訳では、アーキテクチャに興味のある方や技術ブログを盛り上げていただけるエンジニアを募集しています! ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。

参考

Envoy Proxyを使ったファイルベースの動的コンフィグレーション

こんにちは、ed (edward) です。

この記事は、みらい翻訳のカレンダー | Advent Calendar 2022 - Qiita の17日目です。

さて、SaaS として提供している Mirai Translator のようなプロダクトの場合、原則として機能追加等のデプロイ、リリースをゼロダウンタイムで実施することが求められます。

また Strangler Fig Pattern を適用して追加や変更する機能を別サービスとしてデプロイし、最終的に全てのトラフィックが新しいサービスに流れるようにルーティングを切り替えて旧サービスを枯らせていく戦略で迅速かつ継続的なアーキテクチャ (Continuous Architecture) をドライブしたいと考えています。

Kubernetes 環境でこのような切り替えの方法はいくつかあり、AWS App MeshIstio のようなサービスメッシュを活用するケースがよく紹介されていますが、ここではサービスメッシュのサイドカーでよく使用されている Envoy Proxy をファイルベースの動的コンフィグレーションを使用して説明します。

Envoy Proxy とは

Envoy Proxy は、C++ で記述された軽量のプロキシコンポーネントです。API または構成ファイルを使ってゼロダウンタイムでルーティング等の構成変更が可能なため、さまざまなサービスメッシュのデータプレーンのコンポーネントとして活用されています。

ルーティングの変更がゼロダウンタイムで可能なため Strangler Fig Pattern、Feature Toggles (Feature Flags)、A/B テスト、Blue/Green Deployment、カナリアリリース等さまざまなユースケースの実現が可能です。

コントロールプレーン

サービスメッシュのコントロールプレーンでは、Envoy Proxy のようなデータプレーンのサイドカーAPI により制御します。

この記事では、Envoy Proxy の API にアクセスするコントロールプレーンを実装せず、構成ファイルを切り替えて制御する方法を説明します。

環境

プロダクション環境では Amazon EKS 等を利用することになりますが、検証では Apple Silicon 搭載の mac miniRancher Desktop をインストールした環境を使用します。

Amazon EKS と Persistent Volumes に EBS を利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーション (EKS)」を参照してください。

そして、プロダクション環境で利用する場合は「Envoy Proxyを使ったファイルベースの動的コンフィグレーションとk8sのライフサイクル」を参照することをおすすめします。

ストーリー

次のような Strangler Fig Pattern を実現するストーリーをこれから説明します。

  1. 今後枯らせるサービスをイメージした spring-boot-demo サービスをデプロイします
  2. spring-boot-demo の動作確認を実施します
  3. 新サービスを想定した、jp-spring-boot-demo をデプロイします
  4. jp-spring-boot-demo 単体の動作確認を実施します
  5. spring-boot-demo API/jp パスを追加して jp-spring-boot-demo にルーティングするように設定します

準備

この記事のコードは、GitHub リポジトリ にあります。最初にこれを clone します。

git clone https://github.com/takesection-sandbox/envoyproxy-examples.git

以降は、clone した envoyproxy-examplesspring-boot ディレクトリで操作を行なっていきます。

cd envoyproxy-examples/spring-boot

コンテナイメージのビルド

このビルドの前に Java 17 以降の JDKMaven、Rancher Desktop をインストールして、Rancher Desktop では Container Engine に dockerd(moby) を使用するように設定しておきます。

次のコマンドでコンテナイメージをビルドします。

mvn spring-boot:build-image

1. spring-boot-demo をデプロイ

Envoy Proxy のクラスタとリスナーの構成ファイルをコピーします。

mkdir /tmp/rancher-desktop/spring-boot-demo
cp -r config /tmp/rancher-desktop/spring-boot-demo

Envoy Proxy の「Configuration: Dynamic from filesystem」には、次のような記述があります。

Envoy only updates when the configuration file is replaced by a file move, and not when the file is edited in place.

It is implemented this way to ensure configuration consistency.

意訳すると、Envoy の構成の一貫性を確保するため、構成ファイルをその場で編集するのではなく、移動によって置換された場合にのみ更新されるとあります。

ConfigMap や Secret は読み込みのみのボリュームマウントしかサポートしていないこともあって、ファイルベースの動的コンフィグレーションには不適です。

そのため、この記事では読み書きが可能なボリュームマウントを使用します。

次にサービスを Helm チャートでデプロイします。Helm を利用すると、デプロイする構成それぞれのマニフェストファイル (deployment、service、configmap など) をテンプレート化して、必要な部分の変更がしやすくなります。

helm install spring-boot-demo ./spring-boot-demo

2. spring-boot-demo の動作確認を実施

ここでは、Ingress を使用せず、port-forward を使って確認します。まず、次のコマンドを実行します。

kubectl port-forward service/spring-boot-demo 8080:80

構成は次のとおりです。

別のターミナルで、次のコマンドを実行します。

curl http://localhost:8080

レスポンスとして、"Hello World" が表示されます。

3. 新機能を想定した、jp-spring-boot-demo をデプロイ

Envoy Proxy のクラスタとリスナーの構成ファイルをコピーします。

mkdir /tmp/rancher-desktop/jp
cp -r config /tmp/rancher-desktop/jp

Helm チャートでデプロイします。

helm install jp --values=jp-values.yaml ./spring-boot-demo

4. jp-spring-boot-demo 単体の動作確認を実施

ここでも、Ingress を使用せず、port-forward を使って確認します。まず、次のコマンドを実行します。

kubectl port-forward service/jp-spring-boot-demo 8081:80

構成は次のとおりです。

別のターミナルで、次のコマンドを実行します。

curl http://localhost:8081

レスポンスとして、"Hello Japan" が表示されます。

5. ルーティングの変更

では、ここから本題の spring-boot-demo API/jp パスを追加して jp-spring-boot-demo にルーティングするように設定していきます。

ファイルによる動的コンフィグレーション

Envoy Proxy のクラスターとリスナーの構成は cds_configlds_config で設定します。

Helm チャートのテンプレートファイル (spring-boot/spring-boot-demo/templates/configmap.yaml) では次のように設定しています。

    dynamic_resources:
      cds_config:
        path_config_source:
          path: /var/lib/envoy/cds/cds.yaml
      lds_config:
        path_config_source:
          path: /var/lib/envoy/lds/lds.yaml

このサンプルでは、構成ファイルのパスに hostPath を使ったローカルのファイルシステムをマウントするように設定しています。プロダクション環境の場合は、Amazon EFS 等をマウントして利用するとよいでしょう (「Amazon EKS で永続的ストレージを使用するにはどうすればよいですか?」)。

ルーティングを切り替える前に、まず spring-boot-demo のポッド名を取得して、環境変数POD_NAME に設定しておきます。

kubectl get pod

次のようにポッド一覧が表示されます。

NAME                                   READY   STATUS    RESTARTS   AGE
jp-spring-boot-demo-7b5454f8b7-c9vc5   2/2     Running   0          55m
spring-boot-demo-686f67b754-r7g4s      2/2     Running   0          51m

spring-boot-demo で始まるポッド名を環境変数に設定します。

export POD_NAME=spring-boot-demo-686f67b754-r7g4s

kubectl describe pod ポッド名 でポッドの情報を取得できます。そこから、このポッドに spring-boot-demoenvoy という名前の2つのコンテナが含まれていることがわかります。

まず、現在の cds.yaml を表示してみましょう。

kubectl exec $POD_NAME -c envoy -- cat /var/lib/envoy/cds/cds.yaml

resources:
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: us
    load_assignment:
      cluster_name: us
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081

この設定に、jp-spring-boot-demo を追加します。

その前に Envoy の管理画面を表示して確認してみましょう。以下のコマンドを実行して、ブラウザで http://localhost:8082 にアクセスしてください。

kubectl port-forward service/spring-boot-demo-admin 8082:80

clusters リンクをクリックすると現在のクラスタの情報が表示されます。

クラスターの変更

cds.yaml と同じ場所に、jp-spring-boot-demo を追加する cds2.yaml ファイルがあります。これを、cds.yaml に上書きしましょう。

kubectl exec $POD_NAME -c envoy -- mv -f /var/lib/envoy/cds/cds2.yaml /var/lib/envoy/cds/cds.yaml

jp クラスターが増えていることを確認できます。

上書きにより、次の cds.yaml に置換されています。

resources:
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: us
    load_assignment:
      cluster_name: us
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: 127.0.0.1
                    port_value: 8081
  - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
    name: jp
    type: LOGICAL_DNS
    dns_lookup_family: V4_ONLY
    load_assignment:
      cluster_name: jp
      endpoints:
        - lb_endpoints:
            - endpoint:
                address:
                  socket_address:
                    address: jp-spring-boot-demo.default.svc.cluster.local
                    port_value: 80

Kubernetes のサービスにはそれぞれ IP アドレスが割り当てられ、また DNS 名も割り当てられます。jp-spring-boot-demo サービスの DNS 名は jp-spring-boot-demo.default.svc.cluster.local になります。

リスナーの変更

上と同様に、現在の lds.yaml を表示してみましょう。

kubectl exec $POD_NAME -c envoy -- cat /var/lib/envoy/lds/lds.yaml

resources:
  - "@type": type.googleapis.com/envoy.config.listener.v3.Listener
    name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
      - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains:
                      - '*'
                    routes:
                      - match:
                          prefix: "/"
                        route:
                          cluster: us
              http_filters:
                - name: envoy.router
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

そして、cds.yaml の場合と同様に lds2.yaml ファイルを lds.yaml に上書きします。

kubectl exec $POD_NAME -c envoy -- mv -f /var/lib/envoy/lds/lds2.yaml /var/lib/envoy/lds/lds.yaml

この変更により、下の図のように、spring-boot-demo の URL パスが /jp で始まる場合に jp-spring-boot-demo にルーティングされます。

次のコマンドでルーティングの変更を確認できます。

curl http://localhost:8080/jp

レスポンスとして、"Hello Japan" が表示されます。

上書きにより、次の lds.yaml に置換されています。

resources:
  - "@type": type.googleapis.com/envoy.config.listener.v3.Listener
    name: listener_0
    address:
      socket_address:
        address: 0.0.0.0
        port_value: 8080
    filter_chains:
      - filters:
          - name: envoy.filters.network.http_connection_manager
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
              stat_prefix: ingress_http
              route_config:
                name: local_route
                virtual_hosts:
                  - name: local_service
                    domains:
                      - '*'
                    routes:
                      - match:
                          prefix: "/jp"
                        route:
                          prefix_rewrite: "/"
                          cluster: jp
                      - match:
                          prefix: "/"
                        route:
                          cluster: us
              http_filters:
                - name: envoy.router
                  typed_config:
                    "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router

まとめ

ここで確認したように、既存 API の一部を新しいサービスにルーティングするというアプローチで Strangler Fig Pattern を実現できます。

同様なアプローチで、Feature Toggles (Feature Flags) や A/B テスト に応用することもできます。

We're hiring!

みらい翻訳では、アーキテクチャに興味のある方や技術ブログを盛り上げていただけるエンジニアを募集しています! ご興味のある方は、ぜひ下記リンクよりご応募・お問い合わせをお待ちしております。

参考

ドメイン駆動設計と「コントロールプレーン」、「データプレーン」

これは、ドメイン駆動設計(DDD) Advent Calendar 2022 9日目の記事です。

ドメイン駆動設計では、レイヤー化アーキテクチャ (Layered Architecture) でエンティティやバリューオブジェクト、ドメインイベント、ライフサイクルに関連する集約 (Aggregate)やリポジトリ (Repository)、ファクトリ (Factory) などが説明されています。

モダンな分散アーキテクチャが採用されるシステムでは、さまざまな種類のデータを処理することになります。流れるデータパケットには、単一の目的を持つものもあれば、複数の目的とライフサイクルの異なる要素を含むものもあります。

次のようなデータの例で考えてみましょう。

  1. 分散システムの一部のアプリケーションにトラフィックが集中しスケーリングをリクエストするデータ
  2. ユーザの操作に応じたデータに、ライフサイクルの異なる認証情報を同梱したデータ
  3. さまざまな分散システムからデータを集約して提供するデータ

最初に挙げたスケーリングをリクエストするデータについて考えてみます。

あるアプリケーションに流入するトラフィック量が増大したため、スケーリングを要求します。すると分散システムはそのアプリケーションをスケーリングするため、サーバー (やコンテナ) を起動して対処します。次に、起動したインスタンストラフィックが分配されるように、ロードバランサーやこのアプリケーションに依存する別のアプリケーションに増加したインスタンスのデータ (IP アドレスやポート番号等) を分散システムに流します。

ネットワークの世界では、特定の処理が行われる場所に関する抽象的な概念として「プレーン」という用語が「存在のプレーン」という意味で使用されます。

スケーリングによって、データパケットの転送を変更することになりますが、このようなプロセスは「コントロールプレーン」の一部とみなされることが一般的です。

よく知られているプレーンは、この「コントロールプレーン」と「データプレーン」の2種類あります。

例として挙げた 2 や 3 は「データプレーン」を流れるデータと言うことができます。

ドメイン駆動設計のモデリング対象に 1 のようなデータが現れることはほとんどないでしょう。おそらく意識的にか無意識のうちに、ドメインのデータではないとフィルターしていそうです。

ドメイン駆動設計は、ドメインに集中してモデリング、実装を進めることを目的としています。そのため、この「コントロールプレーン」を流れるデータを「アプリケーション」の設計と分離してモデリング、実装することへの異論はないことだろうと思います。

次に 2 のデータについて考えてみます。

現在アプリケーションの多くはブラウザやデスクトップアプリケーションの UI からインターネットを介してアクセスされます。そしてセキュリティを担保するため、ドメインに到達する前に、認証 (Authn)、認可 (Authz) の処理が実行され、条件を満たした場合にのみドメインの処理が実行されます。また、これらの処理の状況は監視、観察され「コントロールプレーン」を流れるデータを生成します。

さらに、アプリケーションが分散システム内の他のアプリケーションや外部システムと依存関係がある場合、それらのスケーリング等による変化を「コントロールプレーン」から受け取らなければならないかもしれません。具体的な例を挙げると、リレーショナルデータベースを読み書きができるプライマリと読み取り専用のレプリカで冗長化している場合に、プライマリが落ちて、レプリカがプライマリに昇格した場合など、アプリケーションはその変化を検知して、接続先をレプリカからプライマリに昇格した場所に切り替える必要があるかもしれません。

このような一般的に「横断的関心事」と呼ばれる、認証、認可や監視、観察のロギングや可観測性、ルーティング処理は、ドメインモデリングに組み込むべきでしょうか。

サービスメッシュパターンを使用すると、アプリケーションの「データプレーン」にプロキシなどのコンポーネントを配置して、このコンポーネントが「横断的関心事」を処理することで、アプリケーションはよりドメインの処理に集中できます。

最後に 3 のデータについて考えてみます。

さまざまな分散システムからデータを集めてレスポンスするだけでなく、UI から流れてきたデータを複数の分散システムに流す必要がある場合に「GraphQL」を使用することができます。これはどちらかというとフロントエンド寄りのものですが、この記事での議論は主にバックエンドを扱っているため GraphQL などをサポートするなんらかのコンポーネントからアクセスされる、ドメイン駆動設計に基づく各アプリケーションはそれぞれのドメインの処理に集中することができます。また、要件によってはデータメッシュパターンを使用することもできます。

参考

リアクティブシステムとドメイン駆動設計の学習コンテンツ

これは、ドメイン駆動設計(DDD) Advent Calendar 2022 6日目の記事です。

2022年11月28日から12月2日の間のラスベガスでの AWS イベント、re:Invent のキーノートの冒頭で非同期の重要性が説明されました。

リアクティブシステムは、非同期とイベント駆動型の特徴を有しています。そして、リアクティブシステムから最大のパフォーマンスを得ようとすれば、需要の高いコンポーネントのスケーリングが必要になり、自ずと分散化へと進みます。またコンポーネントの最小の単位は関数になります。

そうはいっても、無節操に必要な関数を追加していくというアプローチはとれません。そこから構築される分散システムはデススターのような脆弱なシステムになるでしょう (Death Star Architecture)。

強靭で変化に強い分散システムの設計には「ドメイン駆動設計 (Domain-driven design)」がよく用いられます。

複数の関数をドメインエキスパートが理解できる単位にまとめます。「境界づけられたコンテキスト (Bounded Context)」という名でよく知られる、このコンテキストを「マイクロサービス」という単位でまとめることがよく実践されています。

ドメイン駆動設計の習得にはさまざまな方法がありますが、ここでは、無料で参照できるラーニングコンテンツをいくつか紹介します。

最初に Lightbend 社の e-Learning コースです。

かなりのボリュームがありますが、次のコースを受講することで、リアクティブシステムの基礎から CQRS やイベントソーシングまで学ぶことができます。また、デジタルバッジを獲得することもできます。

次は Microsoft 社のコンテンツです。ここでもドメイン駆動設計についての知識を得ることができます。

このコンテンツの中で「ドメインレイヤー」は次のように説明されています。

永続化非依存の原則とインフラストラクチャ非依存の原則に従って、このレイヤーではデータ永続化の詳細を完全に無視する必要があります。

これは関数を設計する視点で考えると、副作用のない純粋関数としてドメインレイヤーを設計することを意味しています。

これらを習得して、ドメイン駆動設計モデルを適用して、強靭で柔軟性のある分散システムの設計に役立てられます。