🔄

Pulumi スタック と AWS Profile を紐付けて切り替える awspp コマンドを作った

に公開

TL;DR

  • fzfベースの awspコマンドを拡張して Pulumi Stack の切り替えに合わせて AWS Profile を切り替える awspp コマンドを作成しました。

はじめに

AWS Profile を良い感じに切り替える方法として、fzf を利用したシンプルな awsp コマンドの実装が下記のスクラップで紹介されています。

https://zenn.dev/ryu022304/scraps/1a702c7a1edfa0

単純に、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パターンが想定され。

  1. ブランチのミス
  2. AWS Profile 指定のミス
  3. スタック指定のミス

があります。
このため、この手順を自動化することがデプロイ作業ミスを減らすために必須の課題となります。

この問題は少々お金をかければ、Pulumi Console やデプロイ作業用のサーバー、専用の CD パイプラインを用意することで、スタックや AWS Profile そのものを切り替えるというような作業そのものをなくすことで回避することも可能です。

しかしながら、サーバーレスをコンセプトとする小さく開発を始めたい環境では、これらデプロイ専用のインフラリソースを準備するのはオーバースペック気味だったので、よりシンプルな解決策として、awspを拡張するという方法をとることにしました。

awspp コマンドを作った

先述の問題を解決するための作成したのが、awsppコマンドです。(追加のppluspulumiの意です)

下記がコマンドの全体です。(長いので折り畳んでいます。)

awspp コマンド
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 "$@"

セットアップ

  • SSO ログインの必要な AWS Profile を想定
  • fzf, yq, gitコマンドがインストール済みであること
  • スタックファイルへのaws:profilebackend:urlの追記
Pulumi.dev.yaml
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でスタック名を指定すると、prodstgなど特定の環境の場合は自動で mainブランチへの切り替えを行い、スタックファイルに書き込まれた AWS Profile と S3 の DIY バックエンドを参照して、自動で切り替えを行なってくれます。

bash
source awspp

# オプションからスタックを指定する
# source awspp -s dev

# 指定のブランチに切り替える
# source awspp -s dev -b feature-hoge
source awspp のログ
stdout
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