本連載では、以下のイメージの構成にあるAWSリソース基盤自動化環境の構築を実践しています。

本連載で構築していく基盤自動化環境のイメージ

前回は、タスク定義したコンテナで実行されるアプリケーションが使用するAWSリソースへのアクセスポリシーを定義し、ECSタスクのIAMロールへアタッチするCloudFormationテンプレートを実装しました。今回は、CloudFormationテンプレートを使ってECSサービスを構築します。

なお、実際のソースコードはGitHub上にコミットしています。以降のソースコードでは本質的でない記述を一部省略しているので、実行コードを作成する場合は、必要に応じて適宜GitHub上のソースコードも参照してください。

ECSサービススタック構築テンプレート

ECSサービス構築は連載「AWSで作るクラウドネイティブアプリケーションの基本」の第10回で実施した要領と同等のものを構築します。ECSサービスをCloudFormationで構築する場合、リソースタイプには「AWS::ECS::Service」が必要です。プロパティとして設定可能な属性については、リンク先を参照してください。加えて、ECSを商用環境、ステージング環境、開発環境という3つのパターンに分けて作成するようにします。また、今回は同一のECSタスク定義で、ターゲットグループごとに実行コンテナを分けて複数サービスを実行してみます。

ECSサービスを構築するテンプレートのサンプルは以下の通りです。

AWSTemplateFormatVersion: '2010-09-09'

// omit

Parameters:
// omit
  EnvType:
    Description: Which environments to deploy your service.
    Type: String
    AllowedValues: ["Dev", "Staging", "Production"]
    Default: Dev

Mappings:
  BackendUserServiceMap:                                                           #(A)
    Production:
      "DesiredCount": 1
      "ContainerName": "mynavi-sample-cloudformation-ecs-backend-service"
      "ContainerPort": 8080
    Staging:
      "DesiredCount": 1
      "ContainerName": "mynavi-sample-cloudformation-ecs-backend-service"
      "ContainerPort": 8080
    Dev:
      "DesiredCount": 1
      "ContainerName": "mynavi-sample-cloudformation-ecs-backend-service"
      "ContainerPort": 8080
  BackendSampleServiceMap:                                                         #(B)
    Production:
      "DesiredCount" : 1
      "ContainerName" : "mynavi-sample-cloudformation-ecs-backend-service"
      "ContainerPort" : 8080
    Staging:
      "DesiredCount" : 1
      "ContainerName" : "mynavi-sample-cloudformation-ecs-backend-service"
      "ContainerPort" : 8080
    Dev:
      "DesiredCount" : 1
      "ContainerName" : "mynavi-sample-cloudformation-ecs-backend-service"
      "ContainerPort" : 8080
  FrontendWebAppMap:                                                               #(C)
    Production:
      "DesiredCount": 1
      "ContainerName" : "mynavi-sample-cloudformation-ecs-frontend-app"
      "ContainerPort" : 8080
    Staging:
      "DesiredCount": 1
      "ContainerName" : "mynavi-sample-cloudformation-ecs-frontend-app"
      "ContainerPort" : 8080
    Dev:
      "DesiredCount": 1
      "ContainerName" : "mynavi-sample-cloudformation-ecs-frontend-app"
      "ContainerPort" : 8080

