Pulumi スタック と AWS Profile を紐付けて切り替える awspp コマンドを作った
TL;DR
-
fzfベースのawspコマンドを拡張して Pulumi Stack の切り替えに合わせて AWS Profile を切り替えるawsppコマンドを作成しました。
はじめに
AWS Profile を良い感じに切り替える方法として、fzf を利用したシンプルな awsp コマンドの実装が下記のスクラップで紹介されています。
単純に、AWS Profile を切り替えるだけであれば、この実装で問題ないのですが、私は IaC のツールとして Pulumi を利用しています。
Pulumi では、1度のデプロイで管理されるインフラリソースの単位を「スタック」として管理しており、この単位は小さいアプリケーションの開発では、prod や stg、dev などの環境の単位とほぼ同じになります。
AWS では環境の管理方法のベストプラクティスとしてマルチアカウント方式が取られているため、1環境に1アカウントが紐づくことになり、Pulumi スタックを切り替える場合は必然的に AWS Profile の切り替えが必要になります。
また、DIY バックエンドに S3 を利用する場合は、スタック単位(1 AWS アカウント単位)で Pulumi State を管理した方が、デプロイ時の競合や State の誤った削除のリスクを減らすことにつながるため、スタックの切り替えに合わせて、pulumi login s3://hoge のような State への再ログインも必要になります。
以上をまとめて、Pulumi スタックと AWS Profile の切り替えは次のような手順になります。
デプロイミスのリスク
Pulumi スタックの切り替えは上記のように少々煩雑で、設定を間違えると意図しない環境にインフラリソースをデプロイしてしまうリスクがあります。デプロイミスには 3パターンが想定され。
- ブランチのミス
- AWS Profile 指定のミス
- スタック指定のミス
があります。
このため、この手順を自動化することがデプロイ作業ミスを減らすために必須の課題となります。
この問題は少々お金をかければ、Pulumi Console やデプロイ作業用のサーバー、専用の CD パイプラインを用意することで、スタックや AWS Profile そのものを切り替えるというような作業そのものをなくすことで回避することも可能です。
しかしながら、サーバーレスをコンセプトとする小さく開発を始めたい環境では、これらデプロイ専用のインフラリソースを準備するのはオーバースペック気味だったので、よりシンプルな解決策として、awspを拡張するという方法をとることにしました。
awspp コマンドを作った
先述の問題を解決するための作成したのが、awsppコマンドです。(追加のpはplusやpulumiの意です)
下記がコマンドの全体です。(長いので折り畳んでいます。)
awspp コマンド
#!/usr/bin/env bash
function awspp() {
local _pulumi_stack=''
local _aws_profile=''
local _branch=''
local _verbose='false'
local _help='false'
local _unknown_option='false'
local _help='false'
# parse options
while :; do
case "${1-}" in
-s | --stack)
_pulumi_stack="${2:-}"
shift
;;
-b | --branch)
_branch="${2:-}"
shift
;;
-v | --verbose)
_verbose='true'
;;
-h | --help)
_help='true'
;;
-?*)
_unknown_option='true'
;;
*)
break
;;
esac
shift
done
if [[ "${_help}" = 'false' && ${_unknown_option} = 'false' ]]; then
# get stack by fzf
# NOTE: stack 名は「.」 を含む文字列を指定しないこと
if [[ -z "${_pulumi_stack}" ]]; then
_pulumi_stack="$(find . -maxdepth 1 -type f -name "Pulumi.*.yaml" -print0 \
| xargs -0 -n1 basename \
| cut -d'.' -f 2 \
| fzf)"
fi
# get aws:profile
[[ -f "Pulumi.${_pulumi_stack}.yaml" ]] || return 1
_aws_profile="$(yq -r '.config.["aws:profile"]' "Pulumi.${_pulumi_stack}.yaml" \
| tr -d '[:space:]')"
fi
(
set -Eeuo pipefail
function awspp::usage() {
cat - <<EOS
Usage: awspp [-h] [-s <stack>] [-b <branch>] [-v]
Script description:
Switch Pulumi stack and AWS profile
Available options:
-h, --help Print this help and exit
-s, --stack <stack> Specify Pulumi stack to use. If empty specified, set by fzf.
-b, --branch <branch> Specify git branch to switch. Current branch is used by default.
-v, --verbose Print debug info
EOS
}
function awspp::git_switch_by_pulumi_stack() {
local -r _stack_name="${1}"
local -r _branch="${2:-}"
if ! command -v git &>/dev/null; then
echo "git command not found." >&2
return 1
fi
if [[ -n "${_branch}" ]]; then
if ! git branch | grep -q "${_branch}"; then
echo "no ${_branch} branch found." >&2
return 1
fi
git switch "${_branch}" || return 1
return 0
fi
if [[ "${_stack_name}" =~ ^(prod|stg|production|staging|main)$ ]]; then
if git branch | grep -q 'main'; then
git switch main || return 1
else
echo "no main branch found." >&2
return 1
fi
fi
}
function awspp::exists_aws_profile() {
local -r _profile="${1}"
if ! command -v aws &>/dev/null; then
echo "aws command not found." >&2
return 1
fi
if aws configure list-profiles | grep -q "${_profile}"; then
return 0
else
echo "AWS profile '${_profile}' does not exist." >&2
return 1
fi
}
function awspp::aws_sso_login_with_cache() {
local -r _profile="${1}"
if ! command -v aws &>/dev/null; then
echo "aws command not found." >&2
return 1
fi
aws sts get-caller-identity --profile "${_profile}" &>/dev/null \
|| aws sso login --profile "${_profile}"
}
function awspp::get_pulumi_state_url_by_pulumi_config() {
local -r _stack_name="${1}"
local -r _base_dir="${2:-.}"
local -r _stack_file="${_base_dir}/Pulumi.${_stack_name}.yaml"
if ! command -v yq &>/dev/null; then
echo "yq command not found." >&2
return 1
fi
local _url
_url="$(yq -r '.config.["backend:url"]' "${_stack_file}" | tr -d '[:space:]')"
if [[ -z "${_url}" ]] || [[ "${_url}" = "null" ]]; then
echo "stack file '${_stack_file}' does not have 'backend:url' defined." >&2
return 1
fi
echo "${_url}"
}
function awspp::awspp() {
[[ "${_verbose}" = 'true' ]] && set -x
[[ "${_help}" = 'true' ]] && awspp::usage && return 0
[[ "${_unknown_option}" = 'true' ]] && awspp::usage && return 1
# NOTE: prod, stg など共通環境は main ブランチに自動で切り替える
echo "git switch ${_branch} and pulumi stack ${_pulumi_stack} start"
awspp::git_switch_by_pulumi_stack "${_pulumi_stack}" "${_branch}" || return 1
echo "git switch ${_branch} and pulumi stack ${_pulumi_stack} end"
echo "exists aws profile ${_aws_profile} start"
awspp::exists_aws_profile "${_aws_profile}" || return 1
echo "exists aws profile ${_aws_profile} end"
echo "aws sso login with profile ${_aws_profile} start"
awspp::aws_sso_login_with_cache "${_aws_profile}" || return 1
echo "aws sso login with profile ${_aws_profile} end"
echo "get pulumi state url for stack ${_pulumi_stack} in profile ${_aws_profile} start"
local _pulumi_state_url=''
_pulumi_state_url="$(awspp::get_pulumi_state_url_by_pulumi_config "${_pulumi_stack}")"
echo "get pulumi state url for stack ${_pulumi_stack} in profile ${_aws_profile} end"
echo "pulumi login for stack ${_pulumi_stack} in profile ${_aws_profile} start"
pulumi login "${_pulumi_state_url}?awssdk=v2&profile=${_aws_profile}" \
|| return 1
echo "pulumi login for stack ${_pulumi_stack} in profile ${_aws_profile} end"
echo "pulumi stack select for stack ${_pulumi_stack} in profile ${_aws_profile} start"
pulumi stack select "${_pulumi_stack}" || return 1
echo "pulumi stack select for stack ${_pulumi_stack} in profile ${_aws_profile} end"
}
awspp::awspp || return 1
) || return 1
# NOTE: bash ./awspp のようにサブプロセスで実行した場合は環境変数は反映されない
export AWS_PROFILE="${_aws_profile}"
export PULUMI_STACK="${_pulumi_stack}"
export PULUMI_BACKEND_URL=''
PULUMI_BACKEND_URL="$(pulumi about --stack "${_pulumi_stack}" --json | jq -r .backend.url)"
}
awspp "$@"
セットアップ
encryptionsalt: v1:ABCDE/ABC=:v1:ABCDEFG+ABC/p:ABCDEFG==
config:
aws:region: ap-northeast-1
aws:profile: <YOUR AWS PROFILE>
backend:url: s3://<YOUR PULUMI STATE BUCKET>
インストール
Pulumi スタックファイルの配置された同一のディレクトリに awsppというファイルを作成し、先述のコマンドをコピペし、chmod a+x awsppのように実行権限を付与してください
使い方
下記のコマンドを実行し、fzfでスタック名を指定すると、prodやstgなど特定の環境の場合は自動で mainブランチへの切り替えを行い、スタックファイルに書き込まれた AWS Profile と S3 の DIY バックエンドを参照して、自動で切り替えを行なってくれます。
source awspp
# オプションからスタックを指定する
# source awspp -s dev
# 指定のブランチに切り替える
# source awspp -s dev -b feature-hoge
source awspp のログ
hoge@hoge otlp-endpoint-lambda-rust % source awspp
git switch and pulumi stack dev start
git switch and pulumi stack dev end
exists aws profile AdministratorAccess-123456789012 start
exists aws profile AdministratorAccess-123456789012 end
aws sso login with profile AdministratorAccess-123456789012 start
aws sso login with profile AdministratorAccess-123456789012 end
get pulumi state url for stack dev in profile AdministratorAccess-123456789012 start
get pulumi state url for stack dev in profile AdministratorAccess-123456789012 end
pulumi login for stack dev in profile AdministratorAccess-123456789012 start
Logged in to hoge.local as hoge (s3://dev-otlp-endpoint-lambda-rust-123456789012?awssdk=v2&profile=AdministratorAccess-123456789012)
pulumi login for stack dev in profile AdministratorAccess-123456789012 end
pulumi stack select for stack dev in profile AdministratorAccess-123456789012 start
pulumi stack select for stack dev in profile AdministratorAccess-123456789012 end
warning: using pulumi-language-nodejs from $PATH at /Users/hoge/workspace/otlp-endpoint-lambda-rust/.devbox/nix/profile/default/bin/pulumi-language-nodejs
まとめ
Pulumi スタックの切り替えには、AWS Profile、git ブランチ、DIY バックエンドなど複数の要素を同時にかつ一貫して切り替える必要があります。これらの作業ミスが発生すると、想定しない環境にインフラリソースがデプロイされてしまうという大きな問題につながります。
この問題を解決するため、AWS Profile を切り替えるawspを拡張した awspp コマンドを作成しました。
もしよろしければ導入してみたり、自分向けの環境に合わせて自由にスクリプトを改良してみたりしてください。
Discussion