こんにちは。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役)間において、インターネット越しに、コネクションの作成とそこからのメッセージの交換が機能するかどうかを検証します。
流れ
大まかな流れです。
- ネットワーク構築(VPC周り)
- Holder役のACA-PyとIndy Ledgerの構築
- Wallet DB構築(RDS/PostgreSQL)
- コンテナリポジトリ構築(ECR)
- オーケストレータ構築(ECS on Fargate)
- 動作検証
では、構築を始めます。
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-strategy
をREPLICA
にすることで、コンテナは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をコンテナオーケストレートすることができました。
どなたかのお役に立てたならば幸いです。