NGINX.COM
Web Server Load Balancing with NGINX Plus

この記事は、Microservices July 2023: マイクロサービスのStart Delivering Microservicesの方法を実行するための4つのチュートリアルの1つです。

すべてのアプリケーションではそれを構成する作業が必要ですが、マイクロサービスの構成時には、モノリシックなアプリケーションの構成時と比べ、考慮するべき点が異なる場合があります。両方のタイプのアプリケーションのガイドラインとして、Twelve-Factor App項目3の「設定を環境変数に格納する」を参照できます。ただし、マイクロサービスではサービスの構成方法が異なり、他のサービスからの入力値に依存するサービスが含まれるため、このガイダンスを参考に必要に応じて調整する必要があります。特に、サービス構成を定義する方法、サービスへ構成情報を提供する方法、他の依存する可能性があるサービスが構成情報を取得することができるようにするためのサービスを利用できるようにする方法です。

項目3の概念をマイクロサービスに適用する方法をより深く理解するには、当社ブログの「Best Practices for Configuring Microservices Apps」もご参照ください。 この記事では、構成ファイル、データベース、およびサービスディスカバリーのベストプラクティスについて説明します。

注:このチュートリアルの目的は、いくつかの主要な概念を説明することであり、本番環境でマイクロサービスをデプロイする方法を示すことではありません。 実際の「マイクロサービス」アーキテクチャを使用していますが、重要な注意点がいくつかあります。

  • このチュートリアルでは、Kubernetes や Nomad 等のコンテナオーケストレーションフレームワークを使用していません。 これは、特定のフレームワークの詳細に入ることなく、マイクロサービスの概念について学ぶことができるようにするためです。 ここで紹介するパターンは、これらのフレームワークのいずれかを実行するシステムに適用できます。
  • 使用するサービスは、理解しやすいように最適化されています。重要なのは、コードの詳細ではなく、システム内でのサービスの役割とその通信方法に注目することです。詳細は、個別サービスのREADMEファイルをご覧ください。

チュートリアルの概要

このチュートリアルでは、項目3の考え方がマイクロサービスアプリにどのように適用されるかを説明しています。4 つの課題セクションでは、一般的なマイクロサービスデプロイパターンをいくつかを確認し、それらのパターンを使ったサービスのデプロイと構成を行います。

  • 課題1課題2では、マイクロサービスアプリの設定を格納する場所を説明する最初のパターンを見ていきます。 代表的な配置場所は3つあります。

    • アプリケーションコード
    • アプリケーションのデプロイ時に使用されるスクリプト
    • デプロイを行うスクリプトが参照する外部ソース
  • 課題3では、さらに2つのパターンを設定します。Consulによるサービス ディスカバリを利用し、リバース プロキシとして NGINX を介してアプリケーションを外部に公開します。
  • 課題4では、最終パターンを設定します。 マイクロサービスのインスタンスを通常機能とは異なる一度限りのアクションを実行する “job runner” として使用します。 (この例では、データベース移行を想定した動作を確認します)

チュートリアルは4つのテクノロジーを使用します。

  • messenger – このチュートリアル用に作成した、メッセージの保存機能を持つシンプルなチャット API
  • NGINX オープンソース – messengerサービスとシステムすべてのエントリポイント
  • Consul – 動的なサービス登録機能とキーバリューのデータストア
  • RabbitMQ – サービス間の非同期通信を可能にする人気のオープンソースメッセージブローカー

Topology diagram showing NGINX Open Source and Consul template running together in a container. Consult template communicates with the Consult client. NGINX Open Source is a reverse proxy for the messenger service, which stores data in messenger_db and communicates with Rabbit MQ.

チュートリアルの概要を理解するにはこのビデオを視聴してください。すべての手順がこの記事と完全に一致はしていませんが、この記事の考え方の理解に役立ちます。

前提条件とセットアップ

前提条件

お使いの環境でチュートリアルを完了するには、以下が必要です。

  • Linux/Unix環境
  • Linux コマンドライン、JavaScript、およびbashの基本を理解していること (初心者向けに、すべてのコードとコマンドが提供され、説明されています)
  • DockerDocker Compose
  • Node.js 19.x またはそれ以降

    • バージョン 19.x をテストしていますが、新しいバージョンの Node.js でも動作することを想定しています。
    • Node.js のインストールについての詳細は、READMEmessengerサービスリポジトリ内)を参照してください。asdfをインストールすると、コンテナで使用した Node.js と全く同じバージョンを取得することもできます。
  • curl (ほとんどのシステムではすでにインストール済み)
  • チュートリアルの概要に記載の4つのテクノロジ: messenger (次のセクションでダウンロードします)、NGINX オープンソースConsulおよびRabbitMQ

