Last active
April 25, 2025 17:16
-
-
Save chattr/5ab07ebb3b8defc1bb422710eef60a82 to your computer and use it in GitHub Desktop.
Auto-configure AWS CLI ~/.aws/config with SSO roles
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
# | |
# Auto-configure AWS CLI ~/.aws/config with SSO roles | |
# | |
# | |
set -o nounset -o pipefail | |
# defaults if unset or null | |
: "${default_region:=eu-west-1}" | |
: "${region_dev:=eu-west-1}" | |
: "${region_qa:=eu-west-1}" | |
: "${region_staging:=eu-central-1}" | |
: "${region_prod:=eu-central-1}" | |
: "${sso_region:=us-east-1}" | |
: "${sso_start_url:=https://d-0123456789.awsapps.com/start}" | |
: "${config_backup_suffix:=$(date +%Y%m%d-%H%M)}" | |
_die() { | |
# restore AWS config from backup | |
trap 'cp ~/.aws/config{."${config_backup_suffix}",}' SIGINT SIGTERM EXIT | |
# print error to stderr and exit | |
printf "\nERROR: %s\n" "$1" >&2 | |
exit "${2:-1}" | |
} | |
# check version of bash and exit if not using at least version 4 | |
[[ ${BASH_VERSINFO[0]} -lt 4 ]] && _die 'This script requires bash 4. Aborting' | |
_cleanup() { | |
rm -f "${tempfile-}" | |
} | |
trap _cleanup EXIT | |
trap _cleanup SIGINT | |
_check_cmd() { | |
command -v "$1" > /dev/null 2>&1 || _die "${1} not installed. Aborting." | |
} | |
_check_cmd aws | |
_check_cmd jq | |
_print_guidance() { | |
local use_env_region=${1:-false} | |
# ANSI escape codes | |
bright_green='\033[1;32m' | |
reset='\033[0m' | |
printf '%b\n' \ | |
"" \ | |
"๐ AWS config created! ๐" \ | |
"" | |
if [ "$use_env_region" != "true" ]; then | |
printf '%b\n' \ | |
"Each profile has been configured with the default region ${bright_green}\"${default_region}\"${reset}" \ | |
"" \ | |
"You may wish to update the value assigned to \"region\" for each profile where appropriate in \"~/.aws/config\"" \ | |
"" | |
fi | |
} | |
_pre_flight() { | |
# unset session variable if it's set | |
unset AWS_DEFAULT_PROFILE | |
# backup existing AWS config | |
if find ~/.aws/config > /dev/null; then | |
mv ~/.aws/config{,."${config_backup_suffix}"} | |
fi | |
# empty sso cache to mitigate conflicts with stale data | |
rm -rf ~/.aws/sso/ | |
# seed boilerplate AWS config | |
printf '%s\n' \ | |
"[default]" \ | |
"region = ${default_region}" \ | |
"output = json" \ | |
"" \ | |
"[sso-session login]" \ | |
"sso_start_url = ${sso_start_url}" \ | |
"sso_region = ${sso_region}" \ | |
"sso_registration_scopes = sso:account:access" > ~/.aws/config | |
} | |
_spinner() { | |
local pid=$1 | |
local delay=0.1 | |
local spinstr="|/-\\" | |
tput civis # hide cursor | |
while kill -0 "$pid" 2>/dev/null; do | |
for i in $(seq 0 3); do | |
printf "\r[%c] Processing..." "${spinstr:i:1}" | |
sleep "$delay" | |
done | |
done | |
printf "\r \r" # clear spinner line | |
tput cnorm # show cursor | |
} | |
_pre_flight || _die 'Pre-flight tasks failed. Aborting.' | |
# login to AWS with SSO | |
"aws" sso login --sso-session login || _die 'AWS SSO login failed. Aborting.' | |
sso_cache="$(jq -s '.[0] * .[1]' ~/.aws/sso/cache/*)" | |
access_token="$(jq -r '.accessToken' <<< "$sso_cache")" || _die 'No access token found. You may need to aws sso login first. Aborting.' | |
start_url="$(jq -r '.startUrl' <<< "$sso_cache")" | |
region_sso=$(jq -r '.region // "us-east-1"' <<< "$sso_cache") | |
# default to "$region_sso" if unset or null | |
: "${assume_role_region:="$region_sso"}" | |
accounts_data="$("aws" sso list-accounts --region "$region_sso" --access-token "$access_token")" || _die 'You may need to aws sso login first. Aborting.' | |
accounts_list="$(jq -r '.accountList[].accountId' <<< "$accounts_data")" | |
tempfile="$(mktemp /tmp/aws_sso.XXXXXX)" | |
# present an option to set the region based on the profile name having dev, qa, staging and prod | |
read -r -p "Do you want to set the region for each profile based on environment (e.g. dev, prod) inset in the profile name? [y/n] " yn | |
# set a flag based on user input | |
use_env_based_region=false | |
case "$yn" in | |
[Yy]*) use_env_based_region=true ;; | |
esac | |
( | |
# collect profiles for sorting | |
profile_entries=() | |
while IFS= read -r account_id; do | |
account_data="$(jq -r ".accountList | .[] | select( .accountId == \"$account_id\" )" <<< "$accounts_data")" | |
account_name="$(jq -r '.accountName // .accountId' <<< "$account_data")" | |
account_roles="$("aws" sso list-account-roles --region "$region_sso" --access-token "$access_token" --account-id "$account_id")" | |
role_names="$(jq -r '.roleList | .[] | .roleName' <<< "$account_roles")" | |
while read -r role_name; do | |
config_profile_name="${account_name}-${role_name}" | |
profile_entries+=("${config_profile_name}|${account_id}|${role_name}") | |
done <<< "$role_names" | |
done <<< "$accounts_list" | |
# sort profile entries alphabetically | |
IFS=$'\n' mapfile -t sorted_entries < <(printf '%s\n' "${profile_entries[@]}" | sort) | |
# process sorted entries | |
for entry in "${sorted_entries[@]}"; do | |
IFS='|' read -r config_profile_name account_id role_name <<< "$entry" | |
if ! grep -q "$config_profile_name" ~/.aws/config; then | |
# default region (can override below) | |
region_value="${default_region}" | |
# override region based on environment if user opted in | |
if [ "$use_env_based_region" = true ]; then | |
case "$config_profile_name" in | |
*dev*) region_value="${region_dev}" ;; | |
*qa*) region_value="${region_qa}" ;; | |
*staging*) region_value="${region_staging}" ;; | |
*prod*) region_value="${region_prod}" ;; | |
*) region_value="${default_region}" ;; | |
esac | |
fi | |
printf '%s\n' \ | |
"" \ | |
"[profile ${config_profile_name}]" \ | |
"sso_start_url = ${start_url}" \ | |
"sso_region = ${region_sso}" \ | |
"sso_account_id = ${account_id}" \ | |
"sso_role_name = ${role_name}" \ | |
"sts_regional_endpoints = regional" \ | |
"region = ${region_value}" \ | |
"sso_registration_scopes = sso:account:access" >> "$tempfile" | |
fi | |
done | |
) & _spinner $! | |
# check if there's config to append to ~/.aws/config | |
if [[ -s "$tempfile" ]]; then | |
new_profile_count="$(grep -c '^\[profile' "$tempfile")" | |
cat "$tempfile" | |
printf "\nINFO: %s\n" "$new_profile_count profile(s) will be added to ~/.aws/config:" | |
read -r -p "Do you want to proceed? [y/n] " yn | |
case $yn in | |
[Yy]* ) cat "$tempfile" >> ~/.aws/config; _print_guidance "$use_env_based_region"; ;; | |
* ) echo "aborted!";; | |
esac | |
else | |
printf "\nINFO: %s\n" "There is no config to append to ~/.aws/config. Exiting." | |
exit 0 | |
fi |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment