CodePipelineでCloudFormationを利用してECSをデプロイ

自分のメモとして残します。

## 手動でECSを立ち上げる

### サンプルアプリ

今回利用するアプリをローカルで実行してみます。

```javascript:app.js
const http = require('http')

var app = function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello\n');
};

http.createServer(app).listen(3000);
```

```dockerfile:Dockerfile
FROM node:12

WORKDIR /usr/src/app

COPY app.js .

EXPOSE 3000
CMD [ "node", "app.js" ]
```

試しに実行してみます。

```console:実行
$ docker build -t ecs_node:latest .
$ docker run -p 3000:3000 -d ecs_node:latest

$ curl localhost:3000
Hello
```

### CloudFormationテンプレート
各種テンプレートです。

```yaml:ecr.yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  Repository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: deploy-test-repository

Outputs:
  Repository:
    Description: Test Repository
    Value: !Ref Repository
    Export: 
      Name: DeployTestRepository
```


```yaml:ecs-cluster.yaml
AWSTemplateFormatVersion: '2010-09-09'

Resources:
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: deploy-test-cluster

Outputs:
  Cluster:
    Description: ECS Cluster
    Value: !Ref Cluster
    Export: 
      Name: DeployTestCluster
```

ECSサービスのテンプレートファイルです。
DockerImageは更新されることが想定されるため、パラメータとして渡せるようにしています。

```yaml:ecs-service.yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SecurityGroup:
    Type: String
  Subnet:
    Type: String
  DockerImage:
    Type: String

Resources:

  # Fargate の起動に必要な権限
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: ecs-tasks.amazonaws.com
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Path: /

  # Fargate（アプリケーション）に付与する権限
  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: ecs-tasks.amazonaws.com
      ManagedPolicyArns:
      - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
      Path: /

  # Task定義
  ApTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      # Family: !Ref AWS::StackName
      ExecutionRoleArn: !GetAtt ExecutionRole.Arn
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: app
          Image: !Ref DockerImage
          Essential: true
          PortMappings:
            - ContainerPort: 3000
      Cpu: 256
      Memory: 512
      NetworkMode: awsvpc
      TaskRoleArn: !GetAtt TaskRole.Arn

  # Service定義
  FargateService:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !ImportValue DeployTestCluster
      LaunchType: FARGATE
      DesiredCount: 1
      TaskDefinition: !Ref ApTaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          SecurityGroups:
            - !Ref SecurityGroup
          Subnets:
            - !Ref Subnet
```

### スタック作成

まずリポジトリとECSクラスターを作成します。

```console:
$ aws cloudformation deploy --template ecr.yaml --stack-name create-ecr

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - create-ecr

$ aws cloudformation deploy --template ecs-cluster.yaml --stack-name create-ecs-cluster

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - create-ecs-cluster

```

続いて作成したECRにサンプルアプリのDockerイメージをpushします。
詳細はこちらをどうぞ [Dockerイメージをレジストリに登録する(ECR含む)](https://qiita.com/YutaSaito1991/items/79e8715a6c9cbaa93ec1#ecrにpushする
)

```console:
$ $(aws ecr get-login --no-include-email)
$ aws ecr describe-repositories
{
    "repositories": [
        {
            "repositoryArn": "arn:aws:ecr:ap-northeast-1:XXXXXXXX:repository/deploy-test-repository",
            "registryId": "XXXXXXXX",
            "repositoryName": "deploy-test-repository",
            "repositoryUri": "XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/deploy-test-repository",
            "createdAt": 1592618305.0
        }
    ]
}

$ docker image tag ecs_node:latest XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/deploy-test-repository:init
$ docker push XXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/deploy-test-repository:init
```

デプロイ

```console:
$ aws cloudformation deploy --template ecs-service.yaml --stack-name create-ecs-service --capabilities CAPABILITY_IAM --parameter-overrides DockerImage=XXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/deploy-test-repository:init
```

デプロイ結果の確認
IPアドレスはECSをコンソールで開いて確認しました。
なぜか`aws ecs describe-tasks`だと表示されませんでした・・・

```console:
$ curl http://XXX.XXX.XXX.XXX:3000/
Hello
```

## CodePiepelineの作成