Resources:
  FrontendWebAppService:                                                           #(D)
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Fn::ImportValue: !Sub ${VPCName}-FrontendEcsCluster-${EnvType}
      DesiredCount: !FindInMap [FrontendWebAppMap, !Ref EnvType, DesiredCount]
      HealthCheckGracePeriodSeconds: 60
      TaskDefinition:
        Fn::ImportValue: !Sub ${VPCName}-FrontendEcsTaskDefinition-${EnvType}
      LaunchType: EC2
      LoadBalancers:
        - ContainerName: !FindInMap [FrontendWebAppMap, !Ref EnvType, ContainerName]
          ContainerPort: !FindInMap [FrontendWebAppMap, !Ref EnvType, ContainerPort]
          TargetGroupArn:
            Fn::ImportValue: !Sub ${VPCName}-Frontend-FrontendWebApp-TargetGroup-${EnvType}

  BackendUserService:                                                              #(E)
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Fn::ImportValue: !Sub ${VPCName}-BackendEcsCluster-${EnvType}
      DesiredCount: !FindInMap [BackendUserServiceMap, !Ref EnvType, DesiredCount]
      HealthCheckGracePeriodSeconds: 60
      TaskDefinition:
        Fn::ImportValue: !Sub ${VPCName}-BackendEcsTaskDefinition-${EnvType}
      LaunchType: EC2
      LoadBalancers:
        - ContainerName: !FindInMap [BackendUserServiceMap, !Ref EnvType, ContainerName]
          ContainerPort: !FindInMap [BackendUserServiceMap, !Ref EnvType, ContainerPort]
          TargetGroupArn:
            Fn::ImportValue: !Sub ${VPCName}-Backend-BackendUserService-TargetGroup-${EnvType}

  BackendSampleService:                                                            #(F)
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Fn::ImportValue: !Sub ${VPCName}-BackendEcsCluster-${EnvType}
      DesiredCount: !FindInMap [BackendSampleServiceMap, !Ref EnvType, DesiredCount]
      TaskDefinition:
        Fn::ImportValue: !Sub ${VPCName}-BackendEcsTaskDefinition-${EnvType}
      LaunchType: EC2
      LoadBalancers:
        - ContainerName: !FindInMap [BackendSampleServiceMap, !Ref EnvType, ContainerName]
          ContainerPort: !FindInMap [BackendSampleServiceMap, !Ref EnvType, ContainerPort]
          TargetGroupArn:
            Fn::ImportValue: !Sub ${VPCName}-Backend-BackendSampleService-TargetGroup-${EnvType}

ECSサービスを構築するテンプレートの記述の基本となるポイントは、下表の通りです。

記述 説明
A パラメータEnvTypeに応じて、Backend Serviceアプリケーションの1つとして構築するBackendUserServiceの定義を切り替えます
B パラメータEnvTypeに応じて、Backend Serviceアプリケーションの1つとして構築するBackendSampleServiceの定義を切り替えます
C パラメータEnvTypeに応じて、Frontend Webアプリケーションの定義を切り替えます
D Frontend WebアプリケーションのECSサービスを定義します。詳細は「AWS::ECS::Service」を参照してください
E Backend Serviceアプリケーションの一つとしてBackendUserServiceをECSサービスとして定義します。詳細は「AWS::ECS::Service」を参照してください。実行されるコンテナイメージは同じでも、ターゲットグループに指定したパスにより異なるコンテナへルーティングするよう設定します
F Backend Serviceアプリケーションの一つとしてBackendSampleServiceをECSサービスとして定義します。詳細は「AWS::ECS::Service」を参照してください。実行されるコンテナイメージは同じでも、ターゲットグループに指定したパスにより異なるコンテナへルーティングするよう設定します

ECSサービスの構築には、これまで構築してきたALBやターゲットグループ、ECSクラスタ、タスク定義などさまざまなリソースを事前に起動しておかなければなりません(ElastiCacheやS3なども未作成だとアプリケーション起動時に失敗します)。

そこで、そうしたリソースは、複数のテンプレートをまとめて実行するようにネストされた親テンプレートで起動するようにします。具体的な作業としては、これまで構築してきたリソースも含め、構築順序関係を定義して、ステージング環境として一括構築するようテンプレートを実装することになります。

なお、VPCとセキュリティグループは事前に構築されていることを前提とします。サンプルとなるテンプレートは以下の通りです。

AWSTemplateFormatVersion: '2010-09-09'

// omit

Parameters:
  VPCName:
    Description: Target VPC Stack Name
    Type: String
    MinLength: 1
    MaxLength: 255
    AllowedPattern: ^[a-zA-Z][-a-zA-Z0-9]*$
    Default: mynavi-sample-cloudformation-vpc
  EnvType:
    Description: Which environments to deploy your service.
    Type: String
    AllowedValues: ["Staging"]
    Default: Staging