セットアップ

  1. ターミナルのセッションを開始します (以下の手順では、「app terminal」という名称で参照します)。
  2. ホームディレクトリで、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
  3. platformリポジトリに移動し、Docker Compose コマンドを実行します。

    cd platform
    docker compose up -d --build

    これで、この後の課題で使用する RabbitMQ と Consul が両方起動します。

    • -dフラグは、Docker Compose が起動したときに、コンテナからデタッチするように指示します (このフラグを指定すると、Docker Composeがバックグラウンドで実行され、ターミナルを引き続き使用できるようになります)。
    • --buildフラグは、Docker Compose が起動したときに、すべてのイメージをビルドするように指示します。これで、ファイルの新しい変更をイメージに反映させます。
  4. messengerリポジトリに移動し、Docker Compose コマンドを実行します。

    cd ../messenger
    docker compose up -d --build

    これで、messengerサービス用の PostgreSQL データベースが起動します。(以下、「messenger-database」という)

課題1: アプリケーションレベルのマイクロサービス構成を定義する

この課題では、チュートリアルで確認する3つの設定の配置場所のうち最初の配置場所を構成します。これがアプリケーションレベルです。(課題2は、2番目と3番目の配置場所であるデプロイスクリプトおよび外部ソースについて記載します。)

Twelve-Factor Appでは、アプリケーションレベルの設定は含んでおらず、これらの設定は異なる環境下(Twelve-Factor Appでは deploys と呼びます)において変更されないためです。しかし、この記事では 3 つの場所すべてをカバーし、サービスを開発、構築、およびデプロイするときに各カテゴリを管理する方法が異なることを確認します。

messengerサービスは Node.jsで記述され、app/index.mjsmessengerリポジトリ内) のエントリポイントとあわせて書き込まれています。ファイルのこの行は

app.use(express.json());

アプリケーション内部の設定の例です。タイプapplication/jsonのリクエストボディを JavaScript オブジェクトに変換するようにExpress frameworkを設定します。

このロジックはお使いのアプリケーションコードと密接に連携され、Twelve-Factor Appが “設定” と判断するものではありません。ただしソフトウェア開発では、すべてがお使いの状況によって異なることをご理解いただけると思います。

次の2つのセクションでは、このラインを修正して、アプリケーション内部の設定の2つの例を実装します。

例1

この例では、messengerサービスが受け入れるリクエストボディの最大サイズを設定します。このサイズ制限は、limit引数でexpress.json関数に設定されます。これはExpress APIドキュメントのとおりです。ここで、limit引数を Express frameworkの JSON ミドルウェアの構成に追加します( 上の説明参照)。

  1. お好みのテキストエディタで、app/index.mjsを開いて

    app.use(express.json())

    app.use(express.json({ limit: "20b" }));

    を入れ替えます。

  2. アプリケーションターミナル(セットアップで使用したもの) で、appディレクトリに移動し、messengerサービスを起動します。

    cd app
    npm install
    node index.mjs
    messenger_service listening on port 4000
  3. 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" }
  4. (再度クライアントターミナルで)やや長いメッセージ本文を送信します。今回はリクエストボディが20バイトを超えたことを示すエラーメッセージを含め、手順 3 以上の出力があります。

    curl -d '{ "text": "hello, world" }' -H "Content-Type: application/json" -X POST http://localhost:4000/conversations
    ...
    \”PayloadTooLargeError: request entity too large"

例2

この例はconvictを使用しますが、これは構成全体の “schema” を1つのファイルで定義できるライブラリです。Twelve-Factor Appの項目3 の 2 つのガイドラインも記載されています。

  • 設定を環境変数に格納 – 最大ボディサイズがアプリケーションコードでハードコード化される代わりに、環境変数を使って設定されるようにアプリケーションを修正します (JSON_BODY_LIMIT)。
  • お使いのサービス設定を明確に定義 – これはマイクロサービスの3項目の適応です。この考え方に慣れていない場合は、当社ブログでBest Practices for Configuring Microservices Appsをお読みになることをお勧めします。

