ECS on FargateでHyperledger Ariesをコンテナオーケストレートする

こんにちは。GMOグローバルサイン・ホールディングスCTO室で分散型IDの研究開発をしている神沼(@t_kanuma)です。

この記事ではタイトルの通り、AWSのコンテナオーケストレーションサービスにおけるコントロールプレーンの1種であるECS(Elastic Container Serivce)、そしてデータプレーンの1種でサーバレス型のFargateに対し、Hyperledger Ariesフレームワークの1種であるACA-Py(Aries Cloud Agent – Python)を冗長構成で構築してみます。

Hyperledger AriesおよびACA-Pyについての詳細は省きますが、端的にいうとACA-PyはDockerコンテナ上のPythonプロセスです。詳細は拙著ではありますが、以下の記事をご参照ください。

環境

AWS環境図

概要

上から二つのAZがIssuer用です。一番下のAZがHolderとLedger用です。AZを分けた理由は分かりやすくするためで他に理由はありません。

AZに跨ったACA-Pyコンテナの前方にALB(アプリケーションロードバランサー)を、後方にWalletとしてRDS(PostgresSQL)を配置する構成です。

動作検証内容

ECS on FargateでコンテナオーケストレートしたACA-Pyクラスタ(Issuer役)と、同じVPCに建てたEC2インスタンス上のACA-Py(Holder役)間において、インターネット越しに、コネクションの作成とそこからのメッセージの交換が機能するかどうかを検証します。

流れ

大まかな流れです。

  1. ネットワーク構築(VPC周り)
  2. Holder役のACA-PyとIndy Ledgerの構築
  3. Wallet DB構築(RDS/PostgreSQL)
  4. コンテナリポジトリ構築(ECR)
  5. オーケストレータ構築(ECS on Fargate)
  6. 動作検証

では、構築を始めます。

1. ネットワーク構築(VPC周り)

詳細は省きますが、上記環境図のように、VPC、Subnet、Security Group、Route Table、Internet GatewayおよびNAT Gatewayを整備します。

2. Holder役のACA-PyとIndy Ledgerの構築

上記環境図の一番下のAZの中を構築していきます。

Indy Ledgerの構築

Indy Ledger用のEC2インスタンスを用意して、そこにdockerおよびdocker composeをインストールします。そこから開発用のIndy Ledgerであるvon-networkをインストールします。インストールと起動方法については、以下の拙著をご参照ください。

ポイントは、Security Groupで9000,9702,9704,9706,9708のポートを開けておくことです。9000はGenesis Fileという特別なデータを取得するためのポート、その他は4つのIndy Nodeそれぞれのクライアント用ポートです。

Holder役ACA-Pyの構築

Indy Ledgerと同様に、Holder役ACA-Py用のEC2インスタンスを用意して、そこにdockerおよびdocker composeをインストールします。そこからACA-Pyコンテナをgit cloneして起動します。起動方法などは前述の拙著をご参照ください。

ご参考までに今回は以下のdocker-compose.ymlと.envファイルを用意しました。

# docker-compose.yml

version: '3.8'
services:
  acapy:
    build:
      context: ../..
      dockerfile: ./docker/Dockerfile.run
    depends_on:
      - db
    ports:
      - "${DIDCOMM_PORT}:${DIDCOMM_PORT}"
      - "${ADMIN_API_PORT}:${ADMIN_API_PORT}"
    entrypoint: /bin/bash
    command: [
      "-c",
      "aca-py  
      start 
      --label ${LABEL} 
      --inbound-transport http '0.0.0.0' ${DIDCOMM_PORT} 
      --endpoint ${DIDCOMM_ENDPOINT} 
      --outbound-transport http 
      --admin '0.0.0.0' ${ADMIN_API_PORT} 
      --admin-insecure-mode 
      --auto-ping-connection 
      --auto-respond-messages 
      --auto-accept-invites 
      --auto-accept-requests 
      --preserve-exchange-records 
      --wallet-type indy 
      --wallet-name ${WALLET_NAME} 
      --wallet-key ${WALLET_KEY} 
      --wallet-storage-type postgres_storage 
      --wallet-storage-config '{"url":"db:${DB_PORT}","wallet_scheme":"DatabasePerWallet"}' 
      --wallet-storage-creds '{"account":"${DB_ACCOUNT_NAME}","password":"${DB_ACCOUNT_PASSWORD}","admin_account":"${DB_ACCOUNT_NAME}","admin_password":"${DB_ACCOUNT_PASSWORD}"}' 
      --wallet-local-did 
      --auto-provision 
      --genesis-url ${LEDGER_URL}/genesis 
      --log-file /home/indy/logs/acapy.log 
      --log-level ${LOG_LEVEL}",
    ]
  db:
    image: postgres:latest
    environment:
      POSTGRES_USER: ${DB_ACCOUNT_NAME}
      POSTGRES_PASSWORD: ${DB_ACCOUNT_PASSWORD}
    ports:
      - "${DB_PORT}:${DB_PORT}"