作業の前にGithubでアクセストークンを設定してください。

私は以下参考にして設定しました。
[CFnでGitHub + Fargate + CodePipelineを構築してみる
](https://dev.classmethod.jp/articles/cfn-github-fargate-codepipeline/)


### テンプレートファイルの作成

```yaml:code-pipeline-yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  SecurityGroup:
    Type: String
  Subnet:
    Type: String
  GitHubRepositoryName:
    Type: String
  GitHubAccountName:
    Type: String
  GitHubOAuthToken:
    Type: String

Resources:

  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: codebuild.amazonaws.com
      Policies:
        - PolicyName: code-build-service
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - cloudformation:ValidateTemplate
                Resource: "*"
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - ecr:GetAuthorizationToken
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - ecr:BatchCheckLayerAvailability
                  - ecr:PutImage
                  - ecr:InitiateLayerUpload
                  - ecr:UploadLayerPart
                  - ecr:CompleteLayerUpload
                Resource: "*"
      Path: /

  # アプリケーションのテストやビルドの定義（開発環境）
  CodeBuild:
    Type: AWS::CodeBuild::Project
    Properties:
      Name: deploy-test-build
      ServiceRole: !Ref CodeBuildServiceRole
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Type: LINUX_CONTAINER
        Image: aws/codebuild/docker:18.09.0-1.7.0
        PrivilegedMode: true
        EnvironmentVariables:
          - Name: REPOSITORY_URI
            Value: !Sub 
              - ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${Repository}
              - Repository: !ImportValue DeployTestRepository
          - Name: Subnet
            Value: !Ref Subnet
          - Name: SecurityGroup
            Value: !Ref SecurityGroup
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
        BuildSpec: ecs_deploy/application/deploy/buildspec.yaml

  # CodePipelineに適用するIAMRole
  CodePipelineServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codepipeline.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: code-pipeline-service
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource: "*"
                Effect: Allow
                Action:
                  - codecommit:GetRepository
                  - codecommit:ListBranches
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                  - codecommit:CancelUploadArchive
                  - cloudformation:CreateChangeSet
                  - cloudformation:CreateStack
                  - cloudformation:CreateUploadBucket
                  - cloudformation:DeleteStack
                  - cloudformation:Describe*
                  - cloudformation:List*
                  - cloudformation:UpdateStack
                  - cloudformation:ValidateTemplate
                  - cloudformation:ExecuteChangeSet
                  - codebuild:StartBuild
                  - codebuild:StopBuild
                  - codebuild:BatchGet*
                  - codebuild:Get*
                  - codebuild:List*
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - s3:*
                  - iam:PassRole

  # S3Bucket
  ArtifactBucket:
    Type: AWS::S3::Bucket

  # 外部イベント発生のwebhook
  PipelineWebhook:
    Type: "AWS::CodePipeline::Webhook"
    Properties:
      Authentication: GITHUB_HMAC
      AuthenticationConfiguration:
        SecretToken: "secret"
      Filters:
        - JsonPath: "$.ref"
          MatchEquals: "refs/heads/master"
      TargetPipeline: !Ref Pipeline
      TargetAction: SourceAction
      Name: GitHubPipelineWebhook
      TargetPipelineVersion: !GetAtt Pipeline.Version
      RegisterWithThirdParty: "true"

  # 継続的デプロイに必要な権限
  CloudFormationRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: cloudformation.amazonaws.com
      Policies:
        - PolicyName: code-pipeline-service
          PolicyDocument:
            Statement:
              - Effect: Allow
                Action:
                  - iam:*
                  - ecs:RegisterTaskDefinition
                  - ecs:DeregisterTaskDefinition
                  - ecs:CreateService
                  - ecs:Delete*
                  - logs:Delete*
                  - cloudwatch:Delete*
                Resource: "*"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceAutoscaleRole
        - arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole
      Path: /

  # CodePipeLine
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      Name: pipeline-test-build
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: ThirdParty
                Version: 1
                Provider: GitHub
              Configuration:
                Owner: !Ref GitHubAccountName
                Repo: !Ref GitHubRepositoryName
                PollForSourceChanges: false
                Branch: master
                OAuthToken: !Ref GitHubOAuthToken
              RunOrder: 1
              OutputArtifacts:
                - Name: SourceCode
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuild
              RunOrder: 1
              InputArtifacts:
                - Name: SourceCode
              OutputArtifacts:
                - Name: BuildOutput
        - Name: Deploy
          Actions:
          Actions:
            - Name: CreateEdgeChangeSet
              ActionTypeId:
                Category: Deploy
                Provider: CloudFormation
                Owner: AWS
                Version: 1
              Configuration:
                StackName: create-ecs-service
                ActionMode: CHANGE_SET_REPLACE
                ChangeSetName: ChangeSet
                RoleArn: !GetAtt CloudFormationRole.Arn
                Capabilities: CAPABILITY_NAMED_IAM
                TemplatePath: SourceCode::ecs_deploy/application/deploy/ecs-service.yaml
                TemplateConfiguration: BuildOutput::config.json
              InputArtifacts:
                - Name: SourceCode
                - Name: BuildOutput
              OutputArtifacts:
                - Name: CreatedEdgeChangeSet
              RunOrder: 1
            - Name: ExecuteEdgeChangeSet
              ActionTypeId:
                Category: Deploy
                Provider: CloudFormation
                Owner: AWS
                Version: 1
              Configuration:
                StackName: create-ecs-service
                ActionMode: CHANGE_SET_EXECUTE
                ChangeSetName: ChangeSet
              InputArtifacts:
                - Name: CreatedEdgeChangeSet
              OutputArtifacts:
                - Name: EdgeDeployed
              RunOrder: 2

```

スタック作成します。
各種パラメータもここで設定して実行します。

```console:
$ aws cloudformation deploy --template code-pipeline.yaml --stack-name create-code-pipeline --parameter-overrides SecurityGroup=sg-XXXXXX Subnet=subnet-XXXXXXXX GitHubRepositoryName=XXXX GitHubAccountName=XXXXX GitHubOAuthToken=XXXXXXXXXXXXXXXXXXXXXXX --capabilities CAPABILITY_IAM
```

### buildSpecの作成
buildSpecを作成します。

途中でDEFAULT変数にパスを格納して戻っていますが、最後にconfig.jsonを出力する際に、初期の場所で吐き出さないとエラーになってしまうからです。
ここで出力したファイルは、CloudFormationを更新するときのパラメータとして利用されます。

```yaml:buildspec.yaml
version: 0.2

phases:
  pre_build:
    commands:
      - IMAGE_NAME="${REPOSITORY_URI}:$(echo ${CODEBUILD_RESOLVED_SOURCE_VERSION} | head -c 7)"
      - $(aws ecr get-login --no-include-email)
  build:
    commands:
      - echo Build started on $(date)
      - DEFAULT=`pwd`
      - cd ./ecs_deploy/application
      - docker build --tag ${IMAGE_NAME} .
      - docker push ${IMAGE_NAME}
      - cd ${DEFAULT}
  post_build:
    commands:
      - echo Build completed on $(date)
      - printf '{"Parameters":{"Subnet":"%s","SecurityGroup":"%s","DockerImage":"%s"}}' ${Subnet} ${SecurityGroup} ${IMAGE_NAME} > config.json

artifacts:
  files: config.json
```


### 結果確認

対象リポジトリを修正してpushすれば修正した内容がデプロイされるようになります！！

```javascript:app.js
const http = require('http')

var app = function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Goodmorning\n');
};

http.createServer(app).listen(3000);
```

```console:
$ curl http://YYY.YYY.YYY.YYY:3000/
Goodmorning
```

今回は省きましたが、ECS Service Discoveryを利用することで新しく作成されたIPをRoute53に登録可能です。


## 参考情報
[Jawsug Fargate アプリケーションの継続的デリバリー](https://github.com/jawsug-container/hands-on/blob/master/fargate/)
[Classmethod CloudFormationのスタック間でリソースを参照する
](https://dev.classmethod.jp/articles/cfn-github-fargate-codepipeline/)
[AWSユーザーガイド ECS リソースタイプのリファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/AWS_ECS.html)


今回のコードは以下に配置しております。
https://github.com/uc4w6c/aws-test/tree/master/ecs_deploy