この例では、課題2で利用する前提条件も設定します。その課題で作成するmessengerのデプロイスクリプトは、ここでアプリケーションコードに挿入するJSON_BODY_LIMIT環境変数を設定します。これは、デプロイスクリプトで指定された設定を説明するためのものです。

  1. 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キーを用いて、アプリケーションから変数へのアクセスを可能にします
  2. app/index.mjs

    app.use(express.json({ limit: "20b" }));

    app.use(express.json({ limit: config.get("jsonBodyLimit") }));

    を入れ替えます。

  3. 手順2の例1messengerサービスを起動したアプリケーションターミナルで、Ctrl+cを押してサービスを停止します。次にJSON_BODY_LIMIT環境変数を使って、最大ボディサイズを 27 bytesバイトに設定し、再度起動します。

    ^c
    JSON_BODY_LIMIT=27b node index.mjs

    これは、特定のユース ケースにおける設定を変更する例です。アプリケーションコードの値 (この場合はサイズ制限) をハードコーディングする代わりに、Twelve-Factor Appで推奨されているように、環境変数を設定しています。

    上で記載のとおり、課題2JSON_BODY_LIMITを環境変数で使用することは、2番目の配置場所に関する例となり、コマンドラインで設定するのではなく、messengerサービスのデプロイスクリプトで環境変数を指定する際に利用できます。

  4. クライアントターミナルで、例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" }

    クライアントターミナルを閉じます。 このチュートリアルの残りのコマンドは、アプリケーションターミナルで実行します。

  5. アプリケーションターミナルで、Ctrl+cを押してmessengerサービス (上記手順3のターミナルでサービスの停止と再度起動をしたもの) を停止します。

    ^c
  6. messenger-databaseを停止します。ネットワークがplatformリポジトリで定義されたインフラストラクチャ コンポーネントによってまだ使用されているためエラーが表示されますが、これは無視しても問題ありません。messengerリポジトリのルートディレクトリでこのコマンドを実行します。

    docker compose down
    ...failed to remove network mm_2023....

課題2: サービスのデプロイスクリプトを作成する

コンフィグとコードは厳密に区別する必要があります(そうしないと、複数のデプロイ環境間の違いをどのように管理できますか?)
– Twelve-Factor AppのFactor3から

一見するとこれは “コンフィグをソースコード管理ツールにチェックインしない”という解釈になる可能性があります。この課題では、このルールを破るように見えるかもしれませんが、実際にはルールを尊重しつつ、マイクロサービス環境に不可欠な価値あるプロセス改善を提供する、マイクロサービス環境によくあるパターンを実装します。

この課題では、マイクロサービスに設定を提供するinfrastructure-as-codeと deployment manifests の機能を真似たデプロイメントスクリプトを作成し、外部にある設定ソースを使用するようにスクリプトを修正し、シークレットを設定し、スクリプトを実行してサービスおよびそのインフラをデプロイします。

messengerリポジトリに新しく作成されたinfrastructureディレクトリにデプロイメントスクリプトを作成します。infrastructure(またはその同義の名前)と呼ばれるディレクトリは、最近のマイクロサービス・アーキテクチャでよく見られるパターンであり、以下のようなものを保存するために使用します。

このパターンのメリットは以下の通りです。

  • サービスのデプロイと、サービス固有のインフラストラクチャ(データベースなど)のデプロイの所有権を、サービスを所有するチームに割り当てることができます。
  • このチームは、これらの要素への変更が開発プロセス(コードレビュー、CIなど)を通過するようにすることができます。
  • チームは、外部チームの作業に依存することなく、サービスとそれをサポートするインフラのデプロイ方法を容易に変更することができます。

前述したように、このチュートリアルの意図は、実際のシステムをセットアップする方法を示すことではなく、また、この課題でデプロイするスクリプトは実際の本番システムに似ているわけではありません。

むしろ、マイクロサービスに関連するインフラストラクチャのデプロイメントを扱う際に、ツール固有の構成によって解決されるいくつかの主要な概念と問題を説明し、同時可能な限り特定のツール向けとならないようにスクリプトを抽象化することを目的としています。

初期デプロイメントスクリプトの作成

  1. ターミナルで、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
  2. お好みのテキストエディタで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バイトと低すぎることを正しく指摘しています。

Screenshot of code review saying a limit of 20 bytes for the message body is too small

外部のソースから設定値を取得するためのデプロイメントスクリプトの修正