# .env

COMPOSE_PROJECT_NAME=holder-dev

LABEL=holder-dev
DIDCOMM_ENDPOINT=http://<Holder用EC2インスタンスのPublic IP>:8030
DIDCOMM_PORT=8030
ADMIN_API_PORT=8031
WALLET_NAME=holder-wallet-dev
WALLET_KEY=thisissecret
LEDGER_URL=http://<Indy Ledger用EC2インスタンスのPrivate IP>:9000
LOG_LEVEL=debug

DB_ACCOUNT_NAME=postgres
DB_ACCOUNT_PASSWORD=foobarbaz
DB_PORT=5432

3. Wallet DB(RDS/PostgreSQL周り)

AWSコンソールから開発/テスト用テンプレートでPostgreSQLを構築します。
特別な設定はしていません。上記環境図のWallet用のプライベートサブネットに配置します。

4. コンテナリポジトリ構築(ECR周り)

ECR(Elastic Container Registry)にプライベートリポジトリを作成し、ACA-Pyのイメージを登録します。後でECSがACA-Pyのイメージをプルできるようにするためです。

ECRでリポジトリの作成

コンソールから作成しました。

ローカルでACA-Pyイメージのビルド

(2022/2/17時点での)最新バージョンをクローンします。

git clone https://github.com/hyperledger/aries-cloudagent-python.git -b 0.7.3 --depth 1 
cd aries-cloudagent-python

イメージをビルドします。イメージ名はECRのリポジトリ名と合わせます。

docker build -f docker/Dockerfile.run -t aca-py .

ビルドしたイメージに対し、ECRで登録可能な形式になるようタグを切ります。

AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)

docker image tag aca-py ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/aca-py:v1

ローカルからECRへのイメージのプッシュ

以下のコマンドでプライベートレジストリに対する認証を通します。
(1つ目のコマンドで12時間有効なトークンを取得しています。)

aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin https://${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/aca-py

イメージをプッシュします。

docker image push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/aca-py:v1

コンソールからプッシュが成功したことを確認できました。

5. オーケストレータの構築(ECS on Fargate周り)

メインパートです。簡単にECSが取り扱う対象を説明します。

  • Cluster(クラスター) – Clusterは、Fargateがプロビジョンするコンテナ群を包含する論理単位です。Clusterは1つから複数のServiceを持ちます。
  • Service(サービス) – Serviceは、Task Definitionに従った1つから複数のTaskをClusterに配備します。いくつのTaskをネットワーク上にどのように配備するか、またTaskの前段にロードバランサーを配備するかどうかなどを指定します。
  • Task(タスク) – Taskは、1つから複数のコンテナの実体を持ちます。今回は1Task=1コンテナのため、実質的にコンテナです。
  • Task Definition(タスク定義) – Taskとして、どのコンテナイメージをどう起動するかや、Taskに割り当てるリソース量を定義します。

各用語間の包含関係のイメージです。

ロードバランサーの構築

まずは、後にECSのServiceで指定するALB(Application Load Balancer)を構築しておきます。これをECS Taskの前段に構えて負荷分散と冗長化を実現します。(上記環境図での、上2つのAZのPublic Subnetの中にいるモノです。)

