[AWS] AWSCloudFormationでCI/CDパイプライン(CodePipeLine)を構築する

# はじめに

今までコンソール画面でCI/CDパイプラインをぽちぽちと構築していたのですが、プロジェクトが増えるたびにデプロイフローが増えるので、「またぽちぽちしないといけんのか…正直めんどい。そうか、CloudFormationでテンプレート化してしおう」と思い立ち、CloudFormationを利用して簡単に構築できるようにしました。
結果、構築する工数が減ったってのと、基本的に同じような構成になるので、デプロイ周りの治安を改善することができました。

## デプロイフローはこんな感じ

![スクリーンショット 2018-04-12 16.24.29.png](https://qiita-image-store.s3.amazonaws.com/0/95875/ac29d82f-276a-d26c-5245-0bc90ba6587c.png)

### 登場人物

|サービス名|ざっくり説明|
|:---------|:---------|
|AWS CloudFormation|今回の主人公|
|AWS CodePipeLine|CI/CDパイプライン|
|AWS CodeCommit|ソースコードのリポジトリ|
|AWS CodeBuild|テストを実行して、完了したらS3にアーカイブする|
|AWS CodeDeploy|DockerImageをCreateしてECRにPushする|
|Amazon ECR|DockerImageのリポジトリ|
|AWS ElasticBeanstalk|アプリケーションが実行されている環境|

## CloudFormationとは？

あるテンプレートファイルを元にStackというタスクを作成し、AWSのリソースをデプロイ、プロビジョニングしてくれるサービスです。
詳細は[公式ドキュメント](https://aws.amazon.com/jp/documentation/cloudformation/)を呼んでください。

## 前提条件

- AWSCLIインストール済み / Credential設定済み
- (必要ならば)DockerイメージをCreateしてPushしてくれる、CodeDeployAgentがインストールされているEC2
    → [こちら](https://docs.aws.amazon.com/ja_jp/codedeploy/latest/userguide/codedeploy-agent-operations-install-linux.html)を参考に `CodeDeployAgent` をインストール、そのあとDockerをインストールしてください。

以上。

## テンプレートファイル

CloudFormationのテンプレートファイルはこんな感じ

```yml:template.yml
AWSTemplateFormatVersion: 2010-09-09

# 各種パラメータ
Parameters:
  AccountID:
    Type: String
    Default: ""

# 各種リソース
Resources:

  # ECRのRepositoryを作成
  # リポジトリポリシーに特定のアカウントからのアクセスのみを許可するように設定
  # rootにしていますが特定のuserだけに絞りたいときは user/hoge のように書き換えてください
  Repository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: "sample_repository"
      RepositoryPolicyText:
        Version: "2012-10-17"
        Statement:
          -
            Sid: cfnSample
            Effect: "Allow"
            Principal:
              AWS:
                - !Join ["", ["arn:aws:iam::", !Ref AccountID, ":root"]]
            Action:
              - "ecr:GetDownloadUrlForLayer"
              - "ecr:BatchGetImage"
              - "ecr:BatchCheckLayerAvailability"

  # CodeBuildに適用するIAMRole
  # 必要に応じて内容を変更してください
  CodeBuildServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codebuild.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource: "*"
                Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                  - dynamodb:*
                  - ses:*
              - Resource: !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:GetObject
                  - s3:PutObject
                  - s3:GetObjectVersion

  # 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: root
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Resource:
                  - !Sub arn:aws:s3:::${ArtifactBucket}/*
                Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketVersioning
              - Resource: "*"
                Effect: Allow
                Action:
                  - codebuild:StartBuild
                  - codebuild:StopBuild
                  - codebuild:BatchGet*
                  - codebuild:Get*
                  - codebuild:List*
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:ListBranches
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                  - codecommit:CancelUploadArchive
                  - codedeploy:CreateDeployment
                  - codedeploy:GetApplicationRevision
                  - codedeploy:GetDeployment
                  - codedeploy:GetDeploymentConfig
                  - codedeploy:RegisterApplicationRevision
                  - s3:GetBucketLocation
                  - s3:ListAllMyBuckets
                  - iam:PassRole
              - Resource: "arn:aws:ssm:*:*:parameter/CodeBuild/*"
                Effect: Allow
                Action:
                  - ssm:PutParameter

  # CodeDeployに適用するIAMRole
  # 必要に応じて内容を変更してください
  # すでにAWS側が用意してくれているPolicyを指定
  CodeDeployServiceRole:
    Type: AWS::IAM::Role
    Properties:
      Path: /
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: codedeploy.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole

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

  # CodeBuild
  CodeBuildProject:
    Type: AWS::CodeBuild::Project
    Properties:
      Artifacts:
        Type: CODEPIPELINE
      Source:
        Type: CODEPIPELINE
      Environment:
        ComputeType: BUILD_GENERAL1_SMALL
        Image: aws/codebuild/nodejs:6.3.1
        Type: LINUX_CONTAINER
      Name: !Ref AWS::StackName
      ServiceRole: !Ref CodeBuildServiceRole

  # CodeDeploy
  CodeDeployAplication:
    Type: AWS::CodeDeploy::Application
    Properties:
      ApplicationName: "build-test-app"

  CodeDeployGroup:
    Type: AWS::CodeDeploy::DeploymentGroup
    Properties:
      ApplicationName: !Ref CodeDeployAplication
      DeploymentGroupName: "build-test-group"
      DeploymentConfigName: CodeDeployDefault.OneAtATime
      Ec2TagFilters:
        - Type: KEY_AND_VALUE
          Key: "Name"
          Value: "docker-ue1"
      ServiceRoleArn: !GetAtt [CodeDeployServiceRole, Arn]

  # CodePipeLine
  # 内容は CodeCommit → CodeBuild → CodeDeploy の順で処理をして、最終的にECRにDockerイメージをPushする
  # DockerイメージをPushするのはCodeDeploy
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      RoleArn: !GetAtt CodePipelineServiceRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactBucket
      Stages:
        - Name: Source
          Actions:
            - Name: SourceAction
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration:
                RepositoryName: IoT.kyoto
                BranchName: build-test
              RunOrder: 1
              OutputArtifacts:
                - Name: App
        - Name: Build
          Actions:
            - Name: Build
              ActionTypeId:
                Category: Build
                Owner: AWS
                Version: 1
                Provider: CodeBuild
              Configuration:
                ProjectName: !Ref CodeBuildProject
              RunOrder: 1
              InputArtifacts:
                - Name: App
              OutputArtifacts:
                - Name: MyApp
        - Name: Deploy
          Actions:
            - Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: CodeDeploy
              Configuration:
                ApplicationName: !Ref CodeDeployAplication
                DeploymentGroupName: !Ref CodeDeployGroup
              RunOrder: 1
              InputArtifacts:
                - Name: MyApp

```

## Stackを作成する

パラメータ `AccountID` は、 `eb deploy` する先のアカウントIDです。
`--stack-name` でStackの名前を指定します。
`--region` でCloudFormationのStackを作成するリージョンを設定する

```console
$ aws cloudformation create-stack --template-body file://codepipeline-template.yml \
    --stack-name cfn-sample --region us-east-1 \
    --capabilities CAPABILITY_NAMED_IAM \
    --parameters ParameterKey=AccountID,ParameterValue=1234567890123 
```

Stackが正常に作成されると、CodePipeLineでデプロイ処理開始されます。


## Stackを削除する

`--stack-name` でStackの名前を指定します。
`--region` で削除するCloudFormationのStackのリージョンを設定する

```console
$ aws cloudformation delete-stack --stack-name cfn-sample --region us-east-1
```

**[注意]** 削除対象のECRとS3Bucketが空でなければdelete-stackは失敗しますので、ECRとBucketの中身を綺麗にしてからDeleteするようにしてください。

## さいごに

こうしてCI/CDパイプライン構築をテンプレート化することができました。美しいですね。
標準化することができたこともあって、構築するときに全くわけのわからない構築をされる心配が減り、治安が改善されたと思います。

真面目な話をすると、今回CloudFormationを初めて触ったのですが、普段コンソール画面でぽちぽちしている裏でどんな設定がされているのかという部分の一面を見ることができました。
また今回構築した各サービスの理解も深まり良かったと思います。
今後もテンプレート化できそうな構成があれば、どんどんCloudFormationを活用していこうと思います！

ではまた！