このパートでは、マイクロサービスの設定の3つ目の配置場所として、デプロイ時に参照される外部のソースをセットアップしています。値を動的に登録し、デプロイ時に外部のソースから取得することは、常に更新する必要があるトラブルの原因となる値をハードコーディングするよりもはるかに良い方法です。この点については、当ブログの「Best Practices for Configuring Microservices Apps」を参照してください。

この時点で、messengerサービスに必要な補助サービスを提供するために、2つのインフラストラクチャーコンポーネントがバックグラウンドで稼働しています。

  1. RabbitMQ、実際のデプロイメントでプラットフォームチームが所有(セットアップの手順 3 で起動)
  2. messenger-database、実際のデプロイメントであなたのチームが所有(セットアップの手順4で起動)

app/config/config.mjsにあるmessengerサービスのconvictスキーマは、外部設定のこれら2つの部分に対応する必要な環境変数を定義しています。このセクションでは、messengerサービスをデプロイするときに参照できるように、一般的にアクセス可能な場所に変数の値を設定することによって、これら2つのコンポーネントをセットアップします。

RabbitMQとmessenger-databaseに必要な接続情報は、Consul Key/Value(KV)ストアに登録されており、これはすべてのサービスがデプロイされたときにアクセスできる共通の場所となっています。Consul KVストアはこの種のデータを保存する標準的な場所ではないですが、このチュートリアルでは簡略化のためにこれを使用しています。

  1. 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_USERPGPORTPGHOSTAMQPHOSTAMQPPORTの環境変数の値を Consul KV ストアから取得します。Consul KVストアの環境変数の値を次の2つの手順で設定します。
  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つの概念を示しています

    • デプロイメントスクリプトで特定の構成を直接指定します。この場合、実行するポートとデフォルト ユーザーのユーザー名を PostgreSQL データベースに伝えます。
    • 実行時にコンテナに環境変数を注入するために-eフラグを付けてDockerを実行します。また、実行中のコンテナの名前をmessenger-dbに設定します。この名前は、セットアップの手順2でplatformサービスを起動したときに作成したDockerネットワーク内のデータベースのホスト名となります。
  3. 実際のデプロイメントでは、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 内)をチェックしてください。(結論としては、シークレット管理ツールは、シークレットを保存するための唯一の真に安全な方法です)。

  1. 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"
  2. 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

デプロイメントスクリプトの実行

  1. messengerリポジトリのappディレクトリに移動し、 messengerサービス用の Docker イメージをビルドします。

    cd ../app
    docker build -t messenger .
  2. platformサービスに属するコンテナのみが実行されていることを確認します。

    docker ps --format '{{.Names}}'
    consul-server
    consul-client
    rabbitmq
  3. 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 \を元に戻してください。

  4. アプリケーションに簡単なヘルスチェックリクエストを送信し、デプロイが成功したことを確認します。

    curl localhost:4000/health
    curl: (7) Failed to connect to localhost port 4000 after 11 ms: Connection refused

    失敗しました!失敗の理由としては、まだ重要な設定の1つが抜けており、messengerサービスは外部には公開されていないのです。mm_2023ネットワーク内では正常に動作していますが、そのネットワークにはDocker内からしかアクセスできません。

  5. 次の課題で新しいイメージを作成するための準備として、実行中のコンテナを停止します。

    docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))

課題3: サービスを外部に公開します

プロダクション環境では、一般的にはサービスを直接外部に公開しません。代わりに、リバースプロキシをサービスの前段に配置する事でアクセスを制御します。こちらがマイクロサービスのサービス公開手法として一般的な手法です。

この課題では、messengerサービスを外部へ公開するためにサービスディスカバリー(新しいサービスの情報を登録し、その情報を動的に更新する役割)を設定します。サービスディスカバリは以下のコンポーネントによって構成されます。

  • Consul – ダイナミックサービスレジストリ、およびConsulデータに基づいてファイルを動的に更新するツールであるConsul テンプレート
  • NGINX オープンソース – リバースプロキシとロードバランサとして、複数のインスタンスで構成されるmessengerサービスの1つのエントリーポイントを公開する

サービスディスカバリについての詳細は、当社ブログの「Best Practices for Configuring Microservices Apps」内Making a Service Available as Configuration) を参照してください。

Consul をセットアップする

