AWS CopilotでServiceを3つのAvailability Zoneにデプロイしたい(が、なかなか厳しい)

2021.04.28

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

コンサル部のとばち(@toda_kk)です。

AWS Copilotでは、ApplicationやEnvironmentを作成した後、ServiceとしてECSサービスを設定しタスクをデプロイできます。

デプロイ先のVPCやサブネットといったリソースについては、Environmentを初期設定する際に作成されます。この際、デフォルトでは作成されるサブネットは2つのAvailability Zone(2AZ)分となります。

Copilotで作成したECSサービスからタスクを3つ以上のAZに分散してデプロイしたい場合、少し工夫する必要があります。

注意

本記事は、AWS Copilot CLI v1.5.0を前提として記載しています。今後のバージョンアップで機能が変更となる可能性がありますので、最新の情報は公式ドキュメントに従ってください。

3AZにデプロイする手順

VPCとサブネットの作成

まずはenv initでリソースを作成します。インタラクティブな形式でCIDRなどの設定を入力していきます。

この際、サブネットのCIDRを指定する箇所ではカンマ区切りで入力することになります。設定例ではCIDRが2つ分で表示されていますが、カンマでつなぐことで3つ以上のCIDRを指定しサブネットを作成することができます。また、サブネットは各AZに分散されて作成されます。

$ copilot env init
What is your environment's name? test
Which credentials would you like to use to create test? [profile tobachi]

  Would you like to use the default configuration for a new environment?
    - A new VPC with 2 AZs, 2 public subnets and 2 private subnets
    - A new ECS Cluster
    - New IAM Roles to manage services and jobs in your environment
  [Use arrows to move, type to filter]
    Yes, use default.
  > Yes, but I'd like configure the default resources (CIDR ranges).
    No, I'd like to import existing resources (VPC, subnets).

  What VPC CIDR would you like to use? [? for help] (10.0.0.0/16) 10.0.0.0/16
  What CIDR would you like to use for your public subnets? [? for help] (10.0.0.0/24,10.0.1.0/24) 10.0.0.0/24,10.0.1.0/24,10.0.2.0/24
  What CIDR would you like to use for your private subnets? [? for help] (10.0.2.0/24,10.0.3.0/24) 10.0.255.0/24,10.0.254.0/24,10.0.253.0/24

✔ Linked account 123456789012 and region ap-northeast-1 to application sample-app.

✔ Proposing infrastructure changes for the sample-app-test-env environment.
- Creating the infrastructure for the sample-app-test-env environment.   [create complete]  [79.2s]
  - An IAM Role for AWS CloudFormation to manage resources               [create complete]  [20.0s]
  - An ECS cluster to group your services                                [create complete]  [9.5s]
  - An IAM Role to describe resources in your environment                [create complete]  [19.8s]
  - A security group to allow your containers to talk to each other      [create complete]  [3.8s]
  - An Internet Gateway to connect to the public internet                [create complete]  [16.4s]
  - Private subnet 1 for resources with no internet access               [create complete]  [16.2s]
  - Private subnet 2 for resources with no internet access               [create complete]  [16.2s]
  - Private subnet 3 for resources with no internet access               [create complete]  [16.2s]
  - Public subnet 1 for resources that can access the internet           [create complete]  [16.2s]
  - Public subnet 2 for resources that can access the internet           [create complete]  [16.2s]
  - Public subnet 3 for resources that can access the internet           [create complete]  [16.2s]
  - A Virtual Private Cloud to control networking of your AWS resources  [create complete]  [16.4s]
✔ Created environment test-env in region ap-northeast-1 under application sample-app.

作成されるリソースの実態

CopilotではCloudFormationスタックを生成することでAWSリソースを作成しています。このとき、スタックのOutputとしてパブリック/プライベートのサブネットのリストが指定されています。

CloudFormationスタックのOutput

Copilotでsvc initsvc deployコマンドを使ってECSリソースを作成・更新する際は、このOutputの値を参照しています。いわゆるクロススタック参照です。

では、このままsvc deployすればタスクが3AZにデプロイされる……というわけではありません。

