この記事は、Microservices July 2023: マイクロサービスのStart Delivering Microservicesの方法を実行するための4つのチュートリアルの1つです。
すべてのアプリケーションではそれを構成する作業が必要ですが、マイクロサービスの構成時には、モノリシックなアプリケーションの構成時と比べ、考慮するべき点が異なる場合があります。両方のタイプのアプリケーションのガイドラインとして、Twelve-Factor Appの項目3の「設定を環境変数に格納する」を参照できます。ただし、マイクロサービスではサービスの構成方法が異なり、他のサービスからの入力値に依存するサービスが含まれるため、このガイダンスを参考に必要に応じて調整する必要があります。特に、サービス構成を定義する方法、サービスへ構成情報を提供する方法、他の依存する可能性があるサービスが構成情報を取得することができるようにするためのサービスを利用できるようにする方法です。
項目3の概念をマイクロサービスに適用する方法をより深く理解するには、当社ブログの「Best Practices for Configuring Microservices Apps」もご参照ください。 この記事では、構成ファイル、データベース、およびサービスディスカバリーのベストプラクティスについて説明します。
注:このチュートリアルの目的は、いくつかの主要な概念を説明することであり、本番環境でマイクロサービスをデプロイする方法を示すことではありません。 実際の「マイクロサービス」アーキテクチャを使用していますが、重要な注意点がいくつかあります。
このチュートリアルでは、項目3の考え方がマイクロサービスアプリにどのように適用されるかを説明しています。4 つの課題セクションでは、一般的なマイクロサービスデプロイパターンをいくつかを確認し、それらのパターンを使ったサービスのデプロイと構成を行います。
課題1と課題2では、マイクロサービスアプリの設定を格納する場所を説明する最初のパターンを見ていきます。 代表的な配置場所は3つあります。
チュートリアルは4つのテクノロジーを使用します。
チュートリアルの概要を理解するにはこのビデオを視聴してください。すべての手順がこの記事と完全に一致はしていませんが、この記事の考え方の理解に役立ちます。
お使いの環境でチュートリアルを完了するには、以下が必要です。
bash
の基本を理解していること (初心者向けに、すべてのコードとコマンドが提供され、説明されています)Node.js 19.x またはそれ以降
curl
(ほとんどのシステムではすでにインストール済み)ホームディレクトリで、microservices-marchディレクトリを作成し、このチュートリアル用の GitHub リポジトリをそこにcloneします。(別のディレクトリ名を使用することもできます。)
注:チュートリアル全体で、Linux コマンドラインのプロンプトは省略され、コマンドをチュートリアルにコピー&ペーストしやすくしています。チルダ (~
) はホーム ディレクトリを表します。
mkdir ~/microservices-march
cd ~/microservices-march
git clone https://github.com/microservices-march/platform.git --branch mm23-twelve-factor-start
git clone https://github.com/microservices-march/messenger.git --branch mm23-twelve-factor-start
platformリポジトリに移動し、Docker Compose コマンドを実行します。
cd platform
docker compose up -d --build
これで、この後の課題で使用する RabbitMQ と Consul が両方起動します。
-d
フラグは、Docker Compose が起動したときに、コンテナからデタッチするように指示します (このフラグを指定すると、Docker Composeがバックグラウンドで実行され、ターミナルを引き続き使用できるようになります)。--build
フラグは、Docker Compose が起動したときに、すべてのイメージをビルドするように指示します。これで、ファイルの新しい変更をイメージに反映させます。messengerリポジトリに移動し、Docker Compose コマンドを実行します。
cd ../messenger
docker compose up -d --build
これで、messengerサービス用の PostgreSQL データベースが起動します。(以下、「messenger-database」という)
この課題では、チュートリアルで確認する3つの設定の配置場所のうち最初の配置場所を構成します。これがアプリケーションレベルです。(課題2は、2番目と3番目の配置場所であるデプロイスクリプトおよび外部ソースについて記載します。)
Twelve-Factor Appでは、アプリケーションレベルの設定は含んでおらず、これらの設定は異なる環境下(Twelve-Factor Appでは deploys と呼びます)において変更されないためです。しかし、この記事では 3 つの場所すべてをカバーし、サービスを開発、構築、およびデプロイするときに各カテゴリを管理する方法が異なることを確認します。
messengerサービスは Node.jsで記述され、app/index.mjs(messengerリポジトリ内) のエントリポイントとあわせて書き込まれています。ファイルのこの行は
app.use(express.json());
アプリケーション内部の設定の例です。タイプapplication/json
のリクエストボディを JavaScript オブジェクトに変換するようにExpress frameworkを設定します。
このロジックはお使いのアプリケーションコードと密接に連携され、Twelve-Factor Appが “設定” と判断するものではありません。ただしソフトウェア開発では、すべてがお使いの状況によって異なることをご理解いただけると思います。
次の2つのセクションでは、このラインを修正して、アプリケーション内部の設定の2つの例を実装します。
この例では、messengerサービスが受け入れるリクエストボディの最大サイズを設定します。このサイズ制限は、limit
引数でexpress.json関数に設定されます。これはExpress APIドキュメントのとおりです。ここで、limit
引数を Express frameworkの JSON ミドルウェアの構成に追加します( 上の説明参照)。
お好みのテキストエディタで、app/index.mjsを開いて
app.use(express.json())
と
app.use(express.json({ limit: "20b" }));
を入れ替えます。
アプリケーションターミナル(セットアップで使用したもの) で、appディレクトリに移動し、messengerサービスを起動します。
cd app
npm install
node index.mjs
messenger_service listening on port 4000
2つ目の別のターミナルセッション (後の説明ではクライアントターミナルと呼びます) を起動し、POST
要求をmessengerサービスに送信します。エラーメッセージは、リクエストボディが手順1で設定した20バイト制限未満であるため、処理には成功したが、JSON ペイロードのコンテンツが正しくないことを示しています。
curl -d '{ "text": "hello" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations
...
{ "error": "Conversation must have 2 unique users" }
(再度クライアントターミナルで)やや長いメッセージ本文を送信します。今回はリクエストボディが20バイトを超えたことを示すエラーメッセージを含め、手順 3 以上の出力があります。
curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations
...
\”PayloadTooLargeError: request entity too large"
この例はconvict
を使用しますが、これは構成全体の “schema” を1つのファイルで定義できるライブラリです。Twelve-Factor Appの項目3 の 2 つのガイドラインも記載されています。
JSON_BODY_LIMIT
)。この例では、課題2で利用する前提条件も設定します。その課題で作成するmessengerのデプロイスクリプトは、ここでアプリケーションコードに挿入するJSON_BODY_LIMIT
環境変数を設定します。これは、デプロイスクリプトで指定された設定を説明するためのものです。
convict
構成ファイルapp/config/config.mjsを開き、amqpport
キーの後に、以下を新しいキーとして追加します。
jsonBodyLimit: {
doc: `The max size (with unit included) that will be parsed by the
JSON middleware. Unit parsing is done by the
https://www.npmjs.com/package/bytes library.
ex: "100kb"`,
format: String,
default: null,
env: "JSON_BODY_LIMIT",
},
手順3のコマンドラインで最大ボディサイズを設定する際に、convict
ライブラリはJSON_BODY_LIMIT
環境変数の値を解析し、動作を決定します。
String
)jsonBodyLimit
キーを用いて、アプリケーションから変数へのアクセスを可能にしますapp/index.mjsで
app.use(express.json({ limit: "20b" }));
と
app.use(express.json({ limit: config.get("jsonBodyLimit") }));
を入れ替えます。
手順2の例1でmessengerサービスを起動したアプリケーションターミナルで、Ctrl+c
を押してサービスを停止します。次にJSON_BODY_LIMIT
環境変数を使って、最大ボディサイズを 27 bytesバイトに設定し、再度起動します。
^c
JSON_BODY_LIMIT=27b node index.mjs
これは、特定のユース ケースにおける設定を変更する例です。アプリケーションコードの値 (この場合はサイズ制限) をハードコーディングする代わりに、Twelve-Factor Appで推奨されているように、環境変数を設定しています。
上で記載のとおり、課題2のJSON_BODY_LIMIT
を環境変数で使用することは、2番目の配置場所に関する例となり、コマンドラインで設定するのではなく、messengerサービスのデプロイスクリプトで環境変数を指定する際に利用できます。
クライアントターミナルで、例1の手順4から(大きいリクエストボディの場合に利用した)curl
コマンドを再度実行します。サイズ制限を 27 バイトに増やしたため、リクエストボディが制限を超えなくなりますが、リクエストは処理されたが JSON ペイロードの内容が正しくないというエラー メッセージが表示されます。
curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations
{ "error": "Conversation must have 2 unique users" }
クライアントターミナルを閉じます。 このチュートリアルの残りのコマンドは、アプリケーションターミナルで実行します。
アプリケーションターミナルで、Ctrl+c
を押してmessengerサービス (上記手順3のターミナルでサービスの停止と再度起動をしたもの) を停止します。
^c
messenger-databaseを停止します。ネットワークがplatformリポジトリで定義されたインフラストラクチャ コンポーネントによってまだ使用されているためエラーが表示されますが、これは無視しても問題ありません。messengerリポジトリのルートディレクトリでこのコマンドを実行します。
docker compose down
...failed to remove network mm_2023....
一見するとこれは “コンフィグをソースコード管理ツールにチェックインしない”という解釈になる可能性があります。この課題では、このルールを破るように見えるかもしれませんが、実際にはルールを尊重しつつ、マイクロサービス環境に不可欠な価値あるプロセス改善を提供する、マイクロサービス環境によくあるパターンを実装します。
この課題では、マイクロサービスに設定を提供するinfrastructure-as-codeと deployment manifests の機能を真似たデプロイメントスクリプトを作成し、外部にある設定ソースを使用するようにスクリプトを修正し、シークレットを設定し、スクリプトを実行してサービスおよびそのインフラをデプロイします。
messengerリポジトリに新しく作成されたinfrastructureディレクトリにデプロイメントスクリプトを作成します。infrastructure(またはその同義の名前)と呼ばれるディレクトリは、最近のマイクロサービス・アーキテクチャでよく見られるパターンであり、以下のようなものを保存するために使用します。
このパターンのメリットは以下の通りです。
前述したように、このチュートリアルの意図は、実際のシステムをセットアップする方法を示すことではなく、また、この課題でデプロイするスクリプトは実際の本番システムに似ているわけではありません。
むしろ、マイクロサービスに関連するインフラストラクチャのデプロイメントを扱う際に、ツール固有の構成によって解決されるいくつかの主要な概念と問題を説明し、同時可能な限り特定のツール向けとならないようにスクリプトを抽象化することを目的としています。
ターミナルで、messengerリポジトリのルートにinfrastructureディレクトリを作成し、messengerサービスとmessenger-databaseのデプロイメントスクリプトを含むファイルを作成します。(環境によっては、chmod
コマンドの前にsudo
を付ける必要があるかもしれません。
mkdir infrastructure
cd infrastructure
touch messenger-deploy.sh
chmod +x messenger-deploy.sh
touch messenger-db-deploy.sh
chmod +x messenger-db-deploy.sh
お好みのテキストエディタでmessenger-deploy.shを開き、以下を追加して、messengerサービスの初期デプロイメントスクリプトを作成します。
#!/bin/bash
set -e
JSON_BODY_LIMIT=20b
docker run \
--rm \
-e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
messenger
このスクリプトは現時点では完全なものではありませんが、いくつかのコンセプトを示しています。
docker
run
コマンドの-e
フラグを使用して、実行時にコンテナに環境変数を注入します。環境変数の値をこのように設定するのは冗長に思えるかもしれませんが、これは、このデプロイメント スクリプトがどんなに複雑になったとしても、スクリプトの一番上をざっと見て、デプロイメントにどのように設定データが提供されているかを理解することができることを意味します。
さらに実際のデプロイメントスクリプトでは、docker
run
コマンドを明示的に呼び出さないこともありますが、このサンプルスクリプトは、Kubernetesマニフェスト等によって解決される主要な問題を伝えることを目的としています。Kubernetesのようなコンテナオーケストレーションシステムを使用する場合、デプロイメントはコンテナを起動し、Kubernetesの設定ファイルから派生したアプリケーション構成をそのコンテナで利用できるようになります。したがってこのサンプルデプロイメントファイルは、Kubernetesマニフェストのようなフレームワーク固有のデプロイメントファイルと同じ役割を果たすデプロイメントスクリプトの最小版であると考えることができます。
実際の開発環境では、このファイルをソースコード管理ツールにチェックインし、コードレビューにかけることもあるでしょう。こうすることで、チームの他のメンバーがあなたの設定についてコメントする機会を得ることができ、誤った設定値が予期せぬ動作につながる事故を回避することができます。たとえばこのスクリーンショットでは、チームメンバーが、受信するJSONリクエストボディの制限値(JSON_BODY_LIMIT
で設定)が20バイトと低すぎることを正しく指摘しています。
このパートでは、マイクロサービスの設定の3つ目の配置場所として、デプロイ時に参照される外部のソースをセットアップしています。値を動的に登録し、デプロイ時に外部のソースから取得することは、常に更新する必要があるトラブルの原因となる値をハードコーディングするよりもはるかに良い方法です。この点については、当ブログの「Best Practices for Configuring Microservices Apps」を参照してください。
この時点で、messengerサービスに必要な補助サービスを提供するために、2つのインフラストラクチャーコンポーネントがバックグラウンドで稼働しています。
app/config/config.mjsにあるmessengerサービスのconvict
スキーマは、外部設定のこれら2つの部分に対応する必要な環境変数を定義しています。このセクションでは、messengerサービスをデプロイするときに参照できるように、一般的にアクセス可能な場所に変数の値を設定することによって、これら2つのコンポーネントをセットアップします。
RabbitMQとmessenger-databaseに必要な接続情報は、Consul Key/Value(KV)ストアに登録されており、これはすべてのサービスがデプロイされたときにアクセスできる共通の場所となっています。Consul KVストアはこの種のデータを保存する標準的な場所ではないですが、このチュートリアルでは簡略化のためにこれを使用しています。
infrastructure/messenger-deploy.sh(前のセクションの手順 2 で作成) のコンテンツを以下と入れ替えます。
#!/bin/bash
set -e
# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb
# Postgres database configuration by pulling information from
# the system
POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true)
PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true)
AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-port?raw=true)
docker run \
--rm \
-e NODE_ENV="${NODE_ENV}" \
-e PORT="${PORT}" \
-e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
-e PGUSER="${POSTGRES_USER}" \
-e PGPORT="${PGPORT}" \
-e PGHOST="${PGHOST}" \
-e AMQPPORT="${AMQPPORT}" \
-e AMQPHOST="${AMQPHOST}" \
messenger
このスクリプトは、2種類の構成例を示しています。
NODE_ENV
)とポート(PORT
)を設定し、JSON_BODY_LIMIT
を20Bよりさらに現実的な値である 100KB に変更します。POSTGRES_USER
、PGPORT
、PGHOST
、AMQPHOST
、AMQPPORT
の環境変数の値を Consul KV ストアから取得します。Consul KVストアの環境変数の値を次の2つの手順で設定します。messenger-db-deploy.shを開き、以下を追加して、messenger-databaseの初期デプロイメントスクリプトを作成します。
#!/bin/bash
set -e
PORT=5432
POSTGRES_USER=postgres
docker run \
-d \
--rm \
--name messenger-db \
-v db-data:/var/lib/postgresql/data/pgdata \
-e POSTGRES_USER="${POSTGRES_USER}" \
-e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
-e PGPORT="${PORT}" \
-e PGDATA=/var/lib/postgresql/data/pgdata \
--network mm_2023 \
postgres:15.1
# Register details about the database with Consul
curl -X PUT http://localhost:8500/v1/kv/messenger-db-port \
-H "Content-Type: application/json" \
-d "${PORT}"
curl -X PUT http://localhost:8500/v1/kv/messenger-db-host \
-H "Content-Type: application/json" \
-d 'messenger-db' # This matches the "--name" flag above
# (the hostname)
curl -X PUT http://localhost:8500/v1/kv/messenger-db-application-user \
-H "Content-Type: application/json" \
-d "${POSTGRES_USER}"
このスクリプトは、デプロイ時にmessengerサービスから参照できる設定を定義することに加えて、「初期デプロイメントスクリプトの作成」にあるmessengerサービスの初期スクリプトと同じ2つの概念を示しています
-e
フラグを付けてDockerを実行します。また、実行中のコンテナの名前をmessenger-dbに設定します。この名前は、セットアップの手順2でplatformサービスを起動したときに作成したDockerネットワーク内のデータベースのホスト名となります。実際のデプロイメントでは、messengerリポジトリmessenger-databaseの場合と同様に、platformリポジトリの RabbitMQ のようなサービスのデプロイとメンテナンスを処理するのは、通常はプラットフォーム チーム (または同様のチーム) です。 その後プラットフォーム チームは、そのインフラストラクチャの場所がそれに依存するサービスによって検出可能であることを確認します。 チュートリアルの目的に合わせ、RabbitMQ の値を自分で設定します。
curl -X PUT --silent --output /dev/null --show-error --fail \
-H "Content-Type: application/json" \
-d "rabbitmq" \
http://localhost:8500/v1/kv/amqp-host
curl -X PUT --silent --output /dev/null --show-error --fail \
-H "Content-Type: application/json" \
-d "5672" \
http://localhost:8500/v1/kv/amqp-port
(なぜRabbitMQの変数の定義にamqp
が使われているのか不思議に思うかもしれませんが、それはAMQPがRabbitMQで使用されるプロトコルだからです)。
messengerサービスのデプロイメントスクリプトに1つの(重要な)データが欠落しています。それは、messenger-databaseのパスワードです!
注: シークレットの管理はこのチュートリアルの焦点ではないため、簡単にするために、シークレットをデプロイメントファイルで定義しています。実際の環境 (開発、テスト、または本番環境) でこれを行わないでください。大きなセキュリティ リスクが生じます。
適切なシークレット管理についての詳細は、Unit2:マイクロサービスにおけるシークレット管理の基本(Microservices July 2023 内)をチェックしてください。(結論としては、シークレット管理ツールは、シークレットを保存するための唯一の真に安全な方法です)。
infrastructure/messenger-db-deploy.shの内容を次のように置き換えて、messenger-databaseのパスワードシークレットを Consul KV ストアに保存します。
#!/bin/bash
set -e
PORT=5432
POSTGRES_USER=postgres
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
POSTGRES_PASSWORD=postgres
docker run \
--rm \
--name messenger-db-primary \
-d \
-v db-data:/var/lib/postgresql/data/pgdata \
-e POSTGRES_USER="${POSTGRES_USER}" \
-e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
-e PGPORT="${PORT}" \
-e PGDATA=/var/lib/postgresql/data/pgdata \
--network mm_2023 \
postgres:15.1
echo "Register key messenger-db-port\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-port \
-H "Content-Type: application/json" \
-d "${PORT}"
echo "Register key messenger-db-host\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-host \
-H "Content-Type: application/json" \
-d 'messenger-db-primary' # This matches the "--name" flag above
# which for our setup means the hostname
echo "Register key messenger-db-application-user\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-application-user \
-H "Content-Type: application/json" \
-d "${POSTGRES_USER}"
echo "Register key messenger-db-password-never-do-this\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \
-H "Content-Type: application/json" \
-d "${POSTGRES_PASSWORD}"
printf "\nDone registering postgres details with Consul\n"
infrastructure/messenger-deploy.shの内容を次のように置き換えて、Consul KVストアからmessenger-databaseのパスワードシークレットを取得します。
#!/bin/bash
set -e
# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb
# Postgres database configuration by pulling information from
# the system
POSTGRES_USER=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-application-user?raw=true)
PGPORT=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-port?raw=true)
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
PGPASSWORD=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true)
# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET http://localhost:8500/v1/kv/amqp-host?raw=true)
AMQPPORT=$(curl -X GET http://localhost:8500/v1/kv/amqp-port?raw=true)
docker run \
--rm \
-d \
-e NODE_ENV="${NODE_ENV}" \
-e PORT="${PORT}" \
-e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
-e PGUSER="${POSTGRES_USER}" \
-e PGPORT="${PGPORT}" \
-e PGHOST="${PGHOST}" \
-e PGPASSWORD="${PGPASSWORD}" \
-e AMQPPORT="${AMQPPORT}" \
-e AMQPHOST="${AMQPHOST}" \
--network mm_2023 \
messenger
messengerリポジトリのappディレクトリに移動し、 messengerサービス用の Docker イメージをビルドします。
cd ../app
docker build -t messenger .
platformサービスに属するコンテナのみが実行されていることを確認します。
docker ps --format '{{.Names}}'
consul-server
consul-client
rabbitmq
messengerリポジトリのルートに移動し、messenger-databaseおよびmessengerサービスをデプロイします。
cd ..
./infrastructure/messenger-db-deploy.sh
./infrastructure/messenger-deploy.sh
messenger-db-deploy.shスクリプトがmessenger-databaseを起動し、システム(ここではConsul KVストア)に適切な情報を登録します。
その後、messenger-deploy.shスクリプトがアプリケーションを起動し、messenger-db-deploy.shで登録した設定をシステム(ここでもConsul KVストア)から取得します。
ヒント: コンテナの起動に失敗した場合は、デプロイメントスクリプトのdocker
run
コマンドの2番目のパラメータ(-d
\
)を削除して、スクリプトを再度実行してください。コンテナがフォアグラウンドで起動するため、ターミナルにログが表示され、問題を特定できる可能性があります。問題が解決したら、実際のコンテナがバックグラウンドで実行されるように、-d
\
を元に戻してください。
アプリケーションに簡単なヘルスチェックリクエストを送信し、デプロイが成功したことを確認します。
curl localhost:4000/health
curl: (7) Failed to connect to localhost port 4000 after 11 ms: Connection refused
失敗しました!失敗の理由としては、まだ重要な設定の1つが抜けており、messengerサービスは外部には公開されていないのです。mm_2023ネットワーク内では正常に動作していますが、そのネットワークにはDocker内からしかアクセスできません。
次の課題で新しいイメージを作成するための準備として、実行中のコンテナを停止します。
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))
プロダクション環境では、一般的にはサービスを直接外部に公開しません。代わりに、リバースプロキシをサービスの前段に配置する事でアクセスを制御します。こちらがマイクロサービスのサービス公開手法として一般的な手法です。
この課題では、messengerサービスを外部へ公開するためにサービスディスカバリー(新しいサービスの情報を登録し、その情報を動的に更新する役割)を設定します。サービスディスカバリは以下のコンポーネントによって構成されます。
サービスディスカバリについての詳細は、当社ブログの「Best Practices for Configuring Microservices Apps」内Making a Service Available as Configuration) を参照してください。
messengerリポジトリ内のapp/consul/index.mjsファイルには、Consul起動時に messengerサービスを登録、および、Consulのグレースフルシャットダウン時に登録を解除するために必要なコードがすべて含まれます。また、Consul のサービスレジストリで新しくデプロイされたサービスを登録するための機能”register
”を含みます。
お好みのテキストエディタでapp/index.mjsを開き、register
機能をapp/consul/index.mjsからimport
するために以下のスニペットを追加します。
import { register as registerConsul } from "./consul/index.mjs";
次に、スクリプトの最後にあるSERVER
START
セクションを以下のように変更し、アプリケーションが起動した後にregisterConsul()
を呼び出すようにします。
/* =================
SERVER START
================== */
app.listen(port, async () => {
console.log(`messenger_service listening on port ${port}`);
registerConsul();
});
export default app;
app/config/config.mjs内のconvict
スキーマを開き、例2の手順1で追加したjsonBodyLimit
キーの後に以下の値を追加します。
consulServiceName: {
doc: "The name by which the service is registered in Consul. If not specified, the service is not registered",
format: "*",
default: null,
env: "CONSUL_SERVICE_NAME",
},
consulHost: {
doc: "The host where the Consul client runs",
format: String,
default: "consul-client",
env: "CONSUL_HOST",
},
consulPort: {
doc: "The port for the Consul client",
format: "port",
default: 8500,
env: "CONSUL_PORT",
},
この設定により新しいサービスが登録された時の名前、及びConsulクライアントのホスト名とポート番号が定義されます。次の手順では、messengerサービスのデプロイスクリプトを修正して、この新しいConsul接続とサービス登録情報を含めるようにします。
infrastructure/messenger-deploy.shを開き、その内容を以下のものと置き換えて下さい。前の手順で設定したConsulコネクションとサービス登録に関する情報をmessengerサービスの構成に含めます。
#!/bin/bash
set -e
# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb
CONSUL_SERVICE_NAME="messenger"
# Consul host and port are included in each host since we
# cannot query Consul until we know them
CONSUL_HOST="${CONSUL_HOST}"
CONSUL_PORT="${CONSUL_PORT}"
# Postgres database configuration by pulling information from
# the system
POSTGRES_USER=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-application-user?raw=true")
PGPORT=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-port?raw=true")
PGHOST=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-host?raw=true")
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
PGPASSWORD=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-password-never-do-this?raw=true")
# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET "http://localhost:8500/v1/kv/amqp-host?raw=true")
AMQPPORT=$(curl -X GET "http://localhost:8500/v1/kv/amqp-port?raw=true")
docker run \
--rm \
-d \
-e NODE_ENV="${NODE_ENV}" \
-e PORT="${PORT}" \
-e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
-e PGUSER="${POSTGRES_USER}" \
-e PGPORT="${PGPORT}" \
-e PGHOST="${PGHOST}" \
-e PGPASSWORD="${PGPASSWORD}" \
-e AMQPPORT="${AMQPPORT}" \
-e AMQPHOST="${AMQPHOST}" \
-e CONSUL_HOST="${CONSUL_HOST}" \
-e CONSUL_PORT="${CONSUL_PORT}" \
-e CONSUL_SERVICE_NAME="${CONSUL_SERVICE_NAME}" \
--network mm_2023 \
messenger
主な注意点:
CONSUL_SERVICE_NAME
には、messengerサービスインスタンスがConsulに登録する際に使用する名前が入ります。CONSUL_HOST
およびCONSUL_PORT
には、デプロイスクリプト実行時に動作する Consulクライアントの値が入ります。注:実環境でサービスのデプロイを行う場合、これはチーム間で合意が必要な設定例です – Consul担当チームは、すべての環境でCONSUL_HOST
とCONSUL_PORT
環境変数を提供しなければいけません。この接続情報がないと、サービスはConsulに問い合わせることが出来ないからです。
アプリケーションのターミナルでappディレクトリに移動し、messengerサービスの実行中のインスタンスを全て停止後、新しいサービスレジストレーションコードを含めるためにDocker imageをリビルドします。
cd app
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))
docker build -t messenger .
ブラウザでhttp://localhost:8500にアクセスすると、Consul UIが実際に動いているのを見ることができます(まだ何も面白いことは起きていないのですが)。
messengerリポジトリのrootユーザとして、デプロイスクリプトを実行してmessengerサービスのインスタンスを開始します。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
ブラウザの Consul UI で、ヘッダーバーの Servicesをクリックして、1つのmessengerサービスが実行されていることを確認します。
デプロイスクリプトをさらに複数回実行し、messengerサービスのインスタンスをさらに起動します。Consul UIで、それらが実行されていることを確認します。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
次に、リバースプロキシ及びロードバランサとしてNGINXオープンソースを追加し、全ての実行中のmessengerインスタンスの受信トラフィックをNGINXにルーティングします。
アプリケーションのターミナルで、rootユーザとしてmessengerリポジトリにディレクトリを移動しload-balancerと呼ばれるディレクトリと3つのファイルを作成します。
mkdir load-balancer
cd load-balancer
touch nginx.ctmpl
touch consul-template-config.hcl
touch Dockerfile
Dockerfileは、NGINXとConsulテンプレートが実行されるコンテナを定義します。Consulテンプレートは他の2つのファイルを使って、サービスレジストリでmessengerサービスが変更(サービスインスタンスの立ち上げや停止)されたときに、NGINXのアップストリーム情報を動的に更新します。
手順1で作成したnginx.ctmplファイルを開き、以下の NGINX 設定のスニペットを追加します。これは Consul テンプレートが NGINX のアップストリームグループを動的に更新する際に使用されます。
upstream messenger_service {
{{- range service "messenger" }}
server {{ .Address }}:{{ .Port }};
{{- end }}
}
server {
listen 8085;
server_name localhost;
location / {
proxy_pass http://messenger_service;
add_header Upstream-Host $upstream_addr;
}
}
このスニペットは、Consulに登録されている各messengerサービスインスタンスのIPアドレスとポート番号を、NGINX messenger_serviceグループに追加します。NGINXは、受信したリクエストを動的に定義されたupstreamサービスインスタンスにプロキシします。
手順1で作成したconsul-template-config.hclファイルを開き、以下の設定を追加します。
consul {
address = "consul-client:8500"
retry {
enabled = true
attempts = 12
backoff = "250ms"
}
}
template {
source = "/usr/templates/nginx.ctmpl"
destination = "/etc/nginx/conf.d/default.conf"
perms = 0600
command = "if [ -e /var/run/nginx.pid ]; then nginx -s reload; else nginx; fi"
}
このConsul テンプレート用の設定は、source
テンプレート (前の手順で作成した NGINX スニペット) を再作成し、指定されたdestination
に配置し、最後に指定されたcommand
(NGINXに設定の再読み込みを実施)を実行するように指示します。
これはサービスインスタンスがConsulに登録、更新、または登録解除されるたびに、新しいdefault.confファイルが作成されることを意味します。 NGINXはその後、ダウンタイムなしで設定を再読み込みし、サーバー郡 (messengerサービスインスタンス) のトラフィックを正常に処理します。
手順1で作成したDockerfileファイルを開き、NGINXサービスをビルドするための以下の内容を追加します。(このチュートリアルの目的上、Dockerfileを理解する必要はありませんが、便宜上インラインでコードを記述しています。)
FROM nginx:1.23.1
ARG CONSUL_TEMPLATE_VERSION=0.30.0
# Set an environment variable for the location of the Consul
# cluster. By default, it tries to resolve to consul-client:8500
# which is the behavior if Consul is running as a container in the
# same host and linked to this NGINX container (with the alias
# consul, of course). But this environment variable can also be
# overridden as the container starts if we want to resolve to
# another address.
ENV CONSUL_URL consul-client:8500
# Download the specified version of Consul template
ADD https://releases.hashicorp.com/consul-template/${CONSUL_TEMPLATE_VERSION}/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip /tmp
RUN apt-get update \
&& apt-get install -y --no-install-recommends dumb-init unzip \
&& unzip /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip -d /usr/local/bin \
&& rm -rf /tmp/consul-template_${CONSUL_TEMPLATE_VERSION}_linux_amd64.zip
COPY consul-template-config.hcl ./consul-template-config.hcl
COPY nginx.ctmpl /usr/templates/nginx.ctmpl
EXPOSE 8085
STOPSIGNAL SIGQUIT
CMD ["dumb-init", "consul-template", "-config=consul-template-config.hcl"]
Docker イメージをビルドします。
docker build -t messenger-lb .
messengerディレクトリにrootユーザで移動し、NGINXサービスのデプロイファイルとしてmessenger-load-balancer-deploy.shというファイルを作成します。(チュートリアルを通してデプロイした他のサービスと同様です)。ご利用の環境によっては、chmod
コマンドにsudo
を付ける必要があるかもしれません。
cd ..
touch infrastructure/messenger-load-balancer-deploy.sh
chmod +x infrastructure/messenger-load-balancer-deploy.sh
messenger-load-balancer-deploy.shを開き、以下のコンテンツを追加します。
#!/bin/bash
set -e
# Consul host and port are included in each host since we
# cannot query Consul until we know them
CONSUL_HOST="${CONSUL_HOST}"
CONSUL_PORT="${CONSUL_PORT}"
docker run \
--rm \
-d \
--name messenger-lb \
-e CONSUL_URL="${CONSUL_HOST}:${CONSUL_PORT}" \
-p 8085:8085 \
--network mm_2023 \
messenger-lb
これですべての環境が整いましたので、NGINX サービスをデプロイします。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-load-balancer-deploy.sh
messengerサービスに外部からアクセスできるか確認します。
curl -X GET http://localhost:8085/health
OK
アクセス出来ています! これで NGINXは、作成されたmessengerサービスのすべてのインスタンスにロードバランシングを行うようになりました。X-Forwarded-For
ヘッダが前のセクションの手順8でConsul UIに表示されたのものと同じmessengerサービス のIPアドレスを表示していることがそれを証明しています。
大規模なアプリケーションは、データ修正等の一回限りのタスクに使用出来る小さなワーカープロセスを持つ”Job Runner”を使用することがよくあります(例としてSidekiqやCeleryがあります)。これらのツールは、RedisやRabbitMQのような、サポートを行うインフラストラクチャが必要な場合が多いです。この場合、messengerサービスそのものを “job runner” として使い、一回限りのタスクを実行することになります。これは、messengerサービスが非常に小さく、データベースやその他の依存するインフラストラクチャと相互作用することができ、トラフィックを提供するアプリケーションから完全に分離して実行されているため、理にかなっています。
これを行うことで、3つのメリットがあります。
この課題では、データベースの設定値をいくつか変更し、新しい値を使用するようにmessengerデータベースを移行します。 またそのパフォーマンスをテストすることによってアーティファクトがどのように修正され、新しい役割を満たせるかを模索します。
本番環境では、異なる権限を持つ2人のユーザー、すなわち「アプリケーションユーザー」と「マイグレーターユーザー」を作成することがあります。この例では簡易的にデフォルトユーザーをアプリケーションユーザーとして使用し、superuser権限を持つマイグレーターユーザーを作成します。実際には、各ユーザーの役割に応じて具体的にどのような最小限の権限が必要か時間をかけて決める必要があります。
アプリケーションターミナルで、superuser権限を持つPostgreSQLユーザーを作成します。
echo "CREATE USER messenger_migrator WITH SUPERUSER PASSWORD 'migrator_password';" | docker exec -i messenger-db-primary psql -U postgres
データベースデプロイスクリプト (infrastructure/messenger-db-deploy.sh) を開き、新しいユーザーの認証情報を追加するためにその内容を置き換えます。
注: 実環境でのデプロイ時には、データベースの認証等のような情報はデプロイ用スクリプトの中には置かず、またシークレット管理ツール以外の場所に保存しないようにしてください。詳細は、Microservices July 2023のUnit2:マイクロサービスにおけるシークレット管理の基本を参照してください。
#!/bin/bash
set -e
PORT=5432
POSTGRES_USER=postgres
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
# Because we’re focusing on other concepts in this tutorial, we
# set the password this way here for convenience.
POSTGRES_PASSWORD=postgres
# Migration user
POSTGRES_MIGRATOR_USER=messenger_migrator
# NOTE: As above, never do this in a real deployment.
POSTGRES_MIGRATOR_PASSWORD=migrator_password
docker run \
--rm \
--name messenger-db-primary \
-d \
-v db-data:/var/lib/postgresql/data/pgdata \
-e POSTGRES_USER="${POSTGRES_USER}" \
-e POSTGRES_PASSWORD="${POSTGRES_PASSWORD}" \
-e PGPORT="${PORT}" \
-e PGDATA=/var/lib/postgresql/data/pgdata \
--network mm_2023 \
postgres:15.1
echo "Register key messenger-db-port\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-port \
-H "Content-Type: application/json" \
-d "${PORT}"
echo "Register key messenger-db-host\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-host \
-H "Content-Type: application/json" \
-d 'messenger-db-primary' # This matches the "--name" flag above
# which for our setup means the hostname
echo "Register key messenger-db-application-user\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-application-user \
-H "Content-Type: application/json" \
-d "${POSTGRES_USER}"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-password-never-do-this \
-H "Content-Type: application/json" \
-d "${POSTGRES_PASSWORD}"
echo "Register key messenger-db-application-user\n"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-migrator-user \
-H "Content-Type: application/json" \
-d "${POSTGRES_MIGRATOR_USER}"
curl -X PUT --silent --output /dev/null --show-error --fail http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this \
-H "Content-Type: application/json" \
-d "${POSTGRES_MIGRATOR_PASSWORD}"
printf "\nDone registering postgres details with Consul\n"
この変更は、データベースのデプロイ後に、Consulで設定されるユーザーにマイグレーターのユーザーを追加するだけです。
新しいファイルをmessenger-db-migrator-deploy.shという名前でinfrastructureディレクトリに作成します(ここでもchmod
コマンドにsudo
を付ける必要があるかもしれません)
touch infrastructure/messenger-db-migrator-deploy.sh
chmod +x infrastructure/messenger-db-migrator-deploy.sh
messenger-db-migrator-deploy.shを開き、以下を追加します。
#!/bin/bash
set -e
# This configuration requires a new commit to change
NODE_ENV=production
PORT=4000
JSON_BODY_LIMIT=100kb
CONSUL_SERVICE_NAME="messenger-migrator"
# Consul host and port are included in each host since we
# cannot query Consul until we know them
CONSUL_HOST="${CONSUL_HOST}"
CONSUL_PORT="${CONSUL_PORT}"
# Get the migrator user name and password
POSTGRES_USER=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-migrator-user?raw=true")
PGPORT=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-port?raw=true")
PGHOST=$(curl -X GET http://localhost:8500/v1/kv/messenger-db-host?raw=true)
# NOTE: Never do this in a real-world deployment. Store passwords
# only in an encrypted secrets store.
PGPASSWORD=$(curl -X GET "http://localhost:8500/v1/kv/messenger-db-migrator-password-never-do-this?raw=true")
# RabbitMQ configuration by pulling from the system
AMQPHOST=$(curl -X GET "http://localhost:8500/v1/kv/amqp-host?raw=true")
AMQPPORT=$(curl -X GET "http://localhost:8500/v1/kv/amqp-port?raw=true")
docker run \--rm \
-d \
--name messenger-migrator \
-e NODE_ENV="${NODE_ENV}" \
-e PORT="${PORT}" \
-e JSON_BODY_LIMIT="${JSON_BODY_LIMIT}" \
-e PGUSER="${POSTGRES_USER}" \
-e PGPORT="${PGPORT}" \
-e PGHOST="${PGHOST}" \
-e PGPASSWORD="${PGPASSWORD}" \
-e AMQPPORT="${AMQPPORT}" \
-e AMQPHOST="${AMQPHOST}" \
-e CONSUL_HOST="${CONSUL_HOST}" \
-e CONSUL_PORT="${CONSUL_PORT}" \
-e CONSUL_SERVICE_NAME="${CONSUL_SERVICE_NAME}" \
--network mm_2023 \
messenger
このスクリプトは、「Consulをセットアップする」の手順3で作成したinfrastructure/messenger-deploy.shスクリプトの最終形とよく似ています。主な違いは、CONSUL_SERVICE_NAME
がmessenger
ではなくmessenger-migrator
であることと、PGUSER
が上記の手順1で作成した “マイグレーター” superuserであることです。
CONSUL_SERVICE_NAME
がmessenger-migrator
であることが重要です。これがmessenger
に設定されている場合、 NGINXはAPIコールを受け取るためにこのサービスを自動的にローテーションさせます。
このスクリプトは、マイグレーターの役割として短い時間だけ起動するインスタンスをデプロイします。これにより、マイグレーションに関する問題によって、メインのmessengerサービスインスタンスでのトラフィック処理に影響することを防ぎます。
PostgreSQL データベースを再デプロイします。このチュートリアルではbash
スクリプトを使用しているため、データベースサービスを停止および再起動する必要があります。本番環境のアプリケーションでは、一般的にinfrastructure-as-codeスクリプトを実行し、変更されたエレメントのみを追加します。
docker stop messenger-db-primary
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-deploy.sh
PostgreSQL データベースマイグレーターサービスをデプロイします。
CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-migrator-deploy.sh
インスタンスが想定どおりに動作していることを確認します。
docker ps --format "{{.Names}}"
...
messenger-migrator
またConsulのUIで、データベースマイグレーターサービスがmessenger-migratorとして正しく登録されていることを確認できます (繰り返しますが、トラフィックを処理しないのでmessengerで登録されてはいません)
それでは、最後の手順であるデータベースマイグレーションスクリプトを実行します! これらのスクリプトは実際のデータベースマイグレーションとは似ていませんが、代わりにmessenger-migrator サービスがデータベースに関する処理を行うスクリプトとして実行されます。データベースのマイグレーションが完了したら、messenger-migratorサービスを停止します。
docker exec -i -e PGDATABASE=postgres -e CREATE_DB_NAME=messenger messenger-migrator node scripts/create-db.mjs
docker exec -i messenger-migrator node scripts/create-schema.mjs
docker exec -i messenger-migrator node scripts/create-seed-data.mjs
docker stop messenger-migrator
これでmessengerデータベースのマイグレーションが完了したため、messengerサービスの動作を確認することができるようになりました! これを行うには、NGINXサービスに対していくつかの基本的なcurl
クエリを実行します (NGINX は「NGINXをセットアップする」で構成済です)。
以下のコマンドの一部は、JSON形式で出力するためにjq
ライブラリを使用しています。必要に応じてインストールするか、コマンドラインから省略してください。
会話を作成します。
curl -d '{"participant_ids": [1, 2]}' -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations'
{
"conversation": { "id": "1", "inserted_at": "YYYY-MM-DDT06:41:59.000Z" }
}
ID 1のユーザーから会話にメッセージを送信します。
curl -d '{"content": "This is the first message"}' -H "User-Id: 1" -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations/1/messages' | jq
{
"message": {
"id": "1",
"content": "This is the first message",
"index": 1,
"user_id": 1,
"username": "James Blanderphone",
"conversation_id": 1,
"inserted_at": "YYYY-MM-DDT06:42:15.000Z"
}
}
別のユーザー(ID 2)からメッセージを返信します。
curl -d '{"content": "This is the second message"}' -H "User-Id: 2" -H "Content-Type: application/json" -X POST 'http://localhost:8085/conversations/1/messages' | jq
{
"message": {
"id": "2",
"content": "This is the second message",
"index": 2,
"user_id": 2,
"username": "Normalavian Ropetoter",
"conversation_id": 1,
"inserted_at": "YYYY-MM-DDT06:42:25.000Z"
}
}
メッセージを取得します。
curl -X GET 'http://localhost:8085/conversations/1/messages' | jq
{
"messages": [
{
"id": "1",
"content": "This is the first message",
"user_id": "1",
"channel_id": "1",
"index": "1",
"inserted_at": "YYYY-MM-DDT06:42:15.000Z",
"username": "James Blanderphone"
},
{
"id": "2",
"content": "This is the second message",
"user_id": "2",
"channel_id": "1",
"index": "2",
"inserted_at": "YYYY-MM-DDT06:42:25.000Z",
"username": "Normalavian Ropetoter"
}
]
}
このチュートリアルではコンテナとイメージを数多く作成しました! 以下のコマンドを使用して、保持したくないDockerコンテナとイメージを削除してください。
実行中の Dockerコンテナを削除するには
docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))
docker rm $(docker stop messenger-db-primary)
docker rm $(docker stop messenger-lb)
platformサービスを削除するには
# From the platform repository
docker compose down
チュートリアルで使用した Docker イメージをすべて削除するには
docker rmi messenger
docker rmi messenger-lb
docker rmi postgres:15.1
docker rmi hashicorp/consul:1.14.4
docker rmi rabbitmq:3.11.4-management-alpine
“こんな簡単なものをセットアップするのに、大変そうだ”と思われるかもしれません。 マイクロサービス中心のアーキテクチャに移行するには、サービスをどのように構成して設定するかについて、細心の注意が必要です。しかし、このような複雑な作業にもかかわらず、あなたは着実に前進しています。
マイクロサービスの学習を継続したい場合、Microservices July 2023 の他の項目もチェックしてください。ユニット2: マイクロサービスにおけるシークレット管理の基本では、マイクロサービス環境におけるシークレット管理について、詳細かつユーザーフレンドリーな概要を説明しています。
"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."