messengerリポジトリ内のapp/consul/index.mjsファイルには、Consul起動時に messengerサービスを登録、および、Consulのグレースフルシャットダウン時に登録を解除するために必要なコードがすべて含まれます。また、Consul のサービスレジストリで新しくデプロイされたサービスを登録するための機能”register”を含みます。

  1. お好みのテキストエディタで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;
  2. 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接続とサービス登録情報を含めるようにします。

  3. 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_HOSTCONSUL_PORT環境変数を提供しなければいけません。この接続情報がないと、サービスはConsulに問い合わせることが出来ないからです。

  4. アプリケーションのターミナルでappディレクトリに移動し、messengerサービスの実行中のインスタンスを全て停止後、新しいサービスレジストレーションコードを含めるためにDocker imageをリビルドします。

    cd app
    docker rm $(docker stop $(docker ps -a -q --filter ancestor=messenger --format="{{.ID}}"))
    docker build -t messenger .
  5. ブラウザでhttp://localhost:8500にアクセスすると、Consul UIが実際に動いているのを見ることができます(まだ何も面白いことは起きていないのですが)。

  6. messengerリポジトリのrootユーザとして、デプロイスクリプトを実行してmessengerサービスのインスタンスを開始します。

    CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh
  7. ブラウザの Consul UI で、ヘッダーバーの Servicesをクリックして、1つのmessengerサービスが実行されていることを確認します。

    Consul UI Services tab with one instance each of consul and messenger services

  8. デプロイスクリプトをさらに複数回実行し、messengerサービスのインスタンスをさらに起動します。Consul UIで、それらが実行されていることを確認します。

    CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-deploy.sh

    Consul UI 'messenger' tab with five instances of the service

NGINX をセットアップする

次に、リバースプロキシ及びロードバランサとしてNGINXオープンソースを追加し、全ての実行中のmessengerインスタンスの受信トラフィックをNGINXにルーティングします。

  1. アプリケーションのターミナルで、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のアップストリーム情報を動的に更新します。

  2. 手順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サービスインスタンスにプロキシします。

  3. 手順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サービスインスタンス) のトラフィックを正常に処理します。

  4. 手順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"]
  5. Docker イメージをビルドします。

    docker build -t messenger-lb .
  6. 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
  7. 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
  8. これですべての環境が整いましたので、NGINX サービスをデプロイします。

    CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-load-balancer-deploy.sh
  9. messengerサービスに外部からアクセスできるか確認します。

    curl -X GET http://localhost:8085/health
    OK

    アクセス出来ています! これで NGINXは、作成されたmessengerサービスのすべてのインスタンスにロードバランシングを行うようになりました。X-Forwarded-For ヘッダが前のセクションの手順8でConsul UIに表示されたのものと同じmessengerサービス のIPアドレスを表示していることがそれを証明しています。

課題4: サービスをJob Runnerとして利用したデータベースのマイグレーション

大規模なアプリケーションは、データ修正等の一回限りのタスクに使用出来る小さなワーカープロセスを持つ”Job Runner”を使用することがよくあります(例としてSidekiqCeleryがあります)。これらのツールは、RedisRabbitMQのような、サポートを行うインフラストラクチャが必要な場合が多いです。この場合、messengerサービスそのものを “job runner” として使い、一回限りのタスクを実行することになります。これは、messengerサービスが非常に小さく、データベースやその他の依存するインフラストラクチャと相互作用することができ、トラフィックを提供するアプリケーションから完全に分離して実行されているため、理にかなっています。

これを行うことで、3つのメリットがあります。

  1. job runner (実行されるスクリプトを含む) へ、本番サービスと全く同じチェックとレビュープロセスが実施されます。
  2. 本番環境でのデプロイをよりセキュアにするために、データベースユーザの変更を簡単に行うことができます。例えば、既存テーブルへの書き込みとクエリのみ可能な”low privelege”ユーザーで本番サービスを実行することができ、別のサービスインスタンスを構成して、テーブルの作成と削除が可能な”higher-privileged”ユーザーとしてデータベース構造を変更することができます。
  3. チームによっては、サービスの本番トラフィックを処理しているインスタンスからジョブを実行することもあります。ジョブがコンテナ内のアプリケーションが実行する他の機能に影響を与える可能性があるため、これは危険です。そのようなことを避けることが、そもそもマイクロサービスを行う理由ではないでしょうか?

この課題では、データベースの設定値をいくつか変更し、新しい値を使用するようにmessengerデータベースを移行します。 またそのパフォーマンスをテストすることによってアーティファクトがどのように修正され、新しい役割を満たせるかを模索します。

messenger データベースをマイグレーションします