実は、svc initして生成されるCloudFormationテンプレートを確認してみると、上記のOutputからサブネットのリストの値を参照しているのですが、Fn::ImportValue:でリストの最初の2つの要素だけを指定して取得する仕組みになっています。

  Service:
    Metadata:
      'aws:copilot:description': 'An ECS service to run and maintain your tasks in the environment cluster'
    Type: AWS::ECS::Service
    DependsOn: WaitUntilListenerRuleIsCreated
    Properties:
      (中略)
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets:
            - Fn::Select:
              - 0
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${AppName}-${EnvName}-PublicSubnets'
            - Fn::Select:
              - 1
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${AppName}-${EnvName}-PublicSubnets'
          SecurityGroups:
            - Fn::ImportValue: !Sub '${AppName}-${EnvName}-EnvironmentSecurityGroup'

つまり、Environmentの設定でサブネットを3つ以上作成していたとしても、Serviceの設定で参照しているサブネットは2つになってしまっているため、そのままではECSタスクを3AZにデプロイできないのです。

解決策

ということは、ServiceのCloudFormationスタックを更新してサブネットのリストを3つ以上参照する形にすれば、3AZにデプロイしてくれるようになると考えられます。

実際に検証してみます。まずは、上記の通りCloudFormationテンプレートを更新します。

  Service:
    Metadata:
      'aws:copilot:description': 'An ECS service to run and maintain your tasks in the environment cluster'
    Type: AWS::ECS::Service
    DependsOn: WaitUntilListenerRuleIsCreated
    Properties:
      (中略)
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets:
            - Fn::Select:
              - 0
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${AppName}-${EnvName}-PublicSubnets'
            - Fn::Select:
              - 1
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${AppName}-${EnvName}-PublicSubnets'
            ### ここから追加 ###
            - Fn::Select:
              - 2
              - Fn::Split:
                - ','
                - Fn::ImportValue: !Sub '${AppName}-${EnvName}-PublicSubnets'
            ### ここまで追加 ###
          SecurityGroups:
            - Fn::ImportValue: !Sub '${AppName}-${EnvName}-EnvironmentSecurityGroup'

次に、CloudFormationスタックの更新を実行します。これでECSサービスが更新され、タスクが新たにデプロイされます。

  • スタック更新前 ECSサービスのサブネット設定(更新前)

  • スタック更新後 ECSサービスのサブネット設定(更新後)

ECSサービスで3つのサブネットが指定されるように変更されました。これでタスクを3つ以上デプロイすれば、3AZで起動することになります。

しかし、落とし穴がある

では、その後にsvc deployを実行しECSサービスの設定を変更するとどうなるでしょうか。

前述の通り、上記コマンドは実態としてはCloudFormationスタックを更新していることになります。この際、更新する情報はCopilot Serviceのマニフェストファイルに記載されている内容になります。

マニフェストファイルの項目で設定値を指定することで、実態として生成されるCloudFormationスタックのパラメーターが変更されるようです。

ということは、Copilotツール自体がベースとなるCloudFormationテンプレートを内包しており、マニフェストファイルと合わせることでスタックを作成するのだと予測されます。

CopilotのソースコードはGitHubから確認できます。該当しそうな箇所を探してみると、Fn::ImportValue:でサブネットのリストから2つの要素だけを参照する記述がベタが記されているテンプレートファイルを発見できました。

https://github.com/aws/copilot-cli/blob/28039f98cb0985de720856b8de72e544893b11cf/templates/workloads/partials/cf/service-base-properties.yml#L28-L38

つまり、Copilotを使ってServiceをデプロイするとサブネットの設定が強制的に上書きされてしまい、タスクを3AZで起動し続けることができなくなるのです。

結論

  • Copilotの機能として、ECSタスクを3AZおよび3つ以上のサブネットにデプロイするように設定することはサポートされていない。
  • Copilotが生成するCloudFormationスタックを直接更新することで一時的に可能となる。
  • しかしその後にCopilotを使ってServiceをデプロイすると、スタックが上書きされデプロイ先のサブネットの設定が戻ってしまう。

CopilotではECSリソースだけでなくVPCなどの関連するリソースをまるっと管理してくれるため、便利な反面、融通がきかないところがあるなという印象です。

ECSのデプロイツールは数多くありますので、こうしたメリットやデメリットを考慮に入れた上で選定する必要があると改めて感じました。

以上、コンサル部のとばち(@toda_kk)でした。