ECS向けのCloudFormationテンプレートを割と簡単にAWS Fargateで動くようにする


# はじめに
* AWS Fargateが東京リージョンに対応しましたね。
* 別件ですが、本日Amazon EFSが東京リージョンに対応しましたね。
* もろもろ胸熱です。（因みに、現時点ではFargateからEFSのmountは行なえません。
* 本記事では、 ~~一生懸命作った~~ ECS向けのCloudFormationテンプレートが割と簡単にFargateに対応できたので手順を書いておきます。


# 概要
* ベースとなるテンプレートは、aws-samplesにある[ecs-refarch-cloudformation](https://github.com/aws-samples/ecs-refarch-cloudformation)です。
* 当該テンプレートでデプロイすると以下のような構成になります。
![2018-07-12_14h55_01.png](https://qiita-image-store.s3.amazonaws.com/0/39596/2e12cbac-5a07-40c8-84b9-b9f473b6980d.png "2018-07-12_14h55_01.png")
* 上記の構成をFargateに対応させるので、以下の構成になる想定です。
![2018-07-12_17h37_21.png](https://qiita-image-store.s3.amazonaws.com/0/39596/d8abcacf-71f4-e9a5-c647-8bf4bf16d4b7.png "2018-07-12_17h37_21.png")
* 対応後のテンプレートは以下に雑に上げておきます。<br/>
https://github.com/ukitiyan/ecs-refarch-cloudformation/tree/future/support-fargate

# 対応手順
## テンプレート取得
1. ベースとなるテンプレートをCloneします。

    ```
    $ git clone git@github.com:aws-samples/ecs-refarch-cloudformation.git
    $ cd ecs-refarch-cloudformation
    $ code .
    ```

## テンプレート修正

### /service/product-service/service.yamlと/service/website-service/service.yamlの修正

#### AWS::ECS::Serviceセクション

修正後は以下の内容になります。

```service.yaml
Service: 
    Type: AWS::ECS::Service
    # Role: !Ref ServiceRole
    DependsOn: ListenerRule
    Properties: 
        Cluster: !Ref Cluster
        DesiredCount: !Ref DesiredCount
        TaskDefinition: !Ref TaskDefinition
        LoadBalancers: 
            - ContainerName: "product-service"
              ContainerPort: 8001
              TargetGroupArn: !Ref TargetGroup
        LaunchType: FARGATE
        NetworkConfiguration:
          AwsvpcConfiguration:
            AssignPublicIp: DISABLED
            SecurityGroups: !Ref ServiceSecurityGroup
            Subnets: !Ref Subnets
```

1. サービスのLaunchTypeにFARGATEの指定を追加します。

    ```
    LaunchType: FARGATE
    ```

1. Fargateタスクはawsvpcネットワーキングモードを利用することになるのでNetworkConfigurationの指定を追加します。ENIが付くのでサービス毎にセキュリティグループが指定出来ます。ここ重要ですね。

    ```
    NetworkConfiguration:
      AwsvpcConfiguration:
        AssignPublicIp: DISABLED
        SecurityGroups: !Ref ServiceSecurityGroup
        Subnets: !Ref Subnets
    ```

1. `You cannot specify an IAM role for services that require a service linked role.` と怒られるので、ServiceRoleの指定をコメントアウトします。

    ```
    # Role: !Ref ServiceRole
    ```


#### AWS::ECS::TaskDefinitionセクション

修正後は以下の内容になります。

```yaml:service.yaml
TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
        Family: product-service
        Cpu: 256
        Memory: 512
        ContainerDefinitions:
            - Name: product-service
              Essential: true
              Image: 275396840892.dkr.ecr.us-east-1.amazonaws.com/ecs-refarch-cloudformation/product-service:1.0.0
              Cpu: 256
              Memory: 512
              PortMappings:
                - ContainerPort: 8001
              LogConfiguration:
                LogDriver: awslogs
                Options:
                    awslogs-group: !Ref AWS::StackName
                    awslogs-region: !Ref AWS::Region
                    awslogs-stream-prefix: product-service
        TaskRoleArn:
            Fn::If:
            - 'HasCustomTaskRole'
            - !Ref 'TaskRole'
            - !Ref "AWS::NoValue"
        ExecutionRoleArn: !Ref ExecutionRole
        NetworkMode: awsvpc
        RequiresCompatibilities:
            - FARGATE
```

1. 起動タイプにFARGATEの指定を追加します。

    ```
    RequiresCompatibilities:
        - FARGATE
    ```

1. NetworkModeにawsvpcの指定を追加します。

    ```
    NetworkMode: awsvpc
    ```

1. 必須ではないですが、TaskRoleとExecutionRoleの指定を追加します。

    ```
    TaskRoleArn:
        Fn::If:
        - 'HasCustomTaskRole'
        - !Ref 'TaskRole'
        - !Ref "AWS::NoValue"
    ExecutionRoleArn: !Ref ExecutionRole
    ```

1. `Fargate requires log configuration options to include awslogs-stream-prefix` と怒られるので 適当な値で `awslogs-stream-prefix` の指定を追加します。

    ```
    LogConfiguration:
      LogDriver: awslogs
      Options:
          awslogs-group: !Ref AWS::StackName
          awslogs-region: !Ref AWS::Region
          awslogs-stream-prefix: product-service
    ```

1. `Fargate requires that 'cpu' be defined at the task level.`と怒られるので、以下のURLを参考にCPUとついでにメモリの指定を追加します。<br>
https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-cpu-memory-error.html

    ```
    TaskDefinition:
        Type: AWS::ECS::TaskDefinition
        Properties:
            Family: product-service
            Cpu: 256
            Memory: 512
    ```

#### AWS::ElasticLoadBalancingV2::TargetGroupセクション

修正後は以下の内容になります。

```yaml:service.yaml
TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
        VpcId: !Ref VPC
        Port: 80
        Protocol: HTTP
        Matcher: 
            HttpCode: 200-299
        HealthCheckIntervalSeconds: 10
        HealthCheckPath: /products
        HealthCheckProtocol: HTTP
        HealthCheckTimeoutSeconds: 5
        HealthyThresholdCount: 2
        TargetType: ip
```

1. awsvpc ネットワークモードを使用する場合、ターゲットグループのターゲットタイプとして ip を指定する必要があります。[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task-networking.html)を参照。そのため、TargetTypeに ip の指定を追加します。

    ```
    TargetType: ip
    ```

1. 最後に必要なParameters、Conditionsの定義を追加します。

    ```
    Parameters:
    ・・・
        ServiceSecurityGroup:
            Description: The ServiceSecurityGroup
            Type: List<AWS::EC2::SecurityGroup::Id>

        Subnets:
            Description: Choose which subnets this ECS cluster should be deployed to
            Type: List<AWS::EC2::Subnet::Id>
        
        TaskRole:
            Description: The ECS task role ARN
            Type: String
            Default: ""
    
        ExecutionRole:
            Description: The ECS Fargate Execution role ARN
            Type: String

    Conditions:
        HasCustomTaskRole: !Not [ !Equals [!Ref 'TaskRole', ''] ]
    ```

### /infrastructure/ecs-cluster-fargate.yamlの追加

ecs-cluster.yamlから大きく変わるのでecs-cluster-fargate.yamlという新しいファイルを作ります。修正後は、以下リンク先の内容になります。

https://github.com/ukitiyan/ecs-refarch-cloudformation/blob/future/support-fargate/infrastructure/ecs-cluster-fargate.yaml

1. ECSClusterを指定します。ecs-cluster.yamlと同じ内容です。

    ```
    ECSCluster:
        Type: AWS::ECS::Cluster
    ```

1. ECSServiceAutoScalingRoleを指定します。ecs-cluster.yamlと同じ内容です。

    ```
    ECSServiceAutoScalingRole:
        Type: AWS::IAM::Role
    ```

1. タスク定義で設定するためのECSTaskExecutionRole、ECSProductTaskRole、ECSWebsiteTaskRoleを指定します。

    ```
    ECSTaskExecutionRole:
        Type: AWS::IAM::Role
    
    ECSProductTaskRole:
        Type: AWS::IAM::Role
    
    ECSWebsiteTaskRole:
        Type: AWS::IAM::Role
    ```


### /master.yamlの修正

修正後は、以下リンク先の内容になります。

https://github.com/ukitiyan/ecs-refarch-cloudformation/blob/future/support-fargate/master.yaml

1. ECSクラスター用のスタックのパスをecs-cluster-fargate.yamlに変更します。あと不要なParametersを削除します。

    ```
    TemplateURL: https://s3.amazonaws.com/ecs-refarch-cloudformation/infrastructure/ecs-cluster-fargate.yaml
    ```

1. ProductService、WebsiteService用のスタックに引き渡す必要のあるParametersを追加します。

    ```
    ServiceSecurityGroup: !GetAtt SecurityGroups.Outputs.ECSHostSecurityGroup
    Subnets: !GetAtt VPC.Outputs.PrivateSubnets
    TaskRole: !GetAtt ECS.Outputs.ECSProductTaskRole
    ExecutionRole: !GetAtt ECS.Outputs.ECSTaskExecutionRole
    ```

1. AutoScaleでホストのDrainingを気にする必要が無くなるので、そこら辺を設定しているLifecycleHookのスタックしていをコメントアウトします。

    ```
    # LifecycleHook:
    #     Type: AWS::CloudFormation::Stack
    #     Properties:
    #         TemplateURL: https://s3.amazonaws.com/ecs-refarch-cloudformation/infrastructure/lifecyclehook.yaml
    #         Parameters:
    #             Cluster: !GetAtt ECS.Outputs.Cluster
    #             ECSAutoScalingGroupName: !GetAtt ECS.Outputs.ECSAutoScalingGroupName
    ```


# ビルド&デプロイ
1. CodeBuildで検証とビルドを行うためのbuildspec.ymlが用意されているので、CodeBuildします。
1. ついでにCodeBuildの環境変数の `S3_BUCKET` に当該テンプレートを配置するS3バケット名を指定して、ビルド実行すれば当該テンプレートがS3に配置されるよう対応しておきました。[こちら](https://github.com/ukitiyan/ecs-refarch-cloudformation/blob/future/support-fargate/buildspec.yml)
1. 上記で配置されたテンプレートのmaster.yamlのURLを指定して、CloudFoamationを実行すれば目的の環境が構成されます。
1. 以下のようなURLがCloudFormationのOutputで確認できるので、アクセスして画面表示されることを確認します。<br>
    http://fargate-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com/<br>
    http://fargate-xxxxxxxxx.ap-northeast-1.elb.amazonaws.com/products

# まとめ
* 割と簡単にFargateに対応できたと思います。
* このテンプレートを拡張していけば良いわけです。