eden: Amazon ECS Dynamic Environment Manager (Amazon ECS動的環境マネージャ)

Amazon ECSを使った環境を簡単に複製するためのツールです。 edenに参照するためのAmazon ECS Serviceを指定すると、edenがそれを複製します。

edenは高速です。作成/削除コマンドの実行は5秒以内に完了します。

なぜ?

edenは、多くのプレビュー開発環境が必要だがデータベースやその他のリソースが共有可能なユースケースのために作られています。

どうやって?

edenは、参照ECSサービスからタスク定義、ECSサービス、およびターゲットグループを複製します。

edenは、すべてのクローンされたサービスに対して1つの共通ALBを使用することでコストと実行時間を削減します(ALBの作成には最大5分かかる場合があります)。 edenはターゲットグループを作成し、サービスを複製します。作成したターゲットグループを共通のALBにアタッチし、共通のALBを指すRoute 53 A ALIASレコードを作成します。

リソース作成順序

  1. ECSタスク定義
    • 参照サービスから複製
  2. ALBターゲットグループ
    • 設定は、参照サービスに接続されたターゲットグループからコピーされます
  3. ECSサービス
    • 参照サービスと同じクラスターに作成
  4. ALBリスナールール
    • ホストヘッダールール
  5. Route 53 A ALIASレコード
    • 共通ALBを指す
  6. Environments JSONファイルへの追加

注釈

リソースの削除は逆の順序で実行されます。作成と削除のいずれもの実行が5秒以内に完了します。

前提条件

  1. S3バケット内のEnvironments JSONファイル
  2. ターゲットグループがアタッチされた参照先となるECSサービス
  3. edenが管理するサービスのための共通ALB
    • ホストヘッダーリスナールールを用いて全ての環境によって再利用されるます
    • 参照サービスが使用するものとは別のもの
    • HTTPSリスナーが必要です
    • リスナーには、ターゲットダイナミックゾーンのワイルドカード証明書が必要です
  4. 単純なALBの利用
    • 複数のパスルールを利用しない、など
    • 1つのECSサービスごとに1つのALB

準備

工事中

Environments JSONファイル

Environments JSONファイルは次の目的で使用されます:

  1. 存在する環境とそのエンドポイントの確認
  2. クライアントアプリケーションに利用可能な環境を知らせる

Environments JSONファイルの例:

{
    "environments": [
        {
            "env": "dev",
            "name": "dev-dynamic-test",
            "api_endpoint": "api-test.dev.example.com"
        }
    ]
}

上記の例では、 config_update_key = api_endpoint を前提としています。

同じ名前で複数の環境を作成できます。単一のeden環境内に複数のエンドポイントを作れるようにするために、 config_update_key 設定が異なるプロファイルを使用すればよいです。たとえば、API、管理ツール、およびフロントエンドサービスを単一の環境として作成したいというユースケースがあるとします。

Let's say we have three profiles, api, admin, and frontend. These profiles are pre-configured with config_update_key equal to api_endpoint, admin_endpoint, frontend_endpoint respectively.

$ eden create -p api --name foo --image-uri xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/api:latest
$ eden create -p admin --name foo --image-uri xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/admin:latest
$ eden create -p frontend --name foo --image-uri xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/frontend:latest

このとき、Environments JSONファイルは以下のようになります:

{
    "environments": [
        {
            "env": "dev",
            "name": "dev-dynamic-test",
            "api_endpoint": "api-test.dev.example.com",
            "admin_endpoint": "admin-test.dev.example.com",
            "frontend_endpoint": "test.dev.example.com"
        }
    ]
}

Environments JSONファイル内では、単一のJSONオブジェクトに複数のエンドポイントが存在します。このオブジェクトにある最後のエンドポイントが削除されると、このオブジェクトがEnvironments JSONファイルから削除されます。

警告

上記の例のように単一の環境で複数のエンドポイントを扱う場合、エンドポイントの作成/削除の時間差により、環境が不完全になる可能性があることに注意してください (必要なすべてのエンドポイントが揃っていない環境が存在しうる)。

CLIとAPI

edenは CLIAPI として利用できます。

まず、eden CLIを試すことをお勧めします。edenをCI/CDパイプラインに追加する準備ができたら、eden APIの利用を推奨します。APIのための プロファイル のプッシュにはCLIが必要であることに注意してください。

eden CLIのインストール

$ pip3 install aws-eden-cli

$ eden -h
usage: eden [-h] {create,delete,ls,config} ...

ECS Dynamic Environment Manager. Clone Amazon ECS environments easily.

positional arguments:
  {create,delete,ls,config}
    create              Create environment or deploy to existent
    delete              Delete environment
    ls                  List existing environments
    config              Configure eden

