Adminがいないクリスマス・イブでも安心してインフラ構築するためのTerraform, Vault, AWS

この記事は[エイチームブライズ/エイチームコネクト/エイチーム引越し侍 Advent Calendar 2018](https://qiita.com/advent-calendar/2018/hikkoshi) 24日目の記事です。

# はじめに
[Terraform](https://www.terraform.io/)はIaC(Infrastructure as Code）を実現するための最も重要なツールの一つといっても
過言ではないと思います。AWS, GCPはもちろんのこと様々な[クラウドサービス](https://www.terraform.io/docs/providers/index.html)の機能をコード化する
ことができます。
Terraformを習得すれば、AWS上に数百台のインスタンスをあっと言う間に起動することだって
いとも簡単にできます。
当然、TerraformでAWS上のリソースを操作するためには、AWSのアクセスキーとシークレットキー、
そして、リソースを操作するためのIAMの権限が必要です。アクセスキーとシークレットキーは定期的
に交換することや、IAMの権限は定期的に見直すことが望ましいですが、特にインフラエンジニアが
増えてくるとこれらの管理は苦痛を伴うものになってきます。
そこで今回はTerraformと[Vault](https://www.vaultproject.io/)を用いて、インフラ管理者以外にアクセスキーとシークレットキーを
配布しないでTerraformの実行環境を整えたいと思います。

# Vaultの準備
まずはVaultの環境を構築します。ローカルでVaultを構築したい場合には、[Hashicorp Vault 1.0で追加されるauto-unsealを試してみた](https://qiita.com/tsutorm/items/781e09dea2cef21b2d1d)を参考に
環境を構築することをおすすめします（これはお試し用の環境ですので、本番環境には適用しないで下さい）。
Terraformと連携する場合にはコンテナの外からVaultにアクセスする必要がありますので、上記の記事を参考
にする場合にdemo.hclのlistnerの部分を以下の用に変更してください。

```hcl:demo.hcl
listener "tcp" {
  address     = "0.0.0.0:8200"
  tls_disable = 1
}
```

# 管理者、作業者用のトークンの発行とポリシーの設定
terraformからVaultに接続するためのトークンをVaultのコマンドで作成します。
管理者用のTerraformと作業者用のTerraform用に２つトークンが必要になりますが、まずは
管理者用のトークンを作成します。ポリシーはrootと同じものでいいと思いますが念の為ttlを設定して
作成しました。

```
# vault token create -ttl=168h
Key                  Value
---                  -----
token                s.4hD6KBn9U20TxwpUMvzoKdzm
token_accessor       4RpO0YsQVM7PHG7FS3jnPgoJ
token_duration       168h
token_renewable      true
token_policies       ["root"]
identity_policies    []
policies             ["root"]
```

次に作業者用のトークンを作成します。さすがにrootポリシーのトークンを発行するのはよろしくないことですので、
作業者に必要なポリシーを作成しました。

```hcl:aws.hcl
path "aws/*" {
  capabilities = ["read"]
}

path "auth/token/*" {
  capabilities = ["update"]
}
```

上記の内容をaws.hclというファイルに保存して、以下のコマンドを実行してポリシーを作成します。

```
# vault policy write aws aws.hcl
Success! Uploaded policy: aws

# vault policies
aws
default
root
```

そして作成したポリシーを使用した作業者用のトークンを作成します。

```
# vault token-create -policy="aws" -ttl=24h
Key                  Value
---                  -----
token                s.12jFUtLq811lJDWLFQnevcTs
token_accessor       33V4nGl9EK3aTE0WYndFLNaO
token_duration       24h
token_renewable      true
token_policies       ["aws" "default"]
identity_policies    []
policies             ["aws" "default"]
```

#インフラ管理者用のTerraformの作成
Vaultと連携してAWSのアクセスキーとシークレットキーを管理するTerraformプロジェクトを作成します。
簡略化のためにバックエンド等の設定はしていませんが、以下のように構成にします。

```
├── admin
│   ├── 01_vault.tf
│   ├── terraform.tfvars
│   └── variable.tf
```

```hcl:variables.tf
variable "aws_access_key" {}
variable "aws_secret_key" {}
variable "vault_addr" {}
variable "vault_token" {}
```

```hcl:terraform.tfvars
aws_access_key = "AWS_ACCESS_KEY"
aws_secret_key = "AWS_SECRET_KEY"
vault_addr = "VAULT_ADDR"
vault_token = "s.4hD6KBn9U20TxwpUMvzoKdzm"
```
Vaultをローカル環境で立ち上げている場合はvault_addrはhttp://127.0.0.1:8200
にしておけばいいでしょう。
vault_tokenは先程の作業で発行した管理者用のトークンを指定します。
これらの情報を元にTerraformからVaultのプロバイダーを使う設定を行います。

```hcl:01_vault.tf
provider "vault" {
  address = "${var.vault_addr}"
  token = "${var.vault_token}"
}

resource "vault_aws_secret_backend" "aws" {
  access_key = "${var.aws_access_key}"
  secret_key = "${var.aws_secret_key}"
  region = "ap-northeast-1"

  default_lease_ttl_seconds = "120"
  max_lease_ttl_seconds     = "240"
}
resource "vault_aws_secret_backend_role" "ec2-admin" {
  backend = "${vault_aws_secret_backend.aws.path}"
  name    = "ec2-admin-role"
policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iam:*", "ec2:*", "rds:*"
      ],
      "Resource": "*"
    }
  ]
}
EOF
}

```

上記の状態でterraform applyを完了すると、VaultにAWSのadmin roleの情報が保存されます。
念の為Vault側で確認してみると以下の情報が見れると思います。

```
/ # vault read aws/roles/ec2-admin-role
Key                 Value
---                 -----
credential_types    [iam_user federation_token]
default_sts_ttl     0s
max_sts_ttl         0s
policy_arns         <nil>
policy_document     {"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["iam:*","ec2:*","rds:*"],"Resource":"*"}]}
role_arns           <nil>
```
#作業者用のTerraformの作成
実際にAWSのリソースを操作してインフラ構築を行う作業者用のTerraformは以下の用に作成します。

```
└── operator
    ├── 01_vault.tf
    ├── terraform.tfvars
    └── variable.tf
```

```hcl:variables.tf
variable "vault_addr" {}
variable "vault_token" {}
variable "aws_region" {}
```

```hcl:terraform.tfvars
vault_addr = "VAULT_ADDR"
vault_token = "s.12jFUtLq811lJDWLFQnevcTs"
aws_region = "ap-northeast-1"
```
vault_tokenには作業者用に作成したトークンを指定します。
管理者用のTerraformプロジェクトではaws_access_keyとaws_secret_keyを定義していましたが、
作業者用のTerraformプロジェクトでは定義しません。これらの情報はVaultから取得出来るように
なるからです。このTerraformプロジェクトでVaultとAWSのproviderを使う設定以下のように
記述します。

```hcl:01_vault.tf
provider "vault" {
	address = "${var.vault_addr}"
	token = "${var.vault_token}"
}

data "vault_aws_access_credentials" "creds" {
  backend = "aws"
  role    = "ec2-admin-role"
}

provider "aws" {
  access_key = "${data.vault_aws_access_credentials.creds.access_key}"
  secret_key = "${data.vault_aws_access_credentials.creds.secret_key}"
  region  = "${var.aws_region}"
}

```

この状態でTerraform applyを行うとVault側から取得したAWSのアクセスキーとシークレットキー
を使用してAWSのリソースを操作することができます。

#AWSコンソールでのIAMの確認
これまでで、作業者向けのAWSのアクセスキーとシークレットキーをVault側から取得することが
できましたが、作業者向けのTerraformをApplyしたとき、AWS側ではどのようなIAM情報が作られているのか
AWSのコンソールから確認してみます。ユーザー名でvaultで検索すると今回のTerraformで作成された
IAMのユーザーを見ることができます。
![スクリーンショット 2018-12-19 20.33.32.png](https://qiita-image-store.s3.amazonaws.com/0/60761/4e94a374-656f-a199-bc3f-a062812cc6e8.png)
ユーザー詳細画面でユーザーに付与されたポリシーを確認するとインラインポリシーで、管理者用のTerraform
で定義したポリシーが付与されていることが確認できます。
![スクリーンショット 2018-12-19 20.34.17.png](https://qiita-image-store.s3.amazonaws.com/0/60761/b27c1018-a962-60b7-1ad9-0bca36d36575.png)


また、このユーザーは永久に残されるわけではなく、default_lease_ttl_secondsに従って自動的に削除されます。しばらく時間が経ってからユーザー名をvaultで検索しても何もヒットしないことが確認出来ると思います。

```
vault_1  | 2018-12-19T11:31:33.876Z [INFO]  expiration: revoked lease: lease_id=aws/creds/ec2-admin-role/KXEYNFRvp21mgAsVtnaAzfNS
```
Vault側のログを確認してみると上記のようなログが残っており、Vault側からIAMのユーザーを削除してくれたことが確認できます。AWS側のリソースがどうなっているかを意識することなくVault側でユーザーの追加と削除を行ってくれるのは素晴らしいことだと思います。

#その他の秘密情報もVaultから取得する
AWSのアクセスキーとシークレットキー以外にも、Vaultから情報を取得することを考えてみます。
例えばRDSを作成する際のDBのパスワードを当然必要ですが、作業者はパスワードを知る必要も
ありませんし、それを平文でGitリポジトリにコミットする必要はないでしょう。
TerraformからVaultにこのような情報を登録する際は[vault_generic_secret](https://www.terraform.io/docs/providers/vault/d/generic_secret.html)を利用します。

```hcl:admin/vault_generic_secret.tf
resource "random_id" "db_password" {
  byte_length = 8
}

resource "vault_generic_secret" "db" {
  path = "secret/db"

  data_json = <<EOT
  {
    "password": "${random_id.db_password.b64}"
  }
  EOT
}
```
下記のようなtfファイルを作成してterraform applyするとVault側にはsecret/dbというキー名で
情報が登録されます。作業者側で呼び出す前にVaultのポリシーを更新する必要があります。
今回の例の場合ではaws.hclの末尾に以下の記述を追加して、再度ポリシーをアップロードします。

```hcl:aws.hcl
...
path "secret/*" {
  capabilities = ["read"]
}
```

作業者側ではTerraformのtfファイルを以下のように記述すれば、管理者側で設定した
パスワードを呼び出すことができます。

```hcl:operator/02_rds.tf
data "vault_generic_secret" "db" {
  path = "secret/db"
}

resource "aws_db_instance" "db" {
  allocated_storage      = 10
  storage_type           = "gp2"
  engine                 = "mysql"
  engine_version         = "5.7"
  password               = "${data.vault_generic_secret.db.data["password"]}"
...
```

#まとめ
TerraformとVaultを連携させることにより、有効期限の短いAWSのシークレットキーの発行と利用
を透過的の行えるようになりました。この方法であれば、AWSのシークレットキーを平文で記述する
必要がないので、漏洩のリスクを抑えることができます。
またシークレットキーが必要な管理者側のTerraformでも[IAMを作る権限さえあればいい](https://www.vaultproject.io/docs/secrets/aws/index.html)ため、
AdministratorAccessのような強いポリシーを持つ必要がないのもメリットです。
Vault側のトークンにもポリシーと有効期限
を設定することが出来るので、こちらも適切なポリシーを設定して有効期限を短くすることにより、
漏洩の影響を最低限に留めることが出来ると思います。

# お知らせ
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひとも[エイチームグループ採用ページ](http://www.a-tm.co.jp/recruit/)（[Webエンジニア詳細ページ](http://www.a-tm.co.jp/recruit/requirements/career/lifestylesupport-webengineer/)）よりお問い合わせ下さい。