以下、ALB、ターゲットグループおよびリスナーの作成コマンドです。

aws elbv2 create-load-balancer --name acapy-didcomm-lb 
--subnets <alb-1のsubnet id> <alb-2のsubnet id> 
--security-groups <alb-1,2のsecurity group id> 
--scheme internet-facing 
--type application
--ip-address-type ipv4
aws elbv2 create-target-group --name acapy-didcomm-tg 
--protocol HTTP --port 8020 --vpc-id <vpc-id> --target-type ip
aws elbv2 create-listener --load-balancer-arn <上記LBのarn> 
--protocol HTTP --port 80  
--default-actions Type=forward,TargetGroupArn=<上記TargetGroupのarn>

ここからCluster、Task DefinitionおよびServiceを作成していきます。

Clusterの作成

aws ecs create-cluster --cluster-name issuer-cluster

Task Definitionの登録

aws ecs register-task-definition --family issuer-task-def  
--task-role-arn arn:aws:iam::${ACCOUNT_ID}:role/issuerTaskRole  
--execution-role-arn arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole   
--network-mode awsvpc 
--requires-compatibilities FARGATE 
--cpu .5vcpu --memory 1GB 
--container-definitions file://acapy-container.json

以下、acapy-container.jsonです。

[
    {
        "name": "aca-py",
        "image" : "<aws account id>.dkr.ecr.ap-northeast-1.amazonaws.com/aca-py:v1",
        "portMappings": [
            {
                "protocol": "tcp",
                "containerPort": 8020,
                "hostPort": 8020
            },
           {
                "protocol": "tcp",
                "containerPort": 8021,
                "hostPort": 8021
           }
        ],
        "entryPoint": ["/bin/bash"],
        "command": [
            "-c",
            "aca-py start --label issuer-fargate  
            --inbound-transport http '0.0.0.0' 8020 
            --endpoint http://<ALBのDomain Name> 
            --outbound-transport http --admin '0.0.0.0' 8021 
            --admin-insecure-mode --auto-ping-connection 
            --auto-respond-messages --auto-accept-invites 
            --auto-accept-requests --preserve-exchange-records 
            --wallet-type indy --wallet-name issuer-wallet-fargate 
            --wallet-key thisissecret --wallet-storage-type postgres_storage 
            --wallet-storage-config '{"url":"<RDSのURL>:5432","wallet_scheme":"DatabasePerWallet"}' 
            --wallet-storage-creds '{"account":"postgres","password":"foobarbaz","admin_account":"postgres","admin_password":"foobarbaz"}' 
            --seed <事前にLedgerにEndorser登録した際のseed> 
            --auto-provision 
            --genesis-url http://<Indy LedgerのPrivate IP>:9000/genesis  
            --log-level debug"
        ],
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-region": "ap-northeast-1",
                "awslogs-stream-prefix":"issuer",
                "awslogs-group": "/acapy"
            }
        }
    }
]

ポイントは以下の通りです。

  • task-role-arnはコンテナつまりACA-Pyに付与するロールです。ACA-PyがWalletであるRDSを利用できるよう、ロール”issuerTaskRole”を作成して付与します。(ポリシーは手取り早くIAMで用意されているAmazonRDSDataFullAccessを使いました。)
  • execution-role-arnは、ホストつまりFargateに付与するロールです。前述のECRに作成したイメージを取得したり、CloudWatch Logsにログを保存できるよう、IAMで用意されているロール”ecsTaskExecutionRole”を付与します。
  • acapy-container.jsonは、docker runコマンドのパラメータ、またはdocker-compose.ymlと同等のファイルです。
    • cmdの中のaca-pyの起動パラメータはenvironmentに定義することもでき、本来であればそうした上で値をSystems Manager Parameter Storeなどに登録すると思いますが、手取り早く直書きしています。
    • logConfigurationを記述することで、ACA-Pyが出力するログをCloudWatch Logsに転送します。

Serviceの作成とコンテナの起動

