CircleCIとTerraformでアカウント管理のコード化とデプロイ自動化

## 概要
[Terraform](https://www.terraform.io)のプロジェクトをCI/CDする情報があまりなかったので、アカウント管理用のプロジェクトを題材に試してみました。
管理対象のアカウントはAWSのIAMユーザーとGitHub Organizationのメンバーです。

## AWSの準備

### Terraform実行ユーザの作成

Terraformを実行するIAMユーザーを作成し、適切なIAMポリシーを割り当てます。
権限の範囲を気にしないのであれば、公式のIAMFullAccessとS3FullAccessのポリシーを割り当てれば十分です。ユーザー作成後はアクセスキー(アクセスキーIDとシークレットアクセスキー)を発行しておきます。

### tfstateを保存するS3バケットの作成

CircleCI上でTerraformを動かす場合、tfstateを永続化する必要があります。
今回はS3にtfstateを保存し、実行時に共有する構成にします。
このtfstateを読み書きするために上記のS3FullAccessのポリシーが必要になります。

## GitHubの準備

GitHubの準備はメンバーの追加削除ができる権限を持つ[Personal Access Token](https://help.github.com/articles/creating-a-personal-access-token-for-the-command-line/)を発行しておきます。
権限はadmin:orgのフルコントロールが必要になります。
<img width="736" alt="スクリーンショット 2018-12-22 2.41.17.png" src="https://qiita-image-store.s3.amazonaws.com/0/253717/d3cae71c-bc19-d15c-78bc-e0b59d2cc199.png">

## アカウント管理のTerraform化
variables.tfに変数を、main.tfに構成の定義を記載します。
実際の運用ではAWSのIAMユーザーは追加削除だけでなく、IAMポリシーの付与やグループへの追加も行うのでもう少し複雑になります。

```:variables.tf
# IAMユーザ一覧
variable "iam_users" {
  type = "list"
  default = [
    "iam_user1",
    "iam_user2",
    "iam_user3",
  ]
}

# GitHub Organizationのメンバー一覧
variable "github_members" {
  type = "list"
  default = [
    "github_account1",
    "github_account2",
    "github_account3",
  ]
}
```

```:main.tf
# tfstateの設定
terraform {
  backend "s3" {
    encrypt = "true"
    region = "ap-northeast-1"
    bucket = "yukihira1992-tfstates"
    key = "account-management/terraform.tfstate"
    acl = "bucket-owner-full-control"
  }
}

# AWS Providerの設定
provider "aws" {
  region = "ap-northeast-1"
}

# IAMユーザの設定
resource "aws_iam_user" "iam_users" {
  count = "${length(var.iam_users)}"
  name = "${element(var.iam_users, count.index)}"
  path = "/"
  force_destroy = false
}

# GitHub Providerの設定
provider "github" {
  organization = "your_organizaion"
}

# GitHub Organizationのメンバー設定
resource "github_membership" "this" {
  count = "${length(var.github_members)}"
  username = "${element(var.github_members, count.index)}"
  role = "member"
}
```

## CircleCIの設定

### 設定ファイルの作成

```yaml:config.yaml
version: 2
jobs:
  test:
    working_directory: ~/account-management
    docker:
      - image: hashicorp/terraform:light
    steps:
      - checkout
      - run:
          name: Init terraform
          command: terraform init ~/account-management
      - run:
          name: Validate terraform
          command: terraform validate ~/account-management
      - run:
          name: Plan terraform
          command: terraform plan ~/account-management
  deploy:
    working_directory: ~/account-management
    docker:
      - image: hashicorp/terraform:light
    steps:
      - checkout
      - run:
          name: Init terraform
          command: terraform init ~/account-management
      - run:
          name: Apply terraform
          command: terraform apply -auto-approve ~/account-management
workflows:
  version: 2
  test-and-deploy:
    jobs:
      - test
      - hold:
          type: approval
          requires:
            - test
          filters:
            branches:
              only: master
      - deploy:
          requires:
            - hold
          filters:
            branches:
              only: master
```

設定ファイルのポイントは次のとおりです。

#### Docker Image
CircleCI公式のTerraform用Docker Imageは無いのでHashicorpが用意しているhashicorp/terraformを採用しています。

#### Terraformのテスト
簡単ではありますが、terraform validateによる構文エラーのチェックとterraform planによる実行計画の確認を持ってしてテストとしています。（実行計画は人間がチェックする必要があります。）

#### terraform applyの確認回避
terraform applyは反映前に人間による確認を必要としているため -auto-approve オプションで確認を回避しています。

```sh
$ terraform apply -auto-approve
```

#### Pull Requestマージ前のテストとマージ後のデプロイ
testジョブはすべてのブランチで実行されるため、testジョブの結果をPull Requestマージの条件にすることができます。
一方でdeployジョブはmasterブランチに限定しているため、Pull Requestがmasterブランチにマージされた後でのみ実行されます。また、deployジョブの前にholdジョブで処理を止め人間の確認を待ちます。

### アクセス権限の設定
CircleCI上でAWSのAPIとGitHubのAPIを操作できるようアクセス権限を設定します。
アクセス権限はCircleCIのプロジェクトに[環境変数](https://circleci.com/docs/2.0/env-vars/#setting-an-environment-variable-in-a-project)として設定します。
必要な環境変数は次の4つになります。

- AWS_DEFAULT_REGION : AWSのリージョン
- AWS_ACCESS_KEY_ID : Terraform用IAMユーザーのアクセスキーID
- AWS_SECRET_ACCESS_KEY : Terraform用IAMユーザーのシークレットアクセスキー
- GITHUB_TOKEN : GitHub OrganizationのPersonal Access Token

## ブランチ保護設定
GitHubの場合になりますが、Terraformリポジトリのmasterブランチの[保護設定](https://help.github.com/articles/configuring-protected-branches/)を有効にします。マージ前のチェックの設定を次のように設定します。test-and-deployは一度CircleCI上でWorkflowを動かすと選択できるようになります。
<img width="724" alt="スクリーンショット 2018-12-22 3.37.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/253717/79e1468d-3f5a-2436-d344-f5e0d2ef2e49.png">

### 動作確認
新しいブランチをGitHubにpushするとtestジョブが単独で実行されます。
<img width="227" alt="スクリーンショット 2018-12-22 3.45.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/253717/d384a83d-dc95-9969-b02e-09527c8932ef.png">

続いてPull Requestをmasterブランチにマージするとtest-and-deplyのすべてのジョブが実行され、holdジョブで確認待ちになります。
<img width="733" alt="スクリーンショット 2018-12-22 3.47.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/253717/e8518426-f2c7-65ac-7f61-98bc62d108f5.png">

holdジョブをクリックすると許可の確認をされるのでApproveをクリックします。
<img width="519" alt="スクリーンショット 2018-12-22 3.48.00.png" src="https://qiita-image-store.s3.amazonaws.com/0/253717/eda49a8b-57d3-bc27-fa5a-5fedb64bbd01.png">

最後に人間がholdジョブを承認するとdeployジョブが動き構成変更が反映されます。
<img width="724" alt="スクリーンショット 2018-12-22 3.48.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/253717/e4668ff4-73e2-b92c-fd9f-76d85532f882.png">

## まとめ
アカウント管理をTerraformで行いCI/CDを組み合わせることでテストとデプロイを自動化してみました。
Terraformのような構成管理ツールにCI/CDを導入すると機密情報はCIサービスにのみ設定すればよく、個々の開発者が機密情報を保持する必要がなくなるのは大きなメリットだと思いました。次はTerraform planやapplyの結果をGitHub側から簡単に見れるようにできたらと思います。