概要
久しぶりにAmazon EC2上で環境変数と秘密情報を取得する必要があったので、シェルスクリプトを書いた。あわせてCloudFormationでそれらをどう管理するか改めて整理した。
2022/09/14追記
- 実用しながらエラーになった箇所を修正
- 利用できない文字種類にアンド(&)を追加
用語の整理
- 環境変数: パブリックに、もしくは組織内であれば公開しても良さそうな環境依存の情報
- 接続先ホスト名、機能のリリースフラグ、etc.
- 秘密情報: パブリックにはもちろん、組織内であっても公開がはばかられる情報
- ユーザー名、パスワード、etc.
「秘密情報も、サーバ側で設定するときは環境変数(という手法を使って設定するの)では?」と思わなくもないが、便宜上こう置く。
全体像
- EC2インスタンスにはPHPで書かれたアプリケーションをデプロイする
- PHPのアプリケーションは、ローカルに置かれたenvファイルから色々読み出す形になっており、「いい感じに取得してくれるくん」で環境変数と秘密情報を解決する
- 環境変数はSystems Manager Parameter Store(以下、SSM Parameter Store)、秘密情報はSecrets Managerで管理する
- アプリケーションのデプロイや「いい感じに取得してくれるくん」の実行はCodeDeployが担当する
- CodeDeployの話は本筋からズレるので本記事では割愛
動作確認環境
Ubuntu 22.04
bash 5.1.16
前提条件
bashのバージョン
bash 4以降。
4以降で実装された記法を使っているため。
cf. Bashで変数を大文字小文字変換(uppercase/lowercase)する - Qiita
IAM権限
EC2インスタンスが以下の権限を持っていること。
- ssm:GetParameters
- secretsmanager:GetSecretValue
ssm:GetParameters
後述のシンプル版を使う場合は、ssm:GetParametersの代わりにssm:GetParameterが必要。
コマンド類
EC2インスタンスに以下のコマンドがインストールされていること。
- awscli v2 (v1でも動くかもしれないが未検証)
- jq
コマンドのインストール方法
CodeDeployを使う場合はその実行の範囲でインストール可能。
後述のスクリプト内でインストールも可能。いずれも、依存解決はあまり難しくない。
envファイルの構造
- KEYが大文字のスネークケースになっていること
- 環境変数のVALUEに
<ENV>
、秘密情報のVALUEに<SECRET>
と記載していること
# KEY=VALUE
DEBUG=false
DB_USER=<SECRET>
DB_PASSWORD=<SECRET>
DB_HOST=<ENV>
...
SSM Parameter Store / Secrets Managerのパラメータ名
envファイルのKEYを小文字にして、先頭とハイフンをスラッシュで置き換えた名前になっていること。
DB_USER => /db/user
DB_PASSWORD => /db/password
利用できる文字種
- 環境変数: セミコロン、アンド以外
- 秘密情報: アットマーク、アンド以外
「取得してくれるくん」で使っているsedの都合。変更すれば好きな文字を使える
環境変数と秘密情報をいい感じに取得してくれるくん
実行すると指定したenvファイルの<ENV>
と<SECRET>
の記述を置換する。
環境変数・秘密情報に対応するパラメータが未登録の場合は、exit codeが非ゼロになる。
実行前後のイメージ:
before | after |
---|---|
DEBUG=false | DEBUG=false |
DB_USER=<SECRET> | DB_USER=app |
DB_PASSWORD=<SECRET> | DB_PASSWORD=hoge_fuga |
DB_HOST=<ENV> | DB_HOST=db.example.com |
効率版
スクリプト末尾の<ENV FILE>
だけ書き換える必要あり。
#!/bin/bash
set -e
function env_from_aws() {
ENV_FILE=$1
declare -a env_keys=()
for v in $(grep -E "^[A-Z0-9_]+=[\"\']?<ENV>[\"\']?" $ENV_FILE | grep -o -E "^[A-Z0-9_]+")
do
tmp=${v,,}
env_keys+=(/${tmp//_/\/})
done
if [ ${#env_keys[@]} -eq 0 ];then
return 0;
fi
# ex. env_keys=(/db/password /db/user)
declare -a env_kvs=()
if [ ${#env_keys[@]} <= 10 ]; then
env_kvs+=($(aws ssm get-parameters --region ap-northeast-1 --names ${env_keys[@]} --query "Parameters[*].{Key: Name,Value:Value}" --output text))
else
for ((i=0; i<${#env_keys[@]}; i+=10)); do
declare -a tmp=()
for ((j=0; j<10; j+=1)); do
tmp+=(${env_keys[i+j]})
done
env_kvs+=($(aws ssm get-parameters --region ap-northeast-1 --names ${tmp[@]} --query "Parameters[*].{Key: Name,Value:Value}" --output text))
done
fi
declare -A envs=()
for ((i=0; i<${#env_kvs[@]}; i+=2)); do
envs[${env_kvs[i]}]=${env_kvs[i+1]}
done
# ex. envs["/db/password"]=mypassword, envs["/db/user"]=app
for v in ${env_keys[@]}
do
if [ -z ${envs["$v"]} ]; then
echo "$v is not registered in parameters store"
exit 1
fi
# ex. v=/db/password
tmp=${v:1}
tmp=${tmp//\//_}
uppercase=${tmp^^}
# ex. uppercase=DB_PASSWORD
echo "$uppercase=${envs[$v]}"
sed -r -e "s@$uppercase=([\"\']?)<ENV>([\"\']?)@$uppercase=\1${envs[$v]}\2@" -i $ENV_FILE
done
}
function secrets_from_aws() {
ENV_FILE=$1
for v in $(grep -E "^[A-Z0-9_]+=[\"\']?<SECRET>[\"\']?" $ENV_FILE | grep -o -E "^[A-Z0-9_]+"); do
lowercase=${v,,}
secret=$(aws secretsmanager get-secret-value --region ap-northeast-1 --secret-id /${lowercase//_/\/} --query "SecretString" --output text | jq ".[]" -r)
echo "$v=*****"
sed -r -e "s@$v=([\"\']?)<SECRET>([\"\']?)@$v=\1$secret\2@" -i $ENV_FILE
done
}
env_from_aws <ENV FILE>
secrets_from_aws <ENV FILE>
対象のenvファイルをスクリプト中に記載する理由
これは、スクリプトを(本記事では詳細を省いた)CodeDeployから実行する都合上。CodeDeployは、その実行中に任意のスクリプトを呼び出せるが、スクリプトに引数を与えることはできない。
- OK: "hoge.sh"
- NG: "hoge.sh parameter"
実行時に引数を与えられるツールを使う場合や手動で実行する場合なら、記載不要。
別パターン:シンプル版
#!/bin/bash
set -e
function env_from_aws() {
for param in $(grep -E "^[A-Z0-9_]+=<ENV>" $ENV_FILE | grep -o -E "^[A-Z0-9_]+"); do
lowercase=${param,,}
env=$(aws ssm get-parameters --region ap-northeast-1 --names "/${lowercase//_/\/}" --query "Parameters[0].Value" --output text)
echo "${param}=${env}"
sed -e "s/${param}=<ENV>/${param}=${env}/" -i $ENV_FILE
done
}
function secrets_from_aws() {
# ここは同じ
}
env_from_aws <ENV FILE>
secrets_from_aws <ENV FILE>
両者の違い
両者の違いは、SSM Parameter Storeを呼び出す回数。効率版は各インスタンスで1回だけ呼び出すが、シンプル版は(インスタンス x 変数)の回数呼び出す。
SSM Parameter Storeは、WebAPIの呼び出し上限がデフォルトで40req/sec、最大でも3000req/sec。変数が多い場合やインスタンスが多い場合は、スロットリングの危険性が高まる1。
cf. AWS Systems Manager endpoints and quotas - AWS General Reference
そのため、少々記述が長くなるが効率版のほうが安定する。一方で、変数の数が少ない && 同時にデプロイするインスタンスが少ない場合は、シンプル版の方が見通しがいい。
お好きな方を。
環境変数をSSM Parameter Store、秘密情報をSecrets Managerに保存している理由
CloudFormationで、可能な限り全てを管理するため。
CloudFormationのAWS::SSM::Parameter
では、SecureStringを登録できない。手動で登録する必要がある。
cf. AWS::SSM::Parameter - AWS CloudFormation
一方、AWS::SecretsManager::Secret
を使えば秘密情報をCloudFormationから登録できる上、秘密情報の生成も任せることができる。
cf. AWS::SecretsManager::Secret - AWS CloudFormation
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
DBAppUsername:
Type: 'String'
NoEcho: true
Resources:
# Omit...
# 秘密情報の作成を任せるとき(作成ルールを指定可能)
# 以下の場合、{"password": "<random string>"} という形で保存される
# 「取得してくれるくん」はkeyを無視するので、"password"の部分は何でも良い
SecretsManagerSecretDBAppPassword:
Type: 'AWS::SecretsManager::Secret'
Properties:
Description: 'managed by cloudformation'
GenerateSecretString:
SecretStringTemplate: '{}'
GenerateStringKey: 'password'
PasswordLength: 16
ExcludeCharacters: "\"@/\\"
Name: '/db/password'
# 秘密情報を明示的に指定するとき
SecretsManagerSecretDBAppUsername:
Type: 'AWS::SecretsManager::Secret'
Properties:
Description: 'managed by cloudformation'
SecretString: !Sub '{"username": "${DBAppUsername}"}'
Name: '/db/username'
# 環境変数
SSMParameterDBHost:
Type: 'AWS::SSM::Parameter'
Properties:
DataType: 'text'
Name: '/db/host'
Type: 'String'
Value: !GetAtt RDSDBCluster.Endpoint.Address
秘密情報を明示的に指定するとき、しないとき
自分が管理する環境では、以下のときだけNoEcho属性でパラメータから指定している。
- 利用しているSaaSのアクセストークンなど、自分たちの都合で生成できないとき
- DBユーザなど、乱数を使うことで著しく使い勝手が悪くなったり理解を妨げるとき
- 人間がそれを使ってログインすることがあるかはさておき、意味を持った名前にするべき
パラメータが増えるとスタックの作成・更新が煩雑になるため、意識的に利用を減らしている。
全部Secrets Managerではだめ?
仕組み上は特に問題ない。
ただし、「Secrets Managerに特別シークレットじゃない情報を保存するのか?」という違和感や、課金体系の違い2もあるため使い分けている。
2つのサービスの比較はこちらの記事が詳しい。
cf. AWSのParameter StoreとSecrets Manager、結局どちらを使えばいいのか?比較 - Qiita
まとめ
- EC2上でenvファイルを相手に、環境変数と秘密情報の解決をした
- 環境変数はSSM Parameter Store、秘密情報はSecrets Managerから取得した
- Secrets Managerを使うと、CloudFormation経由で秘密情報の生成・管理ができる
-
Secrets Managerは5000req/secまで呼び出せる。cf. AWS Secrets Manager quotas - AWS Secrets Manager ↩
-
SSM Parameter Storeは保存するだけなら無料。Secrets Managerは1シークレットあたり0.40USD/月。 ↩