aws ecs create-service  
--service-name issuer-service  
--desired-count 2  
--scheduling-strategy REPLICA 
--deployment-controller type=ECS  
--launch-type FARGATE  
--cluster arn:aws:ecs:ap-northeast-1:${ACCOUNT_ID}:cluster/issuer-cluster  
--task-definition arn:aws:ecs:ap-northeast-1:${ACCOUNT_ID}:task-definition/issuer-task-def:1 
--network-configuration file://task-network.json
--load-balancers file://load-balancer.json --health-check-grace-period-second 3600

以下、task-network.jsonです。

{
    "awsvpcConfiguration": {
        "subnets": ["<agent-1のsubnet id>","<agent-2のsubnet id>"],
        "securityGroups": ["<task用に用意したsecurity group id>"]
    }
}

以下、load-balancer.jsonです。

[
    {
        "targetGroupArn": "<前述のTarget Groupのarn>",
        "containerName": "aca-py",
        "containerPort": 8020
    }
]

ポイントは以下の通りです。

  • タスク(ここでは、=コンテナ)を2つ起動します。
  • scheduling-strategyREPLICAにすることで、コンテナはAvailability Zone間に分散されて起動されます。
  • ここまでに構築したロードバランサー、クラスター、タスク定義、およびタスクを起動するサブネットを指定しています。

クラスタ、サービス、タスクの確認

AWSコンソールとCloudWatch Logs上のACA-Pyのログから、クラスタ、サービスおよびタスクが正常に起動したことを確認できました。

ACA-Py Admin API呼び出し用EC2インスタンス構築

最後に、Fargateが建てたACA-Pyと同じサブネットに、Admin APIを呼ぶための作業用EC2インスタンスを構築します。System Managerからこのインスタンスにログインして、ここからcurlでAdmin APIをコールします。

(今回は手動でAdmin APIを呼び出していますが、プロダクトという観点であれば、アプリから呼び出すことが想定されます。そのような場合は、ACA-Pyの前段にもう1つの内部用ロードバランサーを構える構成になると考えます。)

ここまでで上記のAWS環境図の構築を完了できました。(以下に再掲します。)

6. 動作検証

Walletの存在確認

作業用EC2インスタンスにSystem Managerからログインして、Fargate上の片方のACA-Py(Issuer-1とします)のAdmin APIを呼び出します。

curl -X GET -H 'accept: application/json' 
http://<Issuer-1コンテナに自動で振られたPrivate IP>:8021/wallet/did

以下のレスポンスのようにWalletを確認できました。またIssuerのもう片方のACA-Py(Issuer-2とします)でも同様のデータを確認できました。

{
   "results":[
      {
         "did":"RkDyww6BQ8fuVdwfgtshUx",
         "verkey":"EVFFm6Zf6dr32GgPTDHwtJFRKA6zp81oUr7QzSMtrvan",
         "posture":"wallet_only",
         "key_type":"ed25519",
         "method":"sov"
      }
   ]
}

コネクションの生成

HolderでInvitationを生成する

HolderにはPublic IPを持たせたため、ローカルからAdmin APIを呼び出します。

curl -X POST  -H "Content-Type: application/json" -d '{
  "my_label": "Holder",
  "service_endpoint": "http://<HolderのPublic IP>:8030"
}' http://<HolderのPublic IP>:8031/connections/create-invitation

Invitationを取得できました。

{
    "connection_id": "1bf96b65-b76f-403a-9d34-662d56ca2919",
    "invitation": {
        "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation",
        "@id": "bd8398f7-5167-4180-94b7-bd86771dc705",
        "label": "Holder",
        "serviceEndpoint": "http://<HolderのPublic IP>:8030",
        "recipientKeys": [
            "2PANDUWrXh9AFM3qr4xYjca5MhvKSzzPhfdJNeu4BfsH"
        ],
    },
    "invitation_url": "http://<HolderのPublic IP>:8030?c_i=eyJAdHlwZSI6ICJkaWQ6c292OkJ6Q2JzTlloTXJqSGlxWkRUVUFTSGc7c3BlYy9jb25uZWN0aW9ucy8xLjAvaW52aXRhdGlvbiIsICJAaWQiOiAiYmQ4Mzk4ZjctNTE2Ny00MTgwLTk0YjctYmQ4Njc3MWRjNzA1IiwgImxhYmVsIjogIkhvbGRlciIsICJzZXJ2aWNlRW5kcG9pbnQiOiAiaHR0cDovLzE4LjE3OS42MC4yMjc6ODAzMCIsICJyZWNpcGllbnRLZXlzIjogWyIyUEFORFVXclhoOUFGTTNxcjR4WWpjYTVNaHZLU3p6UGhmZEpOZXU0QmZzSCJdfQ=="
}

