#!/usr/bin/env bash set -xe ############################################################################### # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # This file is licensed under the Apache License, Version 2.0 (the "License"). # # You may not use this file except in compliance with the License. A copy of # the License is located at http://aws.amazon.com/apache2.0/. # # This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR # CONDITIONS OF ANY KIND, either express or implied. See the License for the # specific language governing permissions and limitations under the License. ############################################################################### # # This script contains general-purpose functions that are used throughout # the AWS Command Line Interface (AWS CLI) code examples that are maintained # in the repo at https://github.com/awsdocs/aws-doc-sdk-examples. # # They are intended to abstract functionality that is required for the tests # to work without cluttering up the code. The intent is to ensure that the # purpose of the code is clear. # Set arguments. SOURCE_STACK_SET_NAME=$1 # The name of the source CloudFormation StackSet. TARGET_STACK_SET_NAME=$2 # The name of the target CloudFormation StackSet. CLOUDFORMATION_ADMIN_ROLE_ARN=$3 # The ARN of the CloudFormation admin role ARN. CLOUDFORMATION_EXECUTION_ROLE_NAME=$4 # The name of the CloudFormation execution role. SOURCE_STACK_SET_REGIONS=$5 # A list of regions to delete the source stack instances from. Required to delete stack instances from the source stack set. SOURCE_STACK_ACCOUNT_LIST=$6 # A specific list of account IDs for the source StackSet. Allows limiting the migration to a subset of specific stacks in the stackset. # Usage and Overview # This script is designed to perform stack instance migrations from a source stack set to a target stack set. # As part of this process, you may specify a list of source stack account IDs for stack instances to move. # If SOURCE_STACK_ACCOUNT_LIST is provided, only those stacks in the source stack set will be migrated. # In order to update the target stack once stack instances are migrated, please provide the CLOUDFORMATION_ADMIN_ROLE_ARN and the CLOUDFORMATION_EXECUTION_ROLE. # # # Run the command as seen below to execute the migrate script: # ./migrate.sh [SOURCE_STACK_SET_NAME] [TARGET_STACK_SET_NAME] [CLOUDFORMATION_ADMIN_ROLE_ARN] [CLOUDFORMATION_EXECUTION_ROLE_NAME] [SOURCE_STACK_SET_REGIONS] [SOURCE_STACK_ACCOUNT_LIST] # Define required arguments for execution control. if [ -z "$1" ] then echo "No source stack set name was provided." exit 1 fi if [ -z "$2" ] then echo "No target stack set name was provided." echo "In order to migrate stacks, please provide a target stack set name." exit 1 fi if [ -z "$3" ] then echo "No CloudFormation admin role ARN was provided." echo "In order to update the target stack set, specify the CloudFormation administration role ARN." exit 1 fi if [ -z "$4" ] then echo "No CloudFormation execution role name was provided." echo "In order to update the target stack set, specify the CloudFormation execution role name." exit 1 fi if [ -z "$5" ] then echo "No source stack set regions were specified." echo "Please specify the list of regions from the source stack set." exit 1 fi if [ -z "$6" ] then echo "No source stack account list was provided." echo "In order to migrate specific stack instances, specify a list of AWS Account IDs for the stacks to be migrated." fi # Check for prereqs. if ! command -v jq &> /dev/null then echo "jq could not be found, please install jq." exit fi if ! command -v aws &> /dev/null then echo "AWS CLI could not be found, please install AWS CLI version 2 or higher." exit fi AWS_CLI_MAJOR_VERSION=$(aws --version 2>&1 | cut -d " " -f1 | cut -d "/" -f2 | cut -d "." -f1) if [ "$AWS_CLI_MAJOR_VERSION" -lt 2 ] then echo "Upgrade AWS CLI version to 2.x $(aws --version 2>&1)" exit fi # Return the primary region from the list. All AWS CLI commands will execute against the primary region. printf -v PRIMARY_REGION "%s" "${SOURCE_STACK_SET_REGIONS%% *}" printf 'The primary region is: %s \n' "$PRIMARY_REGION" # Prepare the artifact subfolders. printf "Creating the folder structure for the script execution.\\n" mkdir -p artifacts/"$SOURCE_STACK_SET_NAME" # Evaluate the source stack set and retrieve a list of stack instances. printf 'Retrieving a list of stack instances belonging to the %s stack set...\n' "$SOURCE_STACK_SET_NAME" SOURCE_STACK_SET_INSTANCES=$( \ aws cloudformation list-stack-instances --stack-set-name "$SOURCE_STACK_SET_NAME" --region "$PRIMARY_REGION" ) # Retrieve the Account IDs from the stack instances to create an output list for other functions. SOURCE_STACK_SET_ACCOUNT_IDS=$(echo "$SOURCE_STACK_SET_INSTANCES" | jq -r '.Summaries[] | .Account') if [[ -n "$SOURCE_STACK_SET_ACCOUNT_IDS" ]] then printf 'Creating a list of account IDs for the stack instances in the %s stack set.\n' "$SOURCE_STACK_SET_NAME" printf '%s' "$SOURCE_STACK_SET_ACCOUNT_IDS" | tee ./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_set_account_ids.txt printf "Stack instance account IDs list created successfully.\\n" else printf 'No stack instances were found belonging to the %s stack set.\n' "$SOURCE_STACK_SET_NAME" printf 'Verify that the %s stack set contains stack instances and try again.\n' "$SOURCE_STACK_SET_NAME" exit 1 fi # Retrieve the stack instance ARNs from the stack instances to create an output list for other functions. SOURCE_STACK_SET_STACK_INSTANCE_ARNS=$(echo "$SOURCE_STACK_SET_INSTANCES" | jq -r '.Summaries[] | .StackId') if [[ -n "$SOURCE_STACK_SET_STACK_INSTANCE_ARNS" ]] then printf '%s' "$SOURCE_STACK_SET_STACK_INSTANCE_ARNS" | tee ./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_set_instance_arns.txt printf "Stack instance ARNs list created successfully.\\n" else printf 'No stack instances were found belonging to the %s stack set.\n' "$SOURCE_STACK_SET_NAME" printf 'Verify that the %s stack set contains stack instances and try again.\n' "$SOURCE_STACK_SET_NAME" exit 1 fi # Create an output artifacts containing the ARNs, regions, and account IDs for the stack instances being migrated. if [[ -n "$SOURCE_STACK_ACCOUNT_LIST" ]] then # Create an array using the SOURCE_STACK_ACCOUNT_LIST. printf "Generating an array from the account list input.\\n" IFS=, read -a SOURCE_STACK_ACCOUNT_ARRAY <<<"${SOURCE_STACK_ACCOUNT_LIST}" # printf -v SOURCE_ACCOUNTS '%s ' "${SOURCE_STACK_ACCOUNT_ARRAY[@]}" printf '%s' "${SOURCE_STACK_ACCOUNT_ARRAY[@]}" | tee ./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_account_list.txt printf "Generating an array from the region list input.\\n" IFS=' ' read -a SOURCE_STACK_REGION_ARRAY <<<"${SOURCE_STACK_SET_REGIONS}" printf -v SOURCE_REGIONS '%s' "${SOURCE_STACK_REGION_ARRAY[@]}" printf '%s' "${SOURCE_STACK_REGION_ARRAY[@]}" | tee ./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_region_list.txt # Create artifacts for script execution. SOURCE_STACKSET_ARNS=./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_set_instance_arns.txt SOURCE_ACCOUNT_LIST=./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_account_list.txt SOURCE_ACCOUNT_LIST_ARNS=./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_account_list_arns.txt SOURCE_REGION_LIST=./artifacts/"$SOURCE_STACK_SET_NAME"/source_stack_region_list.txt #SOURCE_MIGRATION_LIST=./artifacts/"$SOURCE_STACK_SET_NAME"/source_migration_list.txt # Run grep to filter on the account list for ARN IDs and output to file. printf "Creating an output file containing the source stack account list ARNs.\\n" SOURCE_LIST=$(grep -f "${SOURCE_ACCOUNT_LIST}" "${SOURCE_STACKSET_ARNS}" > "${SOURCE_ACCOUNT_LIST_ARNS}") # Run grep to filter the ARNs based on the specified region list. printf "Filtering the source stack account list ARNs by the region list.\\n" printf '%s\n' "${SOURCE_LIST[@]}" SOURCE_ARNS=$(grep -f "${SOURCE_REGION_LIST}" "${SOURCE_ACCOUNT_LIST_ARNS}") printf "The following ARN IDs are matched by account ID and region for migration:\\n" printf '%s\n' "${SOURCE_ARNS[@]}" else printf "A source stack instance list was provided.\\n" fi # If the SOURCE_STACK_INSTANCE_LIST is not provided, use the SOURCE_STACK_SET_INSTANCE_IDS. if [[ -n "$SOURCE_STACK_ACCOUNT_LIST" ]] then printf 'The source stack instance list will be used rather than the list of stack IDs from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" printf "Generating an array of stacks to migrate.\\n" IFS=, read -a STACKS_TO_MIGRATE <<<"${SOURCE_STACK_ACCOUNT_ARRAY[@]}" else printf "A source stack account list was not provided.\\n" printf "Continuing...\\n" printf 'The script will migrate all stack instances in the %s stack set.\n' "$SOURCE_STACK_SET_NAME" IFS=, read -a STACKS_TO_MIGRATE <<<"${SOURCE_STACK_SET_ACCOUNT_IDS[@]}" fi echo $STACKS_TO_MIGRATE # Print out the list of stack instances to be migrated. printf "The following account IDs will have stacks migrated:\\n" printf '%s\n' "${STACKS_TO_MIGRATE[@]}" printf "Stack instances will now be migrated.\\n" printf "Source instances will be deleted and retained.\\n" # Delete and retain specified stacks from the source stack set. # Will NOT perform destructive actions. RETAINS the stack instance in the account. if [[ -n "$SOURCE_STACK_ACCOUNT_LIST" ]] then printf 'Deleting the specified stack instances from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" printf 'Deleting stack instance(s) for accounts %s in the %s stackset.\n' "$SOURCE_STACK_ACCOUNT_LIST" "$SOURCE_STACK_SET_NAME" DELETE_OPERATION=$(aws cloudformation delete-stack-instances --stack-set-name "$SOURCE_STACK_SET_NAME" --deployment-targets Accounts="${SOURCE_STACK_ACCOUNT_LIST}" --regions "$SOURCE_STACK_SET_REGIONS" --retain-stacks --region "$PRIMARY_REGION" --operation-preferences RegionConcurrencyType=PARALLEL,FailureToleranceCount=9,MaxConcurrentCount=10) DELETE_OPERATION_ID=$(echo "$DELETE_OPERATION" | jq -r '.OperationId') printf 'Operation ID: %s \n' "$DELETE_OPERATION_ID" # Nested loop to check for SUCCEEDED status of the delete stack instance operation. while [ "$(aws cloudformation describe-stack-set-operation --stack-set-name "$SOURCE_STACK_SET_NAME" --operation-id "$DELETE_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "SUCCEEDED" ]; do printf "Waiting for delete operation to complete.\\n" sleep 10 done printf 'Completed deleting stacks from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" else printf "Continuing...\\n" fi # Delete and retain all stack instances for the source stack set if evaluated TRUE. # Will NOT perform destructive actions. RETAINS the stack instance in the account. if [[ -z "$SOURCE_STACK_ACCOUNT_LIST" ]] then printf 'Deleting all stack instances from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" printf 'Deleting stack instance(s) in the %s stackset.\n' "$SOURCE_STACK_SET_NAME" ALL_DELETE_OPERATION=$(aws cloudformation delete-stack-instances --stack-set-name "$SOURCE_STACK_SET_NAME" --deployment-targets Accounts="${SOURCE_STACK_SET_ACCOUNT_IDS}" --regions "$SOURCE_STACK_SET_REGIONS" --retain-stacks --region "$PRIMARY_REGION" --operation-preferences RegionConcurrencyType=PARALLEL,FailureToleranceCount=9,MaxConcurrentCount=10) ALL_DELETE_OPERATION_ID=$(echo "$ALL_DELETE_OPERATION" | jq -r '.OperationId') printf 'Operation ID: %s \n' "$ALL_DELETE_OPERATION_ID" # Nested loop to check for SUCCEEDED status of the delete stack instance operation. while [ "$(aws cloudformation describe-stack-set-operation --stack-set-name "$SOURCE_STACK_SET_NAME" --operation-id "$ALL_DELETE_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "SUCCEEDED" ]; do printf "Waiting for delete operation to complete.\\n" sleep 10 done printf 'Successfully deleted and retained stack instance(s) for accounts %s in the %s stackset.\n' "${SOURCE_STACK_ACCOUNT_LIST}" "$SOURCE_STACK_SET_NAME" printf 'Completed deleting stacks from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" else printf "Continuing...\\n" fi # Import specified stack instances from the source stack set to the target stack set. if [[ -n "$SOURCE_STACK_ACCOUNT_LIST" && -n "$SOURCE_ARNS" ]] then printf 'Importing the specified stack instances from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" IMPORT_OPERATION=$(aws cloudformation import-stacks-to-stack-set --stack-set-name "$TARGET_STACK_SET_NAME" --stack-ids "$SOURCE_ARNS" --region "$PRIMARY_REGION") IMPORT_OPERATION_ID=$(echo "$IMPORT_OPERATION" | jq -r '.OperationId') printf 'Operation ID: %s \n' "$IMPORT_OPERATION_ID" # Nested loop to check for FAILED status of the import stack to stack set operation. # We expect a FAILED condition due to tag mismatches between LZ pipeline and CfCT pipeline. while [[ "$(aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$IMPORT_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "FAILED" && "$(aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$IMPORT_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "SUCCEEDED" ]]; do printf "Waiting for import operation to complete.\\n" sleep 20 done printf 'Operation ID: %s may have failed to import due to the following reason, which is ok if the following update command succeeds:\n' "$IMPORT_OPERATION_ID" aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$IMPORT_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .StatusReason' printf 'Completed importing stacks to the %s stack set.\n' "$TARGET_STACK_SET_NAME" else printf "Continuing...\\n" fi # Import all stack instances from the source stack set to the target stack set. if [[ -z "$SOURCE_STACK_ACCOUNT_LIST" && -n "$SOURCE_STACK_SET_STACK_INSTANCE_ARNS" ]] then printf 'Importing all stack instances from the %s stack set.\n' "$SOURCE_STACK_SET_NAME" ALL_IMPORT_OPERATION=$(aws cloudformation import-stacks-to-stack-set --stack-set-name "$TARGET_STACK_SET_NAME" --stack-ids "$SOURCE_STACK_SET_STACK_INSTANCE_ARNS" --region "$PRIMARY_REGION") ALL_IMPORT_OPERATION_ID=$(echo "$ALL_IMPORT_OPERATION" | jq -r '.OperationId') printf 'Operation ID: %s \n' "$ALL_IMPORT_OPERATION_ID" # Nested loop to check for FAILED status of the import stack to stack set operation. # We expect a FAILED condition due to tag mismatches between LZ pipeline and CfCT pipeline. while [[ "$(aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$ALL_IMPORT_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "FAILED" && "$(aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$ALL_IMPORT_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "SUCCEEDED" ]]; do printf "Waiting for import operation to complete.\\n" sleep 20 done printf 'Operation ID: %s may have failed to import due to the following reason, which is ok if the following update command succeeds:\n' "$ALL_IMPORT_OPERATION_ID" aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$ALL_IMPORT_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .StatusReason' printf 'Completed importing stacks to the %s stack set.\n' "$TARGET_STACK_SET_NAME" else printf "Only the specified stack instance(s) will be migrated.\\n" fi # Perform an update of the target stack set and wait for confirmation. if [[ -n "${STACKS_TO_MIGRATE[*]}" ]] then printf 'Triggering stack set update on the %s stack set.\n' "$TARGET_STACK_SET_NAME" UPDATE_OPERATION=$(aws cloudformation update-stack-set --stack-set-name "$TARGET_STACK_SET_NAME" --use-previous-template --administration-role-arn "$CLOUDFORMATION_ADMIN_ROLE_ARN" --execution-role-name "$CLOUDFORMATION_EXECUTION_ROLE_NAME" --capabilities CAPABILITY_NAMED_IAM --region "$PRIMARY_REGION") UPDATE_OPERATION_ID=$(echo "$UPDATE_OPERATION" | jq -r '.OperationId') printf 'Operation ID: %s \n' "$UPDATE_OPERATION_ID" while [ "$(aws cloudformation describe-stack-set-operation --stack-set-name "$TARGET_STACK_SET_NAME" --operation-id "$UPDATE_OPERATION_ID" --region "$PRIMARY_REGION" | jq -r '.StackSetOperation | .Status')" != "SUCCEEDED" ]; do printf "Waiting for update operation to complete.\\n" sleep 20 done printf 'Completed updating the %s stack set.\n' "$TARGET_STACK_SET_NAME" else printf 'Failed to update the %s stack set.\n' "$TARGET_STACK_SET_NAME" printf 'Check the %s stack set for imported stack instances and validate manually.\n' "$TARGET_STACK_SET_NAME" fi ./status_report.sh $SOURCE_STACK_SET_NAME $TARGET_STACK_SET_NAME $PRIMARY_REGION # Print closing summary. printf "Stack instance migration complete.\\n"