optional arguments:
  -h, --help            show this help message and exit

ヒント: サブコマンドでも -h を使用できます。

$ eden config -h
 usage: eden config [-h] {setup,check,push,remote-rm} ...

positional arguments:
  {setup,check,push,remote-rm}
    setup               Setup profiles for other commands
    check               Check configuration file integrity
    push                Push local profile to DynamoDB for use by eden API
    remote-rm           Delete remote profile from DynamoDB

optional arguments:
  -h, --help            show this help message and exit

$ eden config push -h
usage: eden config push [-h] [-p PROFILE] [-c CONFIG_PATH] [-v]
                        [--remote-table-name REMOTE_TABLE_NAME]

optional arguments:
  -h, --help            show this help message and exit
  -p PROFILE, --profile PROFILE
                        profile name in eden configuration file
  -c CONFIG_PATH, --config-path CONFIG_PATH
                        eden configuration file path
  -v, --verbose
  --remote-table-name REMOTE_TABLE_NAME
                        profile name in eden configuration file

eden CLIの設定

まず、プロファイルを作成してみましょう。プロファイルを利用すれば、コマンドを実行する際に毎回すべてのパラメーターを指定する必要がなくなり、プロファイル名の指定のみで十分です。

$ eden config setup --endpoint-s3-bucket-name servicename-config
$ eden config setup --endpoint-s3-key endpoints.json
$ eden config setup --endpoint-name-prefix servicename-dev
$ eden config setup --endpoint-update-key api_endpoint
$ eden config setup --endpoint-env-type dev
$ eden config setup --domain-name-prefix api
$ eden config setup --dynamic-zone-id Zxxxxxxxxxxxx
$ eden config setup --master-alb-arn arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:loadbalancer/app/dev-alb-api-dynamic/xxxxxxxxxx
$ eden config setup --name-prefix dev-dynamic
$ eden config setup --reference-service-arn arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:service/dev/dev01-api
$ eden config setup --target-cluster dev

設定は ~/.eden/config に保存されます。上記のコマンドで default というプロファイルが作成されました:

$ cat ~/.eden/config
[api]
name_prefix = dev-dynamic
reference_service_arn = arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:service/dev/dev01-api
target_cluster = dev
domain_name_prefix = api
master_alb_arn = arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:loadbalancer/app/dev-alb-api-dynamic/xxxxxxxxxx
dynamic_zone_name = dev.example.com.
dynamic_zone_id = Zxxxxxxxxxxxx
config_bucket_name = servicename-config
config_bucket_key = endpoints.json
config_update_key = api_endpoint
config_env_type = dev
config_name_prefix = servicename-dev
target_container_name = api

設定ファイルの整合性をチェックしましょう:

$ eden config check
No errors found

edenのプロファイル

複数のプロファイルを使用できます。設定で複数のプロファイルを作成し、コマンドを実行する際にの -p プロファイル名 で使用するプロファイルを指定できます。

$ eden config check -p api
No errors found

ローカルプロファイルをDynamoDBに保存することで、eden APIでそのプロファイルが使用できるようになります。

$ eden config push -p api
Waiting for table creation...
Successfully pushed profile api to DynamoDB

注釈

edenのプロファイルを保存するためのDynamoDBテーブルが存在しない場合、eden CLIがテーブルを自動的に作成します

同じコマンドで既存のプロファイルが上書きされます (既存のプロファイルにプッシュすると上書きされます):

$ eden config push -p api
Successfully pushed profile api to DynamoDB table eden

remote-rmコマンドでリモートのプロファイルを削除することができます:

$ eden config remote-rm -p api
Successfully removed profile api from DynamoDB table eden

コマンドの実行

環境の作成:

$ eden create -p api --name foo --image-uri xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/api:latest
Checking if image xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/api:latest exists
Image exists
Retrieved reference service arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:service/dev/api
Retrieved reference task definition from arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task-definition/api:20
Registered new task definition: arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task-definition/dev-dynamic-api-foo:1
Registered new task definition: arn:aws:ecs:ap-northeast-1:xxxxxxxxxx:task-definition/dev-dynamic-api-foo:1
Retrieved reference target group: arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:targetgroup/api/xxxxxxxxxxxx
Existing target group dev-dynamic-api-foo not found, will create new
Created target group arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:targetgroup/dev-dynamic-api-foo/xxxxxxxxxxxx
ELBv2 listener rule for target group arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:targetgroup/dev-dynamic-api-foo/xxxxxxxxxxxx and host api-foo.dev.example.com does not exist, will create new listener rule
ECS Service dev-dynamic-api-foo does not exist, will create new service
Checking if record api-foo.dev.example.com. exists in zone Zxxxxxxxxx
Successfully created CNAME: api-foo.dev.example.com -> dev-alb-api-dynamic-297517510.ap-northeast-1.elb.amazonaws.com
Updating config file s3://example-com-config/endpoints.json, environment example-api-foo: nodeDomain -> api-foo.dev.example.com
Existing environment not found, adding new
Successfully updated config file
Successfully finished creating environment dev-dynamic-api-foo