IssuerでInvitationを受け取る

Walletの確認と同様に、作業用EC2インスタンスにSystem Managerからログインして、Fargate上の片方のACA-Py(Issuer-1)のAdmin APIを呼び出します。POSTするデータは先ほど取得したInvitationの中身です。

curl -X POST -H "Content-Type: application/json" -d '
{
    "@type": "did:sov:BzCbsNYhMrjHiqZDTUASHg;spec/connections/1.0/invitation", 
    "@id": "bd8398f7-5167-4180-94b7-bd86771dc705", 
    "label": "Holder",
    "serviceEndpoint": "http://<HolderのPublic IP>:8030", 
    "recipientKeys": ["2PANDUWrXh9AFM3qr4xYjca5MhvKSzzPhfdJNeu4BfsH"]
    }' 
    http://<Issuer-1コンテナに自動で振られたPrivate IP>:8021/connections/receive-invitation

Issuerのもう片方のACA-Py(Issuer-2)から、コネクションを確認してみます。

curl -X 'GET' -H 'accept: application/json'
  http://<Issuer-2コンテナに自動で振られたPrivate IP>:8021/connections'
{
   "results":[
      {
         "routing_state":"none",
         "state":"active",
         "connection_id":"3067d0b6-477e-4b0c-8333-773ee9ac74b5",
         "accept":"auto",
         "updated_at":"2022-02-17T04:29:20.895944Z",
         "rfc23_state":"completed",
         "my_did":"RkDyww6BQ8fuVdwfgtshUx",
         "invitation_msg_id":"bd8398f7-5167-4180-94b7-bd86771dc705",
         "their_did":"YS6pTwX85RcW7n6VnjGptC",
         "their_label":"Holder",
         "created_at":"2022-02-17T04:29:20.379276Z",
         "connection_protocol":"connections/1.0",
         "their_role":"inviter",
         "invitation_key":"2PANDUWrXh9AFM3qr4xYjca5MhvKSzzPhfdJNeu4BfsH",
         "request_id":"f6ad1870-d3f2-458e-9725-2c4470419b65",
         "invitation_mode":"once"
      }
   ]
}

コネクションを張ることができました。

メッセージの送信

IssuerからHolderへ

先ほどはコネクションの確立をIssuer-1コンテナから行いました。今回はもう片方のIssuer-2コンテナからメッセージの送信を試みます。

curl -X POST -H "Content-Type: application/json" 
-d '{ "content": "Hi, Holder. I am one of the orchestrated issuers." }' 
http://<Issuer-2コンテナに自動で振られたPrivate IP>:8021/connections/<connection id>/send-message

System ManagerからHolder役のACA-Pyのコンテナにログインして、メッセージの受信をログから確認できました。

HolderからIssuerへ

ALBによる負荷分散の確認も兼ねて、HolderからIssuerへ複数回、メッセージを送信してみます。(ALBの負荷分散アルゴリズムの設定はRound Robinです。)

curl -X 'POST' 
  'http://<HolderのPublic IP>:8031/connections/<connection id>/send-message' 
  -H 'Content-Type: application/json' 
  -d '{
  "content": "Hi, Issuer. Im a holder! (part X)"
}'

CloudWatch Logsから、受信メッセージがIssuerの両ACA-Pyコンテナ間で負荷分散されていることを確認できました。

おわりに

プロダクションレディという観点では、拡張/性能面、運用面、セキュリティ面でまだまだ検討事項がありますが、ECS on FargateでACA-Pyをコンテナオーケストレートすることができました。

どなたかのお役に立てたならば幸いです。