こんにちは。@canon1kyです。
AWS Advent Calendar 2019 20日目を担当させていただきます。
はじめに
皆さんはRDSのインスタンスを立てた際、下記の情報はどこで管理していますか?
- DBホスト名
- DBユーザー名
- DBパスワード
などなど。
Laravelなど、Webフレームワークなどを使ったアプリケーションでこのような情報を使用するとき、DBパスワードなど機密情報の扱いに困る方は多いのではないでしょうか。
- アプリケーションコード内に直接記述する?
- .envファイルに環境変数として書いておく?
- プラットフォーム側で外部から環境変数として注入する?
様々な管理方法があるかと思います。
しかし、DBパスワードは秘匿情報であるため、gitに載せるなどのことはなるべく避けたいですよね。
今回は、業務でTerraformを使う中で知見として得た、「gitやドキュメントなどにパスワードを乗せず、セキュアにRDSの秘匿情報を扱う」方法を紹介します。
Terraformとは
インフラのリソース定義をコードで記述し、 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
を埋め込んでおかずとも、このエンドポイント情報を利用することができます。
今回はこのパラメータストアにRDSのパスワードを格納するようにします。
想定アーキテクチャ
DBにRDSを使用し、アプリケーションはFargateで動く環境とします。
-
terraform apply
を行い、RDSとパラメータストアのリソースが作成される。 - FargateはパラメータストアからDBのパスワード情報を
/rds/password
というキー名で取得する(1) - Fargateは
/rds/password
というキー名のパラメータをDB_PASSWORD
という環境変数にセットする(2) - Fargateは
DB_PASSWORD
環境変数を使ってDBにアクセスする
なお、EC2でコンテナを動かすECSや、EKS(ExternalSecretsなど)でもできます。
Terraform実装
RDS
- random_passwordというリソースを使ってランダムなパスワードを生成する
- そのパスワードをmaster_passwordとしたRDSリソースを生成する
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リソースで設定されたパスワードをパラメータストアに登録する。
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つできている。
詳細を見ると、RDSにランダム文字列で設定されたパスワードの値が格納されていることを確認できる。
ランダム生成したパスワードを持つRDSインスタンスを生成し、DBのパスワードをパラメータストアに格納できました!
Fargate実装
下記のように、タスク作成時の「環境変数」の部分で指定を行います。
すると、DB_PASSWORD
という環境変数に、 /rds/password
というキー名でパラメータストアから読み取った値がセットされます。つまり、/rds/password
という名前でセットしたDBのパスワードです。
ここまで来ればお分かりかと思いますが、Terraformのコードにも、アプリケーションのコードにもパスワードを書いていません。
そのためgitにパスワードが乗る心配がありません。
仮にTerraformのコードをgit管理した場合にも、gitに乗る情報は「/rds/password
という名前でDBのパスワードがパラメータストアに定義されている」ということだけです。
また、AWSアカウントでログインすればパラメータストアからパスワードを確認することができるので、ドキュメントにパスワードを記録しておく必要もありません。
ローカル環境で秘匿情報を扱うとき
さて、「DBの接続情報は環境変数から読みとって動作するWebアプリケーション」が、phpのコンテナで動くとしましょう。
Laravelのようなフレームワークを想定します(何でも良いです)。
このコンテナをdocker-composeを使ってローカル環境で起動し、接続先を先ほど作成したRDSにしたいです。
何も考えずにやろうとすると、下記のようにDBパスワードを指定する方法になるのではないでしょうか。
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
コマンドを使用すると、パラメータストアに取得した値を読み込むことができます。
# パラメータストアから/rds/passwordの値を復号化して読み込む。jsonで返ってくるので、値部分を抜き出す。
$ aws ssm get-parameters --names /rds/password --with-decryption | jq '.Parameters[0].Value')
"aweoijfael32ong93" # 実行結果
では、あとはMakefileとdocker-composeの合わせ技です。
出来上がったファイル
まずは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
を実行します。
.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ライフを!!