Resources:
  NATGatewayStagingStack:                                                    #(A)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-ng-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}

  ALBStagingStack:                                                           #(B)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-alb-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}

  BackendUserServiceTargetGroupStagingStack:                                 #(C)
    Type: AWS::CloudFormation::Stack
    DependsOn: ALBStagingStack
    Properties:
      TemplateURL: ./sample-tg-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}
        SubnetType: Backend
        ServiceName: BackendUserService

  BackendSampleServiceTargetGroupStagingStack:                               #(D)
    Type: AWS::CloudFormation::Stack
    DependsOn: ALBStagingStack
    Properties:
      TemplateURL: ./sample-tg-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}
        SubnetType: Backend
        ServiceName: BackendSampleService

  FrontendWebAppTargetGroupStagingStack:                                     #(E)
    Type: AWS::CloudFormation::Stack
    DependsOn: ALBStagingStack
    Properties:
      TemplateURL: ./sample-tg-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}
        SubnetType: Frontend
        ServiceName: FrontendWebApp

  RDSStagingStack:                                                           #(F)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-rds-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}

  DynamoDBStagingStack:                                                      #(G)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-dynamodb-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}

  ElastiCacheStagingStack:                                                   #(H)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-elasticache-cfn.yml
      Parameters:
        VPCName: !Sub ${VPCName}
        EnvType: !Sub ${EnvType}

  S3StagingStack:                                                            #(I)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-s3-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

  SQSStagingStack:                                                           #(J)
    Type: AWS::CloudFormation::Stack
    Properties:
      TemplateURL: ./sample-sqs-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

  ECSClusterStagingStack:                                                    #(K)
    Type: AWS::CloudFormation::Stack
    DependsOn: NATGatewayStagingStack
    Properties:
      TemplateURL: ./sample-ecs-cluster-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

  ECSTaskDefinitionStagingStack:                                             #(L)
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - ECSClusterStagingStack
    Properties:
      TemplateURL: ./sample-ecs-task-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

  BackendECSTaskRoleStagingStack:                                            #(M)
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - ECSTaskDefinitionStagingStack
    Properties:
      TemplateURL: ./sample-ecs-taskrole-backend-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

  FrontendECSTaskRoleStagingStack:                                           #(N)
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - ECSTaskDefinitionStagingStack
    Properties:
      TemplateURL: ./sample-ecs-taskrole-frontend-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

  ECSServiceStagingStack:                                                    #(O)
    Type: AWS::CloudFormation::Stack
    DependsOn:
      - BackendECSTaskRoleStagingStack
      - FrontendECSTaskRoleStagingStack
      - BackendSampleServiceTargetGroupStagingStack
      - BackendUserServiceTargetGroupStagingStack
      - FrontendWebAppTargetGroupStagingStack
    Properties:
      TemplateURL: ./sample-ecs-service-cfn.yml
      Parameters:
        EnvType: !Sub ${EnvType}

ステージング環境を一括構築するテンプレートの記述の基本となるポイントは、下表の通りです。

記述 説明
A NATGatewayテンプレートをリソースとして定義します。パラメータとしてVPCNameを子テンプレートに渡します
B ALBテンプレートをリソースとして定義します。パラメータとしてVPCName、EnvTypeを子テンプレートに渡します
C BackendUserService向けのターゲットグループとして、リソース定義します。パラメータとしてVPCName、EnvType、SubnetType、ServiceNameを子テンプレートに渡します。テンプレートの実行にはALBを事前に構築しておく必要があるので、DependsOn属性でBのALBのスタックを定義しておきましょう
D BackendSampleService向けのターゲットグループとして、リソース定義します。パラメータとしてVPCName、EnvType、SubnetType、ServiceNameを子テンプレートに渡します。テンプレートの実行にはALBを事前に構築しておく必要があるので、DependsOn属性でBのALBのスタックを定義しておきましょう
E FrontendWebApp向けのターゲットグループとして、リソース定義します。パラメータとしてVPCName、EnvType、SubnetType、ServiceNameを子テンプレートに渡します。テンプレートの実行にはALBを事前に構築しておく必要があるので、DependsOn属性でBのALBのスタックを定義しておきましょう
F RDSテンプレートをリソースとして定義します。パラメータとしてVPCName、EnvTypeを子テンプレートに渡します
G DynamoDBテンプレートをリソースとして定義します。パラメータとしてVPCName、EnvTypeを子テンプレートに渡します
H ElastiCacheテンプレートをリソースとして定義します。パラメータとしてVPCName、EnvTypeを子テンプレートに渡します
I S3テンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します
J SQSテンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します
K ECSクラスタテンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します。テンプレートの実行にはNATGatewayを事前に構築しておく必要があるので、DependsOn属性でAのNATGatewayのスタックを定義しておきましょう
L ECSタスク定義テンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します。テンプレートの実行にはECSクラスタを事前に構築しておく必要があるので、DependsOn属性でKのECSクラスタのスタックを定義しておきましょう
M BackendService向けのECSタスクロール定義テンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します。テンプレートの実行にはECSタスクを事前に構築しておく必要があるので、DependsOn属性でLのECSタスクのスタックを定義しておきましょう
N FrontendWebApp向けのECSタスクロール定義テンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します。テンプレートの実行にはECSタスクを事前に構築しておく必要があるので、DependsOn属性でLのECSタスクのスタックを定義しておきましょう
O ECSサービス構築テンプレートをリソースとして定義します。パラメータとしてEnvTypeを子テンプレートに渡します。テンプレートの実行にはロール定義やターゲットグループを事前に構築しておく必要があるので、DependsOn属性でC、D、Eのスタックを定義しておきましょう

本連載の第33回でも解説した通り、NestedStackとして作成したテンプレートで指定した子のテンプレートのURLは、本来S3にアップロードしてそのオブジェクトキーを指定しなければなりません。AWS CLIのコマンド「aws cloudformation package」で、特定のS3バケットを指定し実行することで、バケットへのアップロードおよびURLをオブジェクトキーに置き換えたテンプレートを生成できます。

事前にアップロード先のバケットを作成した上で(ここでは、連載「AWSで作るクラウドネイティブアプリケーションの基本」の第25回と同様の手順で、「debugroom-mynavi-sample-cloudformation-package」というバケットを事前に作成しておきます)、パッケージを実行するヘルパースクリプトを以下のように作成して実行します。

#!/usr/bin/env bash

template_path="sample-infra-staging-cfn.yml"
output_template="sample-infra-staging-package-cfn.yml"
s3_bucket="debugroom-mynavi-sample-cloudformation-package"

aws cloudformation package --template-file ${template_path} --s3-bucket ${s3_bucket} --output-template-file ${output_template}

実行が正常に終了すると、URLのパスが置き換わったテンプレート「ample-infra-staging-package-cfn.yml」が作成されます。

作成したテンプレートに対して、以下のように、スタック名とテンプレートパスを変更してヘルパースクリプトを実行します。

#!/usr/bin/env bash

stack_name="mynavi-sample-infra-staging"
template_path="sample-infra-staging-package-cfn.yml"

parameters="EnvType=Staging"

aws cloudformation deploy --stack-name ${stack_name} --template-file ${template_path} --parameter-overrides ${parameters} --capabilities CAPABILITY_IAM

実行が正常に終了すると、ECSサービスが実行されます。

また、構築したアプリケーションにアクセスしてみましょう。Frontend ALBのURLに「アプリケーションのパス/frontend/portal」を加えてアクセスすると、今回構築したアプリケーションにアクセスできます。

今回はECSサービスを構築するCloudFormationテンプレートを実装しました。次回は、CodeBuildを使ったCI(Continuous Integration)環境を構築するCloudFormationテンプレートを作成します。

著者紹介


川畑 光平(KAWABATA Kohei) - NTTデータ

金融機関システム業務アプリケーション開発・システム基盤担当、ソフトウェア開発自動化関連の研究開発を経て、デジタル技術関連の研究開発・推進に従事。

Red Hat Certified Engineer、Pivotal Certified Spring Professional、AWS Certified Solutions Architect Professional等の資格を持ち、アプリケーション基盤・クラウドなど様々な開発プロジェクト支援にも携わる。AWS Top Engineers & Ambassadors選出。

本連載の内容に対するご意見・ご質問は Facebook まで。