TerraformでCodePipeline設定 + Slack通知する方法(Ver.ECS Rolling update)

#概要

最近Terraformを利用してAWSインフラ構築する機会をいただいているため、
[CodePipelineでECSにデプロイする(Ver.rolling update)](https://qiita.com/jeayoon/items/74324f338725d29de23f)のTerraformバージョンを書いてみたいと思います。

あ、そして、
Merry christmas :)

#やること

この記事に書く内容を改めて説明させてください。
![スクリーンショット 2020-02-18 9 55 42](https://user-images.githubusercontent.com/17561411/74694804-e3180280-5234-11ea-9600-c9252ac4bb54.png)


上記のイメージにある赤いところです。
AWS CodePipelineでGitHub pushしてECSにRolling updateデプロイする方法と、AWS CodePipelineのStageごとにSlack通知する方法を全部Terraformで作成することです。

手順は下記のようになります。

- Terraform + AWS CodePipeline設定
    - AWS S3 bucket 設定
    - AWS S3 KMSキー 設定
    - AWS CodeBuild 設定
    - AWS CodePipeline 設定
- Terraform + AWS CodePipelineのSlack通知設定
    - AWS CloudWatch Event 設定
    - AWS SNS Topic 設定
    - AWS Lambda Function 設定

**この記事は**
**Terraform v0.12.9を使っています。**
**Terraform variable設定を省略します。**
**AWS ECR&ECSがすでに作成されていることが前提です。**
**Policy json fileはGithubにまとめたので、下記のリンクを参考してください。**
[CodePipeline Policy files](https://github.com/jeayoon/terraform-setup/tree/master/aws/codepipeline-slack-notifications/policies/codepipeline)

## Terraform + AWS CodePipeline設定

### AWS S3 bucket 設定

CodePipelineの動きにArtifact Storeというものがありますが、AWS S3をデプロイプロバイダとして使用しているため、S3 bucketが必要です。
ちなみに、AWSマネジメントコンソールから作成する場合、CodePipelineのArtifact Storeを設定するだけで勝手に作られると思います。

```tf
#--------------------------------------------------------------
# S3 bucket Setting
#--------------------------------------------------------------
resource "aws_s3_bucket" "artifact" {
  bucket = "${var.artifact_bucket_name}"
  acl    = "private"
}
```

### AWS S3 KMSキー 設定

CodePipelineはAWS S3 Artifact StoreとAWSが管理するSSE-KMS暗号化キーを作成します。Masterキーはオブジェクトデータとともに暗号化され、AWSによって管理されます。

```tf
#--------------------------------------------------------------
# s3kmskey Settings
#--------------------------------------------------------------

data "aws_kms_alias" "s3kmskey" {
  name = "alias/aws/s3"
}
```
### AWS CodeBuild 設定

CodePipelineのStageとして、別途に作成する必要があります。

```tf
#--------------------------------------------------------------
# CodeBuild Role
#--------------------------------------------------------------
data "template_file" "codebuild_assume_role" {
  template = "${file("./policies/codepipeline/codebuild_assume_role.json.tpl")}"
}
resource "aws_iam_role" "codebuild_role" {
  name = "codebuild-role"
  assume_role_policy = "${data.template_file.codebuild_assume_role.rendered}"
}

data "template_file" "codebuild_policy" {
  template = "${file("./policies/codepipeline/codebuild_policy.json.tpl")}"

  vars = {
    account_id = "${var.account_id}"
    codebuild_name = "${var.codebuild_name}"
    bucket_name = "${var.artifact_bucket_name}"
  }
}

resource "aws_iam_role_policy" "codebuild_policy" {
  role = "${aws_iam_role.codebuild_role.name}"
  policy = "${data.template_file.codebuild_policy.rendered}"
}

#--------------------------------------------------------------
# CodeBuild Settings
#--------------------------------------------------------------
resource "aws_codebuild_project" "main_build" {
  name          = "${var.codebuild_name}"
  description   = "create for codepipeline stage"
  build_timeout = "60"
  service_role  = "${aws_iam_role.codebuild_role.arn}"

  artifacts {
    type = "NO_ARTIFACTS"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                       = "aws/codebuild/standard:2.0"
    type                        = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"
    privileged_mode             = true
  }

  source {
    type            = "GITHUB"
    location        = "${var.github_project_url}" //GitHub project URL
    git_clone_depth = 1
    buildspec       = "${var.buildspec_path}"     //GitHubにあるbuildspec.ymlの場所
  }
}
```
### AWS CodePipeline 設定

AWS S3とCodeBuildの準備ができたので、これからCodePipelineを作成することができます。

```tf
#--------------------------------------------------------------
# CodePipeline Role
#--------------------------------------------------------------
data "template_file" "codepipeline_assume_role" {
  template = "${file("./policies/codepipeline/codepipeline_assume_role.json.tpl")}"
}

resource "aws_iam_role" "codepipeline_role" {
  name = "codepipeline-role"
  assume_role_policy = "${data.template_file.codepipeline_assume_role.rendered}"
}

data "template_file" "codepipeline_policy" {
  template = "${file("./policies/codepipeline/codepipeline_policy.json.tpl")}"
}
resource "aws_iam_role_policy" "codepipeline_policy" {
  name = "codepipeline-policy"
  role = "${aws_iam_role.codepipeline_role.id}"
  policy = "${data.template_file.codepipeline_policy.rendered}"
}

data "template_file" "codepipeline_s3_policy" {
  template = "${file("./policies/codepipeline/codepipeline_s3_policy.json.tpl")}"
  vars = {
    bucket_name = "${var.artifact_bucket_name}"
  }
}
resource "aws_s3_bucket_policy" "codepipeline_s3_policy" {
  bucket = "${aws_s3_bucket.artifact.id}"
  policy = "${data.template_file.codepipeline_s3_policy.rendered}"
}

#--------------------------------------------------------------
# CodePipeline Settings
#--------------------------------------------------------------
resource "aws_codepipeline" "main" {
  name     = "codepipeline-main"
  role_arn = "${aws_iam_role.codepipeline_role.arn}"

  artifact_store {
    location = "${aws_s3_bucket.artifact.bucket}"
    type     = "S3"

    encryption_key {
      id   = "${data.aws_kms_alias.s3kmskey.arn}"
      type = "KMS"
    }
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "ThirdParty"
      provider         = "GitHub"
      version          = "1"
      output_artifacts = ["source"]

      configuration = {
        Owner  = "${var.github_account_name}" // GitHub アカウント名
        OAuthToken = "${var.github_token}"    // GitHub Token
        Repo   = "${var.github_repository}"   // GitHubリポジトリ名
        Branch = "${var.github_branch}"       // GitHub push先
      }
    }
  }

  stage {
    name = "Build"

    action {
      name             = "Build"
      category         = "Build"
      owner            = "AWS"
      provider         = "CodeBuild"
      input_artifacts  = ["source"]
      output_artifacts = ["build"]
      version          = "1"

      configuration = {
        ProjectName = "${var.codebuild_name}" //CodeBuild名
      }
    }
  }

  stage {
    name = "Deploy"

    action {
      name            = "Deploy"
      category        = "Deploy"
      owner           = "AWS"
      provider        = "ECS"
      input_artifacts = ["build"]
      version         = "1"

      configuration = {
        ClusterName = "${var.ecs_cluster_name}"      //AWS ECS Cluster名
        ServiceName = "${var.ecs_service_name}"      //AWS ECS Service名
        FileName    = "${var.imagedefinitions_path}" //GitHubにあるimagedefinitions.json場所
      }
    }
  }
}
```
TerraformでCodePipelineを作成する時、`input_artifacts`と`output_artifacts`を理解しておいた方がいいと思います。

最初AWS S3設定のところで軽く説明しましたが、CodePipelineを作成する時に選択したAWS S3に保存されている`input&outputartifacts`が使われます。
CodePipeline​は、Stageのアクションタイプに応じて、`input_artifacts`または`output_artifacts`のファイルを圧縮して転送します。

例えば、Source Stageから`output_artifacts = ["source"]`Outputを行い、Build StageにInputとして`input_artifacts  = ["source"]`が取り込まれます。Build Stageから Deploy Stageも同じ動きになります。

## Terraform + AWS CodePipelineのSlack通知設定

### AWS CloudWatch Event 設定

AWS CodePipelineのStageごととそれぞれのStateに対するEvent Ruleを設定します。

```tf
#--------------------------------------------------------------
# CloudWatch Event Settings
#--------------------------------------------------------------

resource "aws_cloudwatch_event_rule" "main" {
  name = "codepipeline-notifications-rule"

  event_pattern = <<PATTERN
{
  "source": [
    "aws.codepipeline"
  ],
  "detail-type": [
    "CodePipeline Stage Execution State Change"
  ],
  "detail": {
    "state": ["STARTED", "SUCCEEDED", "FAILED"]
  }
}
PATTERN
}

resource "aws_cloudwatch_event_target" "main" {
  rule      = "${aws_cloudwatch_event_rule.main.name}"
  target_id = "SendToSNS"
  arn       = "${aws_sns_topic.main.arn}" //AWS SNS Topic 設定セッションから続く
}

```
ちなみに、`event_pattern`はAWSマネジメントコンソールからService NameとEvent Typeを設定すると、
Event Pattern Previewというところで確認できるので、それをCopyしたらいいと思います。

### AWS SNS Topic 設定

AWS Lambda Functionにメッセージを転送するために AWS SNSを作成します。

```tf

#--------------------------------------------------------------
# AWS SNS TOPIC
#--------------------------------------------------------------
resource "aws_sns_topic" "main" {
  name         = "codepipeline-notifications"
  display_name = "CodePipeline notifications"
}

#--------------------------------------------------------------
# AWS SNS TOPIC POLICY
#--------------------------------------------------------------
resource "aws_sns_topic_policy" "main" {
  arn = "${aws_sns_topic.main.arn}"

  policy = <<EOF
{
  "Version": "2012-10-17",
  "Id": "__default_policy_ID",
  "Statement": [
    {
      "Sid": "AWSEvents_smebiz-codepipeline-events_SendToSNS",
      "Effect": "Allow",
      "Principal": {
        "Service": "events.amazonaws.com"
      },
      "Action": "sns:Publish",
      "Resource": "${aws_sns_topic.main.arn}"
    }
  ]
}
EOF
}
```

### AWS Lambda Function 設定

そして、Slackに通知できるようにLambda Functionを作成します。

```tf

#--------------------------------------------------------------
# AWS Lambda FUNCTION IAM ROLE
#--------------------------------------------------------------
data "aws_iam_policy_document" "lambda_assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "lambda" {
  name               = "lambda-role"
  assume_role_policy = "${data.aws_iam_policy_document.lambda_assume_role.json}"
}


data "aws_iam_policy_document" "lambda_permissions" {
  statement {
    actions = [
      "logs:CreateLogGroup",
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = [
      "arn:aws:logs:*:*:*",
    ]
  }
}

resource "aws_iam_role_policy" "lambda" {
  name   = "lambda-policy"
  role   = "${aws_iam_role.lambda.id}"
  policy = "${data.aws_iam_policy_document.lambda_permissions.json}"
}

#--------------------------------------------------------------
# AWS Lambda FUNCTION
#--------------------------------------------------------------
resource "aws_lambda_function" "main" {
  handler          = "index.handler"
  runtime          = "nodejs10.x"
  function_name    = "codepipeline-notifications-function"
  filename         = "${path.module}/functions/notifications/dist.zip"
  role             = "${aws_iam_role.lambda.arn}"
  source_code_hash = "${filebase64sha256("${path.module}/functions/notifications/dist.zip")}"

  environment {
    variables = {
      SLACK_WEBHOOK_URL = "${var.slack_webhook}"
    }
  }
}

resource "aws_lambda_permission" "main" {
  statement_id  = "AllowExecutionFromSNS"
  action        = "lambda:InvokeFunction"
  principal     = "sns.amazonaws.com"
  function_name = "${aws_lambda_function.main.function_name}"
  source_arn    = "${aws_sns_topic.main.arn}"
}

resource "aws_sns_topic_subscription" "main" {
  topic_arn = "${aws_sns_topic.main.arn}"
  protocol  = "lambda"
  endpoint  = "${aws_lambda_function.main.arn}"
}
```

Node.jsコードは[smebizdev/terraform-aws-codepipeline-slack-notifications](https://github.com/smebizdev/terraform-aws-codepipeline-slack-notifications/tree/master/functions/notifications)を使っています。

##動作確認

上記の通りに作成できたら、`terraform plan` > `terraform apply`を行うと、
CodePipelineがGitHubのSource Stage実行をトリガーし、そのままSlackでCI/CDの確認ができると思います。もちろんCodePipelineの方でも確認ができます。

イメージにモザイク処理が多くて、わかりにくいと思いますが、情報としては
`Stage ${stage} (${CodePipeline Name})is now ${state} : 
https://${region}.console.amazon.com/codepipeline/home?region=${region}#/view/${CodePipeline Name}`
となります。

![terraform_deploy_merge](https://user-images.githubusercontent.com/17561411/71312072-520df080-246a-11ea-87fc-bfb27dc86a0d.png)

##感想

AWS CodePipelineをIaC化することができ、非常に勉強になりました。
特に色んなバグを出しながら、解決へたどり着くまでに、AWSサービスの理解とTerraform文法を学ぶことができました。

最初AWSマネジメントコンソールと全く違って、細かい設定まで行うのが難しかったです。
例えば、S3 Bucket設定を抜けたり、Policyが足りなかったりするなど、AWS環境が動いていない原因探しまでがきつかったですが、
AWSを理解することができて、もっと面白くなりました。

これからは、Terraformをよりスマートに使えるように、ディレクトリ構成など、細かいところをやってみたいと思います。


#参考

- [AWS CodePipeline ユーザーガイド](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/welcome.html)
- [Terraform CodePipelineドキュメント](https://www.terraform.io/docs/providers/aws/r/codepipeline.html)
- [CodePipeline サービスロールの管理](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/how-to-custom-role.html)
- [AWS CodePipeline & Slack integration](https://medium.com/@krishnakuntala/aws-codepipeline-slack-integration-41dfaff2414e)