やりたいこと
- アプリケーションの設定を Git で管理したい
- アプリケーションの設定には秘密情報( API キーなど )も含めたい
- これら情報を Terraform で Parameter Store や Secrets Manager などに投入したい
- Terraform の State に秘密情報を残したくない
本記事ではこういった要件をどう実装したかについて、ひとつの方法を説明する。
前提
Terraform
本記事では、 terraform コマンドはセットアップ済みで、基本的な利用方法などは知っている前提で進める。
準備
sops
まず必要なのが sops だ。 Secrets OPerationS から取って sops らしい。これの良いところは、 YAML や JSON などのファイルを「その構造を保ったまま、一部のキーだけ」暗号化してくれる、という点にある。
本記事では、アプリケーションの設定ファイルを YAML で記述し、 sops で暗号化して Git 管理するために利用する。
インストール方法は公式サイトに載っている通りだ。
他にも brew scoop choco などのパッケージ管理ツールでもインストール可能とのこと。
age (rage)
age は極めてシンプルなファイル暗号化ツールだ。もっとも、本記事では暗号化自体は sops を想定していて、 age を直接暗号化に使うことはしない。必要なのは age に含まれている鍵生成プログラム age-keygen のほうである。
rage は age の Rust 実装で、本記事の範囲ではどちらでも良い。 rage の場合は age-keygen に対応する rage-keygen があるのでこれを使う。
インストール方法は公式サイトの READMEにある通りだが、筆者は Rust 環境が既にセットアップ済みだったため、 cargo install rage で代用した。
鍵の作成
sops が暗号化及び復号のために用いる鍵を用意する。sops はデフォルトで所定の位置に秘密鍵があることを想定しているので、各環境に合わせてデフォルト位置1に鍵を生成しておく。
- Linux:
$HOME/.config/sops/age/keys.txt - MacOS:
$HOME/Library/Application Support/sops/age/keys.txt - Windows:
%AppData%\sops\age\keys.txt
鍵は (r)age-keygen コマンドで作成でき、 -o FILE オプションで出力先を指定可能だ。
この鍵は秘密鍵なので漏洩してはいけない。(r)age-keygen -o で作成した場合はデフォルトでアクセス権 600 になっているはずだが、念のためアクセス権を確認し適切な設定をしておこう。
対応する公開鍵は作成の際に一度だけ表示されるが、忘れてしまっても
- ファイル内のコメント
# public key: age1...を見る - コマンドで
(r)age-keygen -y FILEとする
などで確認できる。
Terraform 設定
各ツールと鍵の用意ができたら Terraform 側を設定していく。本記事ではサンプルとして簡易的な構成で示すが、プロジェクトに応じて適切なファイル分割やディレクトリ分割を行ってほしい。
terraform.tf
sops を利用するので required_providers に sops を設定する。
また、今回はサンプルとして AWS SSM Parameter Store に設定ファイルを投入するので aws も設定している。
バックエンドはサンプルとしてローカルを利用するが、複数人のプロジェクトで利用する場合は S3 など何かしらの共有バックエンドを利用しているはずなので、実際にはそのあたりの設定をしてほしい。
terraform {
required_version = ">= 1.12.0"
backend "local" {}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
sops = {
source = "carlpett/sops"
version = "~> 1.3.0"
}
}
}
なお aws プロバイダはメジャーバージョン 5.0 系内であっても、古いものは write-only に対応していない可能性があるため、できるだけ更新しよう。また、Terraform 本体も ephemeral ブロックをサポートしたのが 1.10 以降とのこと。
具体的には、筆者は以下バージョンで動作確認をした。
Terraform v1.14.1
on linux_amd64
+ provider registry.terraform.io/carlpett/sops v1.3.0
+ provider registry.terraform.io/hashicorp/aws v5.100.0
.sops.yaml
続いて sops の設定をしておく。
ちなみに拡張子 .yml は許されないらしく、正しく .sops.yaml である必要がある。
creation_rules:
- path_regex: ^app_config\.enc\.yaml$
age:
- age1qvvd9udk0ggec4pz3f9pm84j5624urppel4s4akyrg85vutn9vdspztl09
encrypted_regex: ^(.*_secret|password)$
-
path_regex:設定を適用する暗号化対象のファイル名を正規表現で指定する -
age:age の公開鍵を指定する。複数人で管理する場合は複数個登録する -
encrypted_regex:暗号化するキー名を正規表現で指定する
詳細は公式のドキュメントを参照してほしい。
app_config.enc.yaml
ファイル名は.sops.yaml で設定した creation_rules.path_regex にマッチすれば何でもいい。ここではサンプルとして app_config.enc.yaml にしてある。
sops edit app_config.enc.yaml とすると標準のエディタ (vi など) が起動し、以下のようなサンプルが表示される。
hello: Welcome to SOPS! Edit this file as you please!
example_key: example_value
# Example comment
example_array:
- example_value1
- example_value2
example_number: 1234.56789
example_booleans:
- true
- false
ここにはそのままアプリケーションとして必要な情報を記載すればよい。例えば:
app:
service1:
username: sample_user
password: super_secret_password_123
そうすると次のようなファイルが生成される。
app:
service1:
username: sample_user
password: ENC[AES256_GCM,data:BO/bcZ0GVziLLquAEeB1xQKp4A9R6z+uVw==,iv:uqHrgp44WW6J+BHZnoHnxWJiRfevv4VrEYiSiwjTRzU=,tag:Ufejvlk1KFAJiqza0TGJ8Q==,type:str]
sops:
age:
- recipient: age1qvvd9udk0ggec4pz3f9pm84j5624urppel4s4akyrg85vutn9vdspztl09
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBvUnlpVFZBYXcwdTJSQi9l
eWxjVFAwd2JnS0hUdmpNV3kyMGpWYXFyMWlrCnBpcWc5RmRhM0VCR05tT3FHanFh
VlhhQXk1YXlITWxnelVMOEhxbXRvVlEKLS0tIGV6TmtMUFBtU3FzNVAwSmF2VEdi
UUhyWUFQUWowOFFsWlIwNzF1S2gwMmMK2UU7g5Okci+9A+N9EK9RbaQ2uaP1YdQt
kAuik5Wlh51S4W2awmSjsQC2OYM38W/Q+l0MR4ym1iGi7dBjJdiTvA==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-12-11T07:17:38Z"
mac: ENC[AES256_GCM,data:sEjU1jV4YxK8bdim0Oxqum0KWVOCyJbeIkJ3qC9evLp5hyVbWQnewbX9oBql4ZcCkYJrdYrIF6W44jieGdUsYA8ut9Xhi5fOkER83MHQSjEUPGP1u3XbR09kZMStOsemsxQfSnQpHEZDJLejD6FEev0DXCepcojLX3aq7oB//jo=,iv:nQorBtHS0u2HSEO4dVDLOBbUhZpYvyRrFX5jmevJ5w4=,tag:2miu72z9QcI0rfLuNLKXcA==,type:str]
encrypted_regex: ^(.*_secret|password)$
version: 3.11.0
YAML の構造を保ったまま、秘密にしたい password だけが暗号化されていることが分かるだろう。これを Git 管理することで
- 公開値は Git の差分としてレビュー可能
- 秘密値は暗号化されているため漏洩しない
- 秘密値もキー自体の追加や削除は Git の差分としてレビュー可能
となる。実務上これはかなりありがたい。
main.tf
実際にこの YAML をパラメータ管理サービスに投入する。ここではサンプルとして AWS SSM Parameter Store を使うが、AWS Secrets Manager など、 provider が write-only arguments に対応してくれていればなんでもいけるはずだ。
locals {
app_config_path = "${path.module}/app_config.enc.yaml"
app_config_hash = filesha256(local.app_config_path)
app_config_version = parseint(substr(local.app_config_hash, 0, 8), 16)
}
# Application config
ephemeral "sops_file" "app_config" {
source_file = local.app_config_path
input_type = "yaml"
}
# SSM Parameter Store with write-only argument
resource "aws_ssm_parameter" "app_config" {
name = "/sops-example/app-config"
description = "sops example application config"
type = "SecureString"
value_wo = ephemeral.sops_file.app_config.raw
value_wo_version = local.app_config_version
}
output "app_config_name" {
description = "Name of the SSM Parameter"
value = aws_ssm_parameter.app_config.name
}
ポイントとしては
- 秘密値を含むファイルは
ephemeralブロックで読み込む - 秘密値は
_woを通して書き込む
ことで、こうすることによって State に秘密値が書き込まれない。
一方、 Terraform 側としては State に値が無いため、更新があったかどうかの判定ができないという問題がある。それを解消するのが _wo_version で、「この値が変わったら本体も変わったものと判断して更新を行う」という挙動をする。
この例では暗号化状態のファイルハッシュを取り、先頭8文字を数値化して _wo_version に与えることにより更新を判断させている。ただ、この場合内容自体に変更がなくても、公開鍵を追加したなどの操作でも更新が走るので、それも避けたいのであれば _wo_version を手動で管理し、更新のたびにインクリメントしていくというのが素直だろう。
なお「復号結果のハッシュを取れば良いのでは?」と思いきや、 _wo_version 値を parseint(substr(sha256(ephemeral.sops_file.app_config.raw), 0, 8), 16) などとして ephemeral から導出すると
Ephemeral values are not valid for "value_wo_version", because it is not a write-only attribute and must be persisted to state.
というエラーが発生してしまいうまくいかない。一方向性ハッシュを取っているのだから良いのでは?とは思うものの、このあたりは厳密に扱っているようだ。
plan & apply
ここまでできれば、あとはいつも通り terraform plan と terraform apply で、復号済みのアプリケーション設定が YAML 丸ごと Parameter Store に書き込まれる。したがって、あとはアプリケーション側でこの値を取得し、 YAML としてパースすれば自由に使えることになる2。
補足
ファイル形式
本記事では YAML をサンプルとして利用したが、 sops が対応していれば JSON など他の形式でも利用できるはずだ。
sops が対応していない形式を利用したい場合は、「ファイル丸ごと」を暗号化する方式をとれば同じ手法を取れるはずではあるが、その場合は sops のメリットである「構造を理解し、一部だけを暗号化することで差分を取れる」というメリットが失われてしまう。
sops_file データの構造
本記事では例として sops_file で復号しつつ読み取った YAML 全体を Parameter Store に丸ごと登録したが、 sops プロバイダはきちんと構造を理解しているので、 ephemeral.sops_file.app_config.data["some.key"] などとすれば個別の値にもアクセスできる。キーごとにストア先を分けたい場合などは利用できるかもしれない。
公開鍵を追加したい
.sops.yaml に公開鍵を追加の上、 sops updatekeys FILE とすればよい。
公開鍵を削除したい
.sops.yaml から公開鍵を削除の上、 sops updatekeys FILE とすればよい。
のだが、注意点がある。Git で管理している場合はこれを行っても、履歴をさかのぼって消えるわけではないので、今までアクセスできていた人は履歴にあるものについては変わらずアクセス可能だ。したがってこの方法で保護している秘密情報(ひいては、その先のリソース)自体を保護したいのであれば、その秘密情報そのものも変更する必要がある。
もっとも、これに関しては本記事の方式固有の話ではなくもっと本質的な問題だ。仮に履歴も含めてこのファイルへのアクセスを禁じることが可能だとしても、元々中身の秘密にアクセスできていた人が、その秘密を忘れてくれるわけではないからだ。
したがって、
- 秘密情報にアクセスできる人は絞る
- キーなどは定期的あるいは必要に応じてローテーションする
などといった一般的なプラクティスは変わらず必要である。
encrypted_regex を変更したい
.sops.yaml で「どのキーを暗号化するか?」を指定する encrypted_regex だが、作業を進めるうちに今はマッチしないキーも暗号化したくなったり、逆に暗号化する必要がないのに暗号化されてしまうキーが発生してしまったりと、変更したい場面が出てくるかもしれない。
この場合、 .sops.yaml を編集して再度 sops edit すれば解決、かと思いきや、 sops は暗号化されたファイルのメタ情報に書いてある encrypted_regex を優先するらしく、これでは解決しないようだ。
なので現状は以下のように、一度復号してから再度暗号化するという手順が必要らしい。
$ sops -d --in-place FILE
$ sops -e --in-place FILE
これに関しては一発で再暗号化するコマンドがあってほしいのだが…。
秘密鍵を保護したい
本記事ではローカルマシンはセキュアに保護されていることを前提にしているが、 sops が利用する age の秘密鍵を保護したい場合は、 (r)age -p コマンドで秘密鍵ファイル自体を暗号化することもできるようだ。こうしておくと、利用の度に毎回パスフレーズの問い合わせが発生する形式にできる。
ただし、 terraform plan 中にパスフレーズの問い合わせが起こると ephemeral.sops_file.app_config: Still opening... [00m10s elapsed] のような出力でパスフレーズ入力欄が隠れてしまうなど、毎回のパスフレーズ入力の手間以外にも若干の不便があるようなので、そういう点には注意が必要だ。