🦑

パラメータストアをCI/CDしたい人生だった

2024/02/22に公開1

こんにちは、zinです🦑

みなさんはパラメータストアをどのように管理していますか。

私は機密情報を入れようとなった時に「あれ、意外とちょうど良いやり方がないぞ?」と思いました。
なんでもGitで管理したい、デプロイも自動化したい病を患っているので、いくつかのやり方を考えてみます。

どうやってGit管理するか

平文そのままコミットする訳にはいかないので、暗号化したファイルをGitで管理します。

sops

https://github.com/getsops/sops

Mozillaが公開している暗号化ツールで、TerraformやHelmなどと連携して使うことができます。
MacであればHomebrewでインストールできます。

$ brew install sops

KMSキーを作成する

個人的には age も好きですが、AWS環境であればKMSの方が扱やすいかと思うのでKMSを使っていきます。

resource "aws_kms_key" "example" {
  description             = "example-sops"
  deletion_window_in_days = 7
}
resource "aws_kms_alias" "example" {
  name          = "alias/example-sops"
  target_key_id = aws_kms_key.example.key_id
}

暗号化にKMSキーのARNが必要になるのでメモしておきます。

$ terraform console
> aws_kms_alias.example.arn
"arn:aws:kms:ap-northeast-1:111111111111:alias/example-sops"

暗号化

環境変数SOPS_KMS_ARNで先ほどメモしたKMSキーのARNを指定します。(.sops.yamlという設定ファイルでも指定できます)

$ export SOPS_KMS_ARN=arn:aws:kms:ap-northeast-1:111111111111:alias/example-sops

コマンドは非常に簡潔です。絶対に見られたくない情報を暗号化してみます。

$ cat <<EOF > my_secret.json
{
  "id": 1,
  "password": "passw0rd",
  "meta": {
    "email": "me@example.com",
    "editors": ["vim"]
  }
}
EOF

$ sops -e my_secret.json > my_secret.enc.json

暗号化されたファイルの中身はこんな感じ。キー名や構造が維持されるのは嬉しいですね。
元の値が同じでも、暗号文の部分は値が毎回変わる点に注意してください。

$ cat my_secret.enc.json
{
	"id": "ENC[AES256_GCM,data:Dg==,iv:VYqppQtqY452Zwr4ROBiKJ2rvnAb/n8H30NA9jPpx0g=,tag:TT0yFrq8B9k6S8nil1bWiA==,type:float]",
	"password": "ENC[AES256_GCM,data:6H8ZWT0UbEg=,iv:OMglI4sFvSwBHMUv/o0+ls58IHZIK/BoZgUiojGubok=,tag:cBKDQJMRNPoep/m80LOa9Q==,type:str]",
	"meta": {
		"email": "ENC[AES256_GCM,data:lVwcbORaVNZHlA364FNa,iv:IA9OJghstw23FUiYbnHE8u6YoDVdQowKOqT0wxdNtqA=,tag:L7oFoEksu0kP7yBdwrREJg==,type:str]",
		"editors": [
			"ENC[AES256_GCM,data:5sIz,iv:VoudBcpmltgzc2kD9qupHwVQFXUS18AxFrQolkVUVps=,tag:v+AjPS7aL/W9sAgqI1eSPA==,type:str]"
		]
	},
	"sops": {
		"kms": [
			{
				"arn": "arn:aws:kms:ap-northeast-1:111111111111:alias/example-sops",
				"created_at": "2024-02-19T16:46:20Z",
				"enc": "AQICAHjZxY5dMX9q5nZaakbQ8XX2VzLiQuzhgyTcSDvjt5OyVAG4e+Mr5IR+GK8/JxHR9/1TAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQM5o0Sug1PLn6X02vIAgEQgDt5PvRzVM97vioO/F7fH3dV/+lJqMiOz518wyRKoNomCZdofAmbPn+CztxYPVQmaVvSgKy09gWG/FK0/g==",
				"aws_profile": ""
			}
		],
		"gcp_kms": null,
		"azure_kv": null,
		"hc_vault": null,
		"age": null,
		"lastmodified": "2024-02-19T16:46:20Z",
		"mac": "ENC[AES256_GCM,data:edut5dfuA4dJ0+d6t+hio3ahk5BDdNjx2I7qrGVXDNSwnPNeWwEkNttlnzV8Bjp8yfFFdIoMBKA87jKuekK2dZnLWq/8S1Pr+A4ppM7soGVJsqzaR37CUjC2OIPHCCldF39+YN7VwVWadXfuYhsHlP8wkHNLm9iBhl3qFvMt1sM=,iv:PdKLDAIJhHaN70JwQmrclN83SQZZMbYMG2OLOfIQync=,tag:WqvsnzNOeyh6NhMV8HJ7Qw==,type:str]",
		"pgp": null,
		"unencrypted_suffix": "_unencrypted",
		"version": "3.7.3"
	}
}

