Terraform を使うとインフラの構成がコード化される、Infrastructure as Code (IoC) という利点があります。そして、Terraform をある程度使っていくと、ソフトウェア開発と同様に
「秘密情報をコードに直接書かないためにどうすれば良いか」
について考える必要が出てきます。
本記事では、Terraform で秘密情報を扱う方法や注意点など、雑多な話題について書いていきます。
なお、書いているうちに、以下のページの内容と似ているなと思ったので、なるべく違う視点からの内容を書こうと思います。
A comprehensive guide to managing secrets in your Terraform code | by Yevgeniy Brikman | Gruntwork
基本的な注意点
tf ファイルなどに秘密情報を直書きしない
*.tf ファイルなどにパスワードのような秘密情報を直接書いてはいけません。当たり前すぎる話ですが、一応書きました。
state ファイルには秘密情報が「含まれる!」
後の方で *.tf ファイルに秘密情報を書かないための方法をいくつか紹介しますが、大半の方法ではそうした秘密情報は *.tf ファイルに書かれていなくても state ファイル(terraform.tfstate
)には含まれています。詳しくは、以下のページの「Sensitive Values」の項を参照。
Variables – Workspaces – Terraform Cloud and Terraform Enterprise – Terraform by HashiCorp
従って、state ファイルの置き場所は安全な場所にする必要があります。具体的には以下のような選択肢があります。
- ローカルに保存する場合は、
.gitignore
に指定して、間違えて commit しないようにする - S3 などのリモートストレージに保存する場合は、アクセス制限、ディスクの暗号化などを施す
- Terraform Cloud などを使う(詳しくないので、本記事では扱いません)
また、以下の GitHub issue でも色々な議論・情報があります。
Storing sensitive values in state files · Issue #516 · hashicorp/terraform
秘密情報を扱う主な方法
基本的な注意点が分かったので、次に、秘密情報を扱う方法の概要をいくつか説明します。詳細な使い方は、次の章で扱います。
方法1: 環境変数、外部ファイルに外出しする
1つ目の方法は、秘密情報を以下の方法で渡すというものです。
- 環境変数(
TF_VAR_*
という名前の環境変数) - 外部ファイル(
*.tfvars
)
簡単に実現出来るのがメリットですが、環境変数なり外部ファイルを何らかの形で配布する必要があり、以下のような点が問題となり得ます。
- ×: 一度配布した情報は取り戻せない。従って、秘密情報を持っている人が退職し時などに、秘密情報の内容などによっては、秘密情報(パスワード等)を変更する必要があるかもしれない
また、この方法は、state ファイルに秘密情報が書き込まれます。
方法2: ファイルを暗号化する
2つ目の方法は、秘密情報が含まれたファイルを暗号化し、暗号化されたファイルのみをソースコード管理にコミットする方法です。
この「方法2」全般のメリット・デメリットを以下に記載します。
- ○: 秘密情報も暗号化された上でバージョン管理される
- 元に戻したりするのが簡単
- 設定し忘れなどが無い
- ×: 暗号化・復号の際にコマンドを打たなければいけないので面倒
- ×: 復号したファイルはローカルに保存されるので、方法1と同様に退職者が発生した場合の問題がある
なお、この方法の場合、暗号化に使った鍵をどう管理するかもポイントとなります。良くない方法、あまり良くない方法、及びなぜ良くないのかの理由を記載します。
- ×:秘密鍵ファイルをソースコード管理にコミットする
- ×の理由: 暗号化する意味がない
- △:秘密鍵ファイルを、アクセス出来る人を限定した別のレポジトリや共有ドライブに保管する
- △の理由: 秘密鍵ファイルがローカルにも保存される
- △の理由: 秘密鍵を持っている人が退職したときなど、秘密鍵を変更するなどの対応が必要となる
良い方法としては、AWS の Key Management Service (KMS) などを利用して鍵を管理する方法です。KMS のメリット・デメリットを以下に記載します。
- ○: 秘密鍵ファイルは AWS 上に保管され、ダウンロードは出来ない
- ○: 秘密鍵のローテーションが行える
- ○: IAM ユーザー、ロールで鍵に対するアクセスを管理することが出来る
- ×: 若干の料金が発生する
Azure Key Valut, GCP KMS など、他のベンダーの同様機能でも似たようなものです。
なお、この方法も、state ファイルに秘密情報が書き込まれます。
方法3: AWS Secrets Manager などに保管する
3つ目の方法は、AWS Secrets Manager などの外部サービスに秘密情報を保存して、Terraform でリソース作成する際に秘密情報をそれらのサービスから読み込む方法です。具体的なサービス名としては以下の通りです。
- AWS Secrets Manager
- AWS Systems Manager Parameter Store
- GCP Secrets Manager
- HashiCorp Vault
なお、以下のページを見る限り、Azure には同種の機能は無さそうです。
AWS to Azure services comparison – Azure Architecture Center | Microsoft Docs
また、AWS で似たようなサービスが2つあるのは、AWS あるあるです。2つのサービスの違い、使い分けに関しては、検索してみるといろ色情報があると思います。
さて、この方法のメリット・デメリットを以下に記載します。
- ○: ローカルに情報は保存されない
- ○: サービス側で色々な有用な機能がある
- 定期的に秘密情報を変更する機能
- Terraform 以外にも、アプリケーションから秘密情報を取得出来る
- 監査ログ
- ○: IAM ユーザー、ロールで秘密情報に対するアクセスを管理することが出来る
- ×: 秘密情報がバージョン管理されず(※)、(主に UI 経由で)手動で登録する必要がある
- ×: 若干のコストがかかる
なお、バージョン管理についてですが、Secrets Manger を使った場合、Git でのバージョン管理はされませんが、Secrets Manager 自身に version を扱う機能があります。詳しくはドキュメントを参照して下さい。
Key terms and concepts for AWS Secrets Manager – AWS Secrets Manager
ちなみに、この方法も、state ファイルに秘密情報が書き込まれます。
方法4: 秘密情報を Terraform で扱わない
「Terraform で秘密情報を扱う」というタイトルのブログ記事なのに、と思うかもしれません。ここで紹介する方法は、正確には
「秘密情報を Terraform では直接扱わない」
です。
Terraform でリソース作成するときにパスワード等の秘密情報を指定すると、それが環境変数なり Secrets Manager なりの外部から取得したものであっても、state ファイルに書き込まれることは避けられません。remote state を使って安全に管理しておけば基本的には問題無いと思いますが、セキュリティ要件によっては方法1〜3だとダメな場合もあるかもしれません。
方法4は、基本的には以下の内容です。
- 秘密情報は Secrets Manager などに入れておく
- Terraform でのリソースの作成時に秘密情報は適当に指定しておく
- リソース作成後に、他の方法で秘密情報を変更する
少し手間がかかりますが、Terraform の null_resource
や local-exec
を使う事により、リソース作成完了後に別のスクリプトを起動して Secrets Manager から値を取得してセットする事が出来ます。
具体的な方法
次に、具体的なコマンドなどを紹介します。方法1〜3は、前述の Gruntwork のブログ記事に詳しく書いてあるので、さらっと説明します。
方法1詳細: 環境変数、外部ファイルに外出しする
db.tf
を以下の通り作成します。
variable "db_username" {
type = string
}
variable "db_password" {
type = string
}
resource "aws_db_instance" "db" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
name = "db"
# 以下を環境変数か外部ファイルから取得する
username = var.db_username
password = var.db_password
}
環境変数を使うのであれば、以下の通り設定し、terraform apply
を普通に実行します。
export TF_VAR_db_username=kanrisya
export TF_VAR_db_password="himitsudayo!!"
外部ファイルを使うのであれば、例えば以下のような db.tfvars
ファイルを用意します。このファイルは、当然ですが .gitignore
に指定しておきます。
db_username = "kanrisya"
db_password = "himitsudayo!!"
その上で、以下のコマンドを実行します。
terraform apply -var-file="db.tfvars"
方法2詳細: ファイルを暗号化する
ここでは、KMS を使った方法を書きますが、他のサービスでも同様だと思います。
まずは、平文での秘密情報ファイルを作成します。仮に secrets.yaml
とします。このファイルは .gitignore
に指定しておく必要があります。
db_username: kanrisya
db_password: himitsudayo!!
次に、KMS で鍵を作成します。手動で作成しても良いですし、Terraform で作成しても良いですが、私は手動で作成しました。そして、その鍵の ID を控えておきます。
次に、以下のコマンドで secrets.yaml
を暗号化しておきます。
aws kms encrypt \
--key-id <KMS の鍵の ID> \
--region <AWS REGION> \
--plaintext fileb://secrets.yaml \
--output text \
--query CiphertextBlob \
> secrets.yaml.encrypted
暗号化された secrets.yaml.encrypted
は、バージョン管理に含めて問題無いです。
この暗号化されたファイルを Terraform から使うには以下ようなファイルを作成して、 terraform apply
を実行します。
# KMS の鍵を使って、secrets.yaml.encrypted を復号する
data "aws_kms_secrets" "secrets" {
secret {
name = "secrets"
payload = file("${path.module}/secrets.yaml.encrypted")
}
}
# 復号した内容を local.secrets に読み込む
locals {
secrets = yamldecode(data.aws_kms_secrets.secrets.plaintext["secrets"])
}
# その内容を使って、インスタンスを作成する
resource "aws_db_instance" "db" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
name = "db"
username = local.secrets.db_username
password = local.secrets.db_username
}
ちなみに、secrets.yaml.encrypted
を手動で復号するには、以下のコマンドを使用します。
aws kms decrypt \
--key-id <KMS の鍵の ID> \
--region <AWS REGION> \
--ciphertext-blob fileb://<(base64 -d ./secrets.yml.encrypted) \
--output text \
--query Plaintext
secrets.yaml.encrypted
をそのまま渡すのでは無く base64
を使うのがポイントです。これに関しては、以下のページに経緯などが記載されています。
aws kms decrypt InvalidCiphertextException error · Issue #1043 · aws/aws-cli
方法3詳細: AWS Secrets Manager などに保管する
まずは、Secrets Manager にYAML か JSON 形式で秘密情報を登録します。その時の名前を控えておいて下さい。
その上で、以下のファイルを作成します。
resource "aws_secretsmanager_secret_version" "secrets" {
# Secrets Manager に登録したときの名前
secret_id = "secrets"
}
# Secrets Manager から値を取得してセットする
locals {
# yaml の場合は yamldecode を使用する
secrets = jsondecode(
data.aws_secretsmanager_secret_version.secrets.secret_string
)
}
# その内容を使って、インスタンスを作成する
resource "aws_db_instance" "db" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
name = "db"
username = local.secrets.db_username
password = local.secrets.db_username
}
方法4詳細: 秘密情報を Terraform で扱わない
具体的な方法を扱ったページがあったので、リンクを貼っておきます。
Keeping Secrets out of Terraform State | Object Partners
resource "aws_db_instance" "db" {
engine = "mysql"
engine_version = "8.0"
instance_class = "db.t3.micro"
name = "db"
username = "admin"
password = "password" # 後から変更する
lifecycle {
# パスワードが変更されていても、Terraform では無視する(初期パスワードで再設定されないようにする)
ignore_changes = ["password"]
}
}
# パスワード更新
resource "null_resource" "update_password" {
# DB 作成をトリガーとして実行する
triggers {
endpoint = "${aws_db_instance.db.endpoint}"
}
provisioner "local-exec" {
# パスワードを変更する
command = "python3 update_password.py --db_identifier=${aws_db_instance.default.identifier}"
}
}
ここで update_password.py
の実装方法として2パターン考えられます。
- Secrets Manager にパスワードを事前に置いておく
- 事前に Secrets Manager を設定、パスワードを保存しておく
- スクリプトで Secrets Manager からパスワードを取得
- DB のパスワードを変更
- スクリプトでパスワードを生成
- スクリプトでパスワードを生成
- スクリプトで DB のパスワードを生成
- スクリプトが、そのパスワードを Secrets Manager に保存する
前者の方が、ローテーションをやってくれたりして楽かなと思います。
その他の話題
random_password
Terraform には random_password
というのがあります。その名の通り、ランダムなパスワードを生成してくれるものですが、これも生成されたパスワードは state ファイルに書き込まれます。
具体的な方法は以下のページを参照して下さい。
Terraform password hack. A simple way to create passwords in… | by Marcelo Clavel | Medium
ちなみに、このページでは random_string
を使っていて、ページ末尾に記載されている通り、生成されたパスワードが画面に表示されてしまうと言う問題があるのですが、その後に実装された random_password
にはその問題はありません。
sensitive フラグ
Terraform 0.14 から、リソースに sensitive
というフラグ(?)をつけられるようになりました。これを設定しておくと、terraform plan
や terraform apply
の出力に含まれなくなります。詳しくは、以下のページを参照して下さい。
Terraform 0.14 Adds the Ability to Redact Sensitive Values in Console Output
その他、参考にしたページ
- Stack Overflow とか
- ブログ記事
まとめ
Terraform で秘密情報を扱う方法を色々紹介してきました。セキュリティと利便性、コストなどはトレードオフにありますが、AWS Secrets Manager などのサービスにより、少ないコストで利便性を享受出来ますので、それらを積極的に使うと良いと思います。