本番環境では、異なる権限を持つ2人のユーザー、すなわち「アプリケーションユーザー」と「マイグレーターユーザー」を作成することがあります。この例では簡易的にデフォルトユーザーをアプリケーションユーザーとして使用し、superuser権限を持つマイグレーターユーザーを作成します。実際には、各ユーザーの役割に応じて具体的にどのような最小限の権限が必要か時間をかけて決める必要があります。

  1. アプリケーションターミナルで、superuser権限を持つPostgreSQLユーザーを作成します。

    echo "CREATE USER messenger_migrator WITH SUPERUSER PASSWORD 'migrator_password';" | docker exec -i messenger-db-primary psql -U postgres
  2. データベースデプロイスクリプト (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で設定されるユーザーにマイグレーターのユーザーを追加するだけです。

  3. 新しいファイルをmessenger-db-migrator-deploy.shという名前でinfrastructureディレクトリに作成します(ここでもchmodコマンドにsudoを付ける必要があるかもしれません)

    touch infrastructure/messenger-db-migrator-deploy.sh
    chmod +x infrastructure/messenger-db-migrator-deploy.sh
  4. 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_NAMEmessengerではなくmessenger-migratorであることと、PGUSERが上記の手順1で作成した “マイグレーター” superuserであることです。

    CONSUL_SERVICE_NAMEmessenger-migratorであることが重要です。これがmessengerに設定されている場合、 NGINXはAPIコールを受け取るためにこのサービスを自動的にローテーションさせます。

    このスクリプトは、マイグレーターの役割として短い時間だけ起動するインスタンスをデプロイします。これにより、マイグレーションに関する問題によって、メインのmessengerサービスインスタンスでのトラフィック処理に影響することを防ぎます。

  5. PostgreSQL データベースを再デプロイします。このチュートリアルではbashスクリプトを使用しているため、データベースサービスを停止および再起動する必要があります。本番環境のアプリケーションでは、一般的にinfrastructure-as-codeスクリプトを実行し、変更されたエレメントのみを追加します。

    docker stop messenger-db-primary
    CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-deploy.sh
  6. PostgreSQL データベースマイグレーターサービスをデプロイします。

    CONSUL_HOST=consul-client CONSUL_PORT=8500 ./infrastructure/messenger-db-migrator-deploy.sh
  7. インスタンスが想定どおりに動作していることを確認します。

    docker ps --format "{{.Names}}"
    ...
    messenger-migrator

    またConsulのUIで、データベースマイグレーターサービスがmessenger-migratorとして正しく登録されていることを確認できます (繰り返しますが、トラフィックを処理しないのでmessengerで登録されてはいません)

    Consul UI Services tab with one instance each of consul and messenger-migrator services, and four instances of messenger service

  8. それでは、最後の手順であるデータベースマイグレーションスクリプトを実行します! これらのスクリプトは実際のデータベースマイグレーションとは似ていませんが、代わりに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データベースのマイグレーションが完了したため、messengerサービスの動作を確認することができるようになりました! これを行うには、NGINXサービスに対していくつかの基本的なcurlクエリを実行します (NGINX は「NGINXをセットアップする」で構成済です)。

以下のコマンドの一部は、JSON形式で出力するためにjqライブラリを使用しています。必要に応じてインストールするか、コマンドラインから省略してください。

  1. 会話を作成します。

    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" }
    }
  2. 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"
      }
    }
  3. 別のユーザー(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"
      }
    }
  4. メッセージを取得します。

    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: マイクロサービスにおけるシークレット管理の基本では、マイクロサービス環境におけるシークレット管理について、詳細かつユーザーフレンドリーな概要を説明しています。

anner reading 'Microservices March 2023: Sign Up for Free, Register Today'

Hero image
NGINXクックブック 設定レシピ集(日本語版)

待望の【O'Reilly】NGINX Cookbook日本語版がついに完成!NGINXクックブックは、NGINXを最大限に活用する方法を解説しています。



著者について

About F5 NGINX

F5 NGINXについて
F5, Inc.は、人気のオープンソースプロジェクト「NGINX」を支援しています。NGINXはモダンアプリケーションを開発・構築するためのテクノロジースイートを提供しています。NGINXとF5製品との併用で、コードからユーザーまでの広範なアプリケーション領域をサポートし、マルチクラウドアプリケーションサービスとしてNetOpsとDevOps間の課題を解決します。

詳しくはnginx.co.jpをご覧ください。Twitterで@nginxをフォローして会話に参加することもできます。