注釈

create、deleteコマンドは、リモートステート(DynamoDBテーブル)を更新します。 eden config push と同様に、テーブルが存在しない場合、テーブルが自動的に作成されます。

作成の確認:

$ eden ls
Profile api:
dev-dynamic-api-foo api-foo.dev.example.com (last updated: 2019-11-20T19:44:10.179760)

注釈

この一覧は、 Environments JSON ファイルではなく、リモートステートのDynamoDBテーブルから生成されます。最後に更新されたタイムスタンプは、createでの環境の新規作成、すでに存在する環境へのデプロイで更新されます。

環境を削除し、削除を確認します:

$ eden delete -p api --name foo
Updating config file s3://example-com-config/endpoints.json, delete environment example-api-foo: nodeDomain -> api-foo.dev.example.com
Existing environment found, and the only optional key is nodeDomain,deleting environment
Successfully updated config file
Checking if record api-foo.dev.example.com. exists in zone Zxxxxxxxxx
Found existing record api-foo.dev.example.com. in zone Zxxxxxxxxx
Successfully removed CNAME record api-foo.dev.example.com
ECS Service dev-dynamic-api-foo exists, will delete
Successfully deleted service dev-dynamic-api-foo from cluster dev
ELBv2 listener rule for target group arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:targetgroup/dev-dynamic-api-foo/xxxxxxxxxxxx and host api-foo.dev.example.com found, will delete
Deleted target group arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxx:targetgroup/dev-dynamic-api-foo/xxxxxxxxxxxx
Deleted all task definitions for family: dev-dynamic-api-foo, 1 tasks deleted total
Successfully finished deleting environment dev-dynamic-api-foo

$ eden ls
No environments available

APIへの移行

CLIとAPIの両方がDynamoDBテーブルでステートを管理します。このテーブルはCLIによってのみ作成されます。また、APIはリモートステートに格納されたプロファイルのみを使用できます。 APIを初めて実行する前に、 eden config --push を実行して、APIのためのプロファイルをプッシュしてください。TerraformでAPIを構築する際に、リモートステートテーブルが存在しない場合、terraform applyの実行が失敗します。

API内部

eden APIは以下の要素で構成されます:

  1. Lambda関数 (API本体)
  2. API GatewayとAPIを保護するためのAPIキー
  3. ステート管理用のDynamoDBテーブル
    • デフォルトのテーブル名は「eden」です。

Terraformを使用したeden APIの構築

module "eden_api" {
  source  = "baikonur-oss/lambda-eden-api/aws"
  version = "0.2.0"

  lambda_package_url = "https://github.com/baikonur-oss/terraform-aws-lambda-eden-api/releases/download/v0.2.0/lambda_package.zip"
  name               = "eden"

  # eden API Gateway variables
  api_acm_certificate_arn = "${data.aws_acm_certificate.wildcard.arn}"
  api_domain_name         = "${var.env}-eden.${data.aws_route53_zone.main.name}"
  api_zone_id             = "${data.aws_route53_zone.main.zone_id}"

  endpoints_bucket_name = "somebucket"

  dynamic_zone_id = "${data.aws_route53_zone.dynamic.zone_id}"
}

警告

ステート管理のためのDynamoDBテーブルはeden CLIによって作成されます。 terraform apply を実行する前に、一度 eden config --push を実行してください。

複数のプロファイルを使うことで、1つのアカウント/リージョンに対して1つのeden APIで十分です。詳しくは プロファイル セクションを参照してください。

eden APIのコマンド

eden APIには、createとdeleteという2つのAPIコマンドのみあります。

GET /api/v1/create

必須クエリパラメータ:

  • name: 環境名
  • image_uri: デプロイするECRイメージのURI。すでにECRにプッシュされ、同じアカウントにある必要があります(eden APIは、デプロイ前にイメージURIが有効であるかを確認します)

任意のクエリパラメータ:

  • profile: edenのリモートプロファイルの指定 (デフォルト値は default )。プロファイルには、コマンドを実行するために必要なすべての設定が含まれています。リモートプロファイルは eden config --push コマンドで作成できます(詳細は こちら を参照)。

GET /api/v1/delete

必須クエリパラメータ:

  • name: 環境名

任意のクエリパラメータ:

  • profile: edenのリモートプロファイルの指定 (デフォルト値は default )。プロファイルには、コマンドを実行するために必要なすべての設定が含まれています。リモートプロファイルは eden config --push コマンドで作成できます(詳細は こちら を参照)。

eden APIキー

eden API Terraformモジュールは、1つのAPIキーを作成します。API GatewayコンソールからAPIキーが確認できます。

APIにアクセスするには、このキーを指定しなければなりません。

APIキーをHTTPヘッダーとして指定する必要があります:

x-api-key: YOURAPIKEY

API使用例

api という名前のリモートプロファイルを使ってcreate APIを実行してみましょう:

curl https://eden.example.com/api/v1/create?name=test-create&image_uri=xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/servicename-api-dev:latest&profile=api -H "x-api-key:YOURAPIKEY"

APIのLambda関数が出力したログを見てみましょう:

2019-04-08T20:32:05.151Z INFO     [main.py:check_cirn:382] Checking if image xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/servicename-api-dev:latest exists
2019-04-08T20:32:05.270Z INFO     [main.py:check_cirn:401] Image exists
2019-04-08T20:32:05.446Z INFO     [main.py:create_env:509] Retrieved reference service arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:service/dev/dev01-api
2019-04-08T20:32:05.484Z INFO     [main.py:create_task_definition:58] Retrieved reference task definition from arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task-definition/dev01-api:15
2019-04-08T20:32:05.557Z INFO     [main.py:create_task_definition:96] Registered new task definition: arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task-definition/dev-dynamic-test-create:1
2019-04-08T20:32:05.584Z INFO     [main.py:create_target_group:112] Retrieved reference target group: arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:targetgroup/dev01-api/9c68a5f91f34d9a4
2019-04-08T20:32:05.611Z INFO     [main.py:create_target_group:125] Existing target group dev-dynamic-test-create not found, will create new
2019-04-08T20:32:06.247Z INFO     [main.py:create_target_group:144] Created target group
2019-04-08T20:32:06.310Z INFO     [main.py:create_alb_host_listener_rule:355] ELBv2 listener rule for target group arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:targetgroup/dev-dynamic-test-create/b6918e6e5f10389d and host api-test.dev.example.com does not exist, will create new listener rule
2019-04-08T20:32:06.361Z INFO     [main.py:create_env:554] ECS Service dev-dynamic-test-create does not exist, will create new service
2019-04-08T20:32:07.672Z INFO     [main.py:check_record:414] Checking if record api-test.dev.example.com. exists in zone Zxxxxxxxxxxxx
2019-04-08T20:32:08.133Z INFO     [main.py:create_cname_record:477] Successfully created ALIAS: api-test.dev.example.com -> dev-alb-api-dynamic-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com
2019-04-08T20:32:08.134Z INFO     [main.py:create_env:573] Successfully finished creating environment dev-dynamic-test-create

環境のステートがリモートのDynamoDBテーブルで管理されているため、eden CLIで環境の作成を確認できます:

$ eden ls
Profile api:
dev-dynamic-test-create api-test.dev.example.com (last updated: 2019-04-08T20:32:08.134469)

次のコマンドを実行して、この環境を削除しましょう:

curl https://eden.example.com/api/v1/delete?name=test&profile=api -H "x-api-key:YOURAPIKEY"

API Lambdaログは次のようになります:

2019-04-10T23:11:38.515Z INFO     [main.py:check_record:495] Checking if record api-test.dev.example.com. exists in zone Zxxxxxxxxxxxx
2019-04-10T23:11:38.752Z INFO     [main.py:check_record:506] Found existing record api-test.dev.example.com. in zone Zxxxxxxxxxxxx
2019-04-10T23:11:38.996Z INFO     [main.py:delete_cname_record:596] Successfully removed ALIAS record api-test.dev.example.com
2019-04-10T23:11:39.245Z INFO     [main.py:delete_env:665] ECS Service dev-dynamic-test exists, will delete
2019-04-10T23:11:39.401Z INFO     [main.py:delete_env:670] Successfully deleted service dev-dynamic-test from cluster dev
2019-04-10T23:11:39.573Z INFO     [main.py:delete_alb_host_listener_rule:397] ELBv2 listener rule for target group arn:aws:elasticloadbalancing:ap-northeast-1:xxxxxxxxxxxx:targetgroup/dev-dynamic-test/xxxxxxxx and host api-test.dev.example.com found, will delete
2019-04-10T23:11:40.483Z INFO     [main.py:delete_env:697] Deleted all task definitions for family: dev-dynamic-test, 5 tasks deleted total
2019-04-10T23:11:40.483Z INFO     [main.py:delete_env:700] Successfully finished deleting environment dev-dynamic-test