Terraform × パラメータストアでRDSの機密情報をセキュアに扱う

こんにちは。[@canon1ky](https://twitter.com/canon1ky)です。
[AWS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/aws) 20日目を担当させていただきます。

# はじめに
皆さんはRDSのインスタンスを立てた際、下記の情報はどこで管理していますか？

* DBホスト名
* DBユーザー名
* DBパスワード

などなど。

Laravelなど、Webフレームワークなどを使ったアプリケーションでこのような情報を使用するとき、DBパスワードなど機密情報の扱いに困る方は多いのではないでしょうか。

* アプリケーションコード内に直接記述する？
* .envファイルに環境変数として書いておく？
* プラットフォーム側で外部から環境変数として注入する？

様々な管理方法があるかと思います。
しかし、**DBパスワードは秘匿情報であるため、gitに載せるなどのことはなるべく避けたい**ですよね。

今回は、業務でTerraformを使う中で知見として得た、「gitやドキュメントなどにパスワードを乗せず、セキュアにRDSの秘匿情報を扱う」方法を紹介します。

# Terraformとは
![スクリーンショット 2019-12-19 1.00.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/e9a22d4d-5bbe-c929-b6f7-ec99aee222b2.png)

インフラのリソース定義をコードで記述し、 `terraform apply` を実行することで、その定義に従ってリソースを構築することのできるツールです。
有名な3大パブリッククラウドサービスであるAWS、GCP、Azureには言わずもがな対応しています。
参考URL: https://qiita.com/Chanmoro/items/55bf0da3aaf37dc26f73

# パラメータストアとは
AWS Systems Managerというサービスの機能の一つで、「〇〇という名前のキーに対して□□という値を設定する」というように、パラメータなどの機密情報をAWSのリソースとして設定し、保持しておけるものです。

例えばFargateに構築されたアプリケーション内で、「SQSのエンドポイント情報」（今回は`xxx.sqs-sample.com`とします）を使用したいとします。
しかしこのエンドポイント情報は、gitやドキュメントサービスなど、AWS外のサービスでなるべく管理したくなかったとします。

その場合、このエンドポイント情報を`/sqs/endpoint` というキー名でパラメータストアに保存しておき、Fargateで `/sqs/endpoint` をパラメータストアから読み取るように設定しておけば、アプリケーションソースに `xxx.sas-sample.com` を埋め込んでおかずとも、このエンドポイント情報を利用することができます。

![スクリーンショット 2019-12-19 1.13.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/88cae57d-f334-055c-03e1-4c60646f96a3.png)


公式リンク: https://docs.aws.amazon.com/ja_jp/systems-manager/latest/userguide/systems-manager-parameter-store.html

今回はこのパラメータストアにRDSのパスワードを格納するようにします。

# 想定アーキテクチャ
DBにRDSを使用し、アプリケーションはFargateで動く環境とします。
![スクリーンショット 2019-12-19 0.53.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/608296f0-b1af-4e78-9754-ec0c324032b2.png)

* `terraform apply` を行い、RDSとパラメータストアのリソースが作成される。
* FargateはパラメータストアからDBのパスワード情報を `/rds/password` というキー名で取得する(1)
* Fargateは`/rds/password`というキー名のパラメータを`DB_PASSWORD`という環境変数にセットする(2)
* Fargateは`DB_PASSWORD`環境変数を使ってDBにアクセスする

なお、EC2でコンテナを動かすECSや、EKS([ExternalSecrets](https://github.com/godaddy/kubernetes-external-secrets)など)でもできます。

# Terraform実装
## RDS
1. random_passwordというリソースを使ってランダムなパスワードを生成する
2. そのパスワードをmaster_passwordとしたRDSリソースを生成する

```rds.tf
resource "aws_rds_cluster" "main" {
  cluster_identifier                  = "sample_aurora_cluster"
  engine                              = "aurora"
  engine_version                      = "5.6.10a"
  database_name                       = "sample-db"
  master_username                     = "sample_username"
  master_password                     = random_password.password.result # ランダムで生成したパスワードを設定
  vpc_security_group_ids              = [xxxxxxxx]
  backup_retention_period             = 1
  backtrack_window                    = 0
  preferred_backup_window             = "17:00-19:00"
  preferred_maintenance_window        = "wed:19:00-wed:19:30"
  enabled_cloudwatch_logs_exports     = true
  skip_final_snapshot                 = true
  copy_tags_to_snapshot               = true
  deletion_protection                 = false
  storage_encrypted                   = true
  db_subnet_group_name                = "my-subnet"
  db_cluster_parameter_group_name     = "my-parameter-group"
  iam_database_authentication_enabled = true
}

# パスワードをランダムで生成する。初回実行時の1度だけ生成。
resource "random_password" "password" {
  length           = 16
  special          = true
  override_special = "_%@"
}
```

## パラメータストア
`/rds/password` という名前（キー名）で、RDSリソースで設定されたパスワードをパラメータストアに登録する。

```parameter_store.tf
resource "aws_ssm_parameter" "rds_password" {
  name   = "/rds/password"
  type   = "SecureString" # KMSで暗号化して保存
  key_id = xxxxxx # 暗号化に使う KMS key のID
  value  = aws_rds_cluster.main.master_password # RDSリソースのパスワードを参照
}
```

## リソース作成実行
`terraform apply` を実行します。
すると、RDSリソースとパラメータストアリソースが1つずつできます。


`/rds/password` という名前でパラメータストアにレコードが1つできている。
![スクリーンショット 2019-12-18 22.14.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/26e678b0-ada9-9054-b85a-aeb8d1b868c3.png)

詳細を見ると、RDSにランダム文字列で設定されたパスワードの値が格納されていることを確認できる。
![スクリーンショット 2019-12-18 22.14.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/47bc7596-fac5-fbbe-84b8-404b7547d693.png)

ランダム生成したパスワードを持つRDSインスタンスを生成し、DBのパスワードをパラメータストアに格納できました！

# Fargate実装
下記のように、タスク作成時の「環境変数」の部分で指定を行います。
![スクリーンショット 2019-12-18 22.29.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/154121/bc990c4d-5639-69bb-c1c2-ac6ae5c2be45.png)

すると、`DB_PASSWORD`という環境変数に、 `/rds/password`というキー名でパラメータストアから読み取った値がセットされます。つまり、`/rds/password`という名前でセットしたDBのパスワードです。

ここまで来ればお分かりかと思いますが、Terraformのコードにも、アプリケーションのコードにもパスワードを書いていません。
そのためgitにパスワードが乗る心配がありません。
仮にTerraformのコードをgit管理した場合にも、gitに乗る情報は「`/rds/password`という名前でDBのパスワードがパラメータストアに定義されている」ということだけです。

また、AWSアカウントでログインすればパラメータストアからパスワードを確認することができるので、ドキュメントにパスワードを記録しておく必要もありません。


# ローカル環境で秘匿情報を扱うとき
さて、「DBの接続情報は環境変数から読みとって動作するWebアプリケーション」が、phpのコンテナで動くとしましょう。
Laravelのようなフレームワークを想定します（何でも良いです）。
このコンテナをdocker-composeを使ってローカル環境で起動し、接続先を先ほど作成したRDSにしたいです。

何も考えずにやろうとすると、下記のようにDBパスワードを指定する方法になるのではないでしょうか。

```docker-compose.yml
php:
  container_name: php_container
  build:
    context: .
    dockerfile: ./docker/php/Dockerfile
  environment: |
    DB_HOST: sample_aurora_cluster.xxx.com
    DB_NAME: sample-db
    DB_USERNAME: sample_username
    DB_PASSWORD: aweoijfael32ong93 # このようにパスワードをdocker-compose.ymlに書き込む
  volumes:
    - .:/var/local/
```

すると、このdocker-compose.ymlがアプリケーションのリポジトリに含まれる場合、結局パスワードの情報がgitに乗ってしまうことになりますね。
これではせっかくgitにパスワードが乗らないようにした意味がなくなってしまいます。

そのため、ここはAWS CLIを使用してセキュアに情報を扱いましょう。
下記のように`aws ssm`コマンドを使用すると、パラメータストアに取得した値を読み込むことができます。

```bash

# パラメータストアから/rds/passwordの値を復号化して読み込む。jsonで返ってくるので、値部分を抜き出す。
$ aws ssm get-parameters --names /rds/password --with-decryption | jq '.Parameters[0].Value')
"aweoijfael32ong93" # 実行結果
```

では、あとはMakefileとdocker-composeの合わせ技です。

## 出来上がったファイル
まずはdocker-compose.ymlから。

```docker-compose.yml
php:
  container_name: php_container
  build:
    context: .
    dockerfile: ./docker/php/Dockerfile
  environment: |
    DB_HOST: sample_aurora_cluster.xxx.com
    DB_NAME: sample-db
    DB_USERNAME: sample_username
    DB_PASSWORD: ${RDS_PASSWORD} # RDS_PASSWORD変数の中身をDB_PASSWORD環境変数にセット
  volumes:
    - .:/var/local/
```

そして下記のようにMakefileを用意します。
`RDS_PASSWORD`という変数に、パラメータストアから読み取ったDBパスワードをセットし、`docker-compose run`を実行します。

```Makefile
.PHONY: run
run:
    RDS_PASSRORD=$(shell aws ssm get-parameters \
                                 --names /rds/password \
                                 --with-decryption \
                   | jq '.Parameters[0].Value') \
    docker-compose up
```

準備ができました。

## 実行
`make run` を実行。
するとコンテナが立ち上がるので、コンテナに入って環境変数を確認しましょう。

```
$ docker-compose exec php_container bash
~~~コンテナに接続~~~
/ # printenv
HOSTNAME=7b5534f5e208
HOME=/root
LANG=C.UTF-8
...
DB_PASSWORD=aweoijfael32ong93 # DBパスワードが環境変数として設定されている
...
TZ=Asia/Tokyo
```

ローカル環境のコンテナでもパラメータストアから取得した値を環境変数として埋め込めたことが確認できましたね！

# 最後に
今回はTerraformとパラメータストアを使って、セキュアにRDSの機密情報を扱う方法を紹介しました。
パラメータストアを使えば、**機密情報をgitに乗せたりドキュメントで管理せず**とも、AWSリソースやローカル環境で扱うことができます。
また、パラメータストアの参照権限を持つAWSアカウント、及びIAMユーザーはパラメータストア上に格納されている値を参照することもできるので、必要な時に機密情報を取得することも問題なくできます。

サービスを作る上で、セキュリティ周りをどこまで堅牢にするかはビジネス的な要件によってきますが、知っておいて損はない方法だったので紹介させていただきました。

それでは皆さん良きAWSライフを！！