標準入力から渡すこともできます。

$ echo '{"key": "value"}' | sops -e --input-type json /dev/stdin

復号

KMSキーのARNがファイルに含まれているので、復号の時に指定する必要はありません。

$ sops -d my_secret.enc.json
{
	"id": 1,
	"password": "passw0rd",
	"meta": {
		"email": "zin@example.com",
		"editors": [
			"vim"
		]
	}
}

標準入力から渡すこともできます。

$ cat my_secret.enc.json | sops -d --input-type json --output-type json /dev/stdin

さて、これで元になる機密情報をGitで管理できるようになりました。
間違って平文ファイルをコミットしないように注意です。

どうやってデプロイするか

Gitで管理できるようになったらデプロイも自動化したいものです。
CloudFormationはSecureStringがサポートされていないので、それ以外の手段を探っていきます。

手で管理する?

CI/CD欲を満たすことはできませんが、有力な選択肢です。
Gitではなくパスワードマネージャーで管理すれば十分、という説もあります。

Terraform?

Terraformに sops Provider があるので、これを利用すると楽です。
ただしstateには平文が載ってしまうので対策が必要です。(state平文をどの程度嫌うべきなんだろうか...?)

provider "sops" {}

data "sops_file" "my_secret" {
  source_file = "test/files/my_secret.enc.json"
}

resource "aws_ssm_parameter" "my_secret" {
  name  = "my_secret"
  type  = "SecureString"
  value = jsonencode(data.sops_file.my_secret.data)
}

自作する?

パラメータストアの管理が複雑になることはあまりないだろうと想定し、単純なツールを作ってなんとかしてみます。

  • files/配下にsopsで暗号化したファイルを置いておく
  • put_secret_parameters.shを実行すると、よしなにデプロイする
$ tree .
.
├── files
│   └── my_secret.enc.json
└── put_secret_parameters.sh
put_secret_parameters.sh
#!/bin/bash
set -euo pipefail
cd "$(dirname "${0}")"

for target in files/*.enc.json; do
    # パラメータ名=ファイル名としておく
    secret_name="/${target#*/}"
    secret=$(sops -d "${target}")

    # 既存のものは変更がなければスキップ
    # CIならgit diffで判断しても良いかも
    ssm_exists=$(
        aws ssm get-parameters --names "${secret_name}" \
        | jq '.Parameters | length'
    )
    if [[ ${ssm_exists} -gt 0 ]]; then
        ssm_secret=$(
            aws ssm get-parameter --with-decryption \
                --name "${secret_name}" \
                --query Parameter.Value \
                --output text
        )
        if [[ ${secret} = "${ssm_secret}" ]]; then
            echo "${secret_name}: Not changed."
            continue
        fi
    fi

    # 新規 or 更新
    res=$(
        aws ssm put-parameter \
            --name "${secret_name}" \
            --value "$(sops -d "${target}")" \
            --type "SecureString" \
            --overwrite
    )
    echo "${secret_name}: $(printf '%s' "${res}")"
done

パラメータ数10 ~ 20個程度のプロジェクトでこの管理方法を導入してみましたが、悪くはないかなという感触でした。
例に挙げたスクリプトは単純な作りになっていますが、環境ごとにディレクトリを分けたり削除に対応したりなどある程度の拡張は可能です。

おわりに

どの管理方法がベストかは自分でも答えが出ていませんが、チームに合った方法をとれれば良いのかなと思っています。
AWSがベストプラクティスを出してくれる(CloudFormationがSecureStringをサポートする)と嬉しいですね。

より良い解をお持ちの方はコメントいただけると嬉しいです!

ソーシャルデータバンク テックブログ

Discussion

rakiraki

ansible-vault で暗号化して git に含めておいて、play で登録変更削除できるようにすると暗号化ツールの選定をしなくていいし、IaC向きかなって。