View a markdown version of this page

Amazon EC2 examples using AWS CLI with Bash script - AWS Command Line Interface

Amazon EC2 examples using AWS CLI with Bash script

The following code examples show you how to perform actions and implement common scenarios by using the AWS Command Line Interface with Bash script with Amazon EC2.

Basics are code examples that show you how to perform the essential operations within a service.

Actions are code excerpts from larger programs and must be run in context. While actions show you how to call individual service functions, you can see actions in context in their related scenarios.

Scenarios are code examples that show you how to accomplish specific tasks by calling multiple functions within a service or combined with other AWS services.

Each example includes a link to the complete source code, where you can find instructions on how to set up and run the code in context.

Basics

The following code example shows how to:

  • Create a key pair and security group.

  • Select an Amazon Machine Image (AMI) and compatible instance type, then create an instance.

  • Stop and restart the instance.

  • Associate an Elastic IP address with your instance.

  • Connect to your instance with SSH, then clean up resources.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

Run an interactive scenario at a command prompt.

############################################################################### # function get_started_with_ec2_instances # # Runs an interactive scenario that shows how to get started using EC2 instances. # # "EC2 access" permissions are needed to run this code. # # Returns: # 0 - If successful. # 1 - If an error occurred. ############################################################################### function get_started_with_ec2_instances() { # Requires version 4 for mapfile. local required_version=4.0 # Get the current Bash version # Check if BASH_VERSION is set local current_version if [[ -n "$BASH_VERSION" ]]; then # Convert BASH_VERSION to a number for comparison current_version=$BASH_VERSION else # Get the current Bash version using the bash command current_version=$(bash --version | head -n 1 | awk '{ print $4 }') fi # Convert version strings to numbers for comparison local required_version_num current_version_num required_version_num=$(echo "$required_version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }') current_version_num=$(echo "$current_version" | awk -F. '{ print ($1 * 10000) + ($2 * 100) + $3 }') # Compare versions if ((current_version_num < required_version_num)); then echo "Error: This script requires Bash version $required_version or higher." echo "Your current Bash version is number is $current_version." exit 1 fi { if [ "$EC2_OPERATIONS_SOURCED" != "True" ]; then source ./ec2_operations.sh fi } echo_repeat "*" 88 echo "Welcome to the Amazon Elastic Compute Cloud (Amazon EC2) get started with instances demo." echo_repeat "*" 88 echo echo "Let's create an RSA key pair that you can be use to securely connect to " echo "your EC2 instance." echo -n "Enter a unique name for your key: " get_input local key_name key_name=$get_input_result local temp_dir temp_dir=$(mktemp -d) local key_file_name="$temp_dir/${key_name}.pem" if ec2_create_keypair -n "${key_name}" -f "${key_file_name}"; then echo "Created a key pair $key_name and saved the private key to $key_file_name" echo else errecho "The key pair failed to create. This demo will exit." return 1 fi chmod 400 "${key_file_name}" if yes_no_input "Do you want to list some of your key pairs? (y/n) "; then local keys_and_fingerprints keys_and_fingerprints="$(ec2_describe_key_pairs)" && { local image_name_and_id while IFS=$'\n' read -r image_name_and_id; do local entries IFS=$'\t' read -ra entries <<<"$image_name_and_id" echo "Found rsa key ${entries[0]} with fingerprint:" echo " ${entries[1]}" done <<<"$keys_and_fingerprints" } fi echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's create a security group to manage access to your instance." echo -n "Enter a unique name for your security group: " get_input local security_group_name security_group_name=$get_input_result local security_group_id security_group_id=$(ec2_create_security_group -n "$security_group_name" \ -d "Security group for EC2 instance") || { errecho "The security failed to create. This demo will exit." clean_up "$key_name" "$key_file_name" return 1 } echo "Security group created with ID $security_group_id" echo local public_ip public_ip=$(curl -s http://checkip.amazonaws.com) echo "Let's add a rule to allow SSH only from your current IP address." echo "Your public IP address is $public_ip" echo -n "press return to add this rule to your security group." get_input if ! ec2_authorize_security_group_ingress -g "$security_group_id" -i "$public_ip" -p tcp -f 22 -t 22; then errecho "The security group rules failed to update. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 fi echo "Security group rules updated" local security_group_description security_group_description="$(ec2_describe_security_groups -g "${security_group_id}")" || { errecho "Failed to describe security groups. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } mapfile -t parameters <<<"$security_group_description" IFS=$'\t' read -ra entries <<<"${parameters[0]}" echo "Security group: ${entries[0]}" echo " ID: ${entries[1]}" echo " VPC: ${entries[2]}" echo "Inbound permissions:" IFS=$'\t' read -ra entries <<<"${parameters[1]}" echo " IpProtocol: ${entries[0]}" echo " FromPort: ${entries[1]}" echo " ToPort: ${entries[2]}" echo " CidrIp: ${parameters[2]}" local parameters parameters="$(ssm_get_parameters_by_path -p "/aws/service/ami-amazon-linux-latest")" || { errecho "Failed to get parameters. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } local image_ids="" mapfile -t parameters <<<"$parameters" for image_name_and_id in "${parameters[@]}"; do IFS=$'\t' read -ra values <<<"$image_name_and_id" if [[ "${values[0]}" == *"amzn2"* ]]; then image_ids+="${values[1]} " fi done local images images="$(ec2_describe_images -i "$image_ids")" || { errecho "Failed to describe images. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } new_line_and_tab_to_list "$images" local images=("${list_result[@]}") # Get the size of the array local images_count=${#images[@]} if ((images_count == 0)); then errecho "No images found. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 fi echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's create an instance from an Amazon Linux 2 AMI. Here are some options:" for ((i = 0; i < images_count; i += 3)); do echo "$(((i / 3) + 1)) - ${images[$i]}" done integer_input "Please enter the number of the AMI you want to use: " 1 "$((images_count / 3))" local choice=$get_input_result choice=$(((choice - 1) * 3)) echo "Great choice." echo local architecture=${images[$((choice + 1))]} local image_id=${images[$((choice + 2))]} echo "Here are some instance types that support the ${architecture} architecture of the image:" response="$(ec2_describe_instance_types -a "${architecture}" -t "*.micro,*.small")" || { errecho "Failed to describe instance types. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } local instance_types mapfile -t instance_types <<<"$response" # Get the size of the array local instance_types_count=${#instance_types[@]} echo "Here are some options:" for ((i = 0; i < instance_types_count; i++)); do echo "$((i + 1)) - ${instance_types[$i]}" done integer_input "Which one do you want to use? " 1 "${#instance_types[@]} " choice=$get_input_result local instance_type=${instance_types[$((choice - 1))]} echo "Another great choice." echo echo "Creating your instance and waiting for it to start..." local instance_id instance_id=$(ec2_run_instances -i "$image_id" -t "$instance_type" -k "$key_name" -s "$security_group_id") || { errecho "Failed to run instance. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" return 1 } ec2_wait_for_instance_running -i "$instance_id" echo "Your instance is ready:" echo local instance_details instance_details="$(ec2_describe_instances -i "${instance_id}")" echo print_instance_details "${instance_details}" local public_ip public_ip=$(echo "${instance_details}" | awk '{print $6}') echo echo "You can use SSH to connect to your instance" echo "If the connection attempt times out, you might have to manually update the SSH ingress rule" echo "for your IP address in the AWS Management Console." connect_to_instance "$key_file_name" "$public_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's stop and start your instance to see what changes." echo "Stopping your instance and waiting until it's stopped..." ec2_stop_instances -i "$instance_id" ec2_wait_for_instance_stopped -i "$instance_id" echo "Your instance is stopped. Restarting..." ec2_start_instances -i "$instance_id" ec2_wait_for_instance_running -i "$instance_id" echo "Your instance is running again." local instance_details instance_details="$(ec2_describe_instances -i "${instance_id}")" print_instance_details "${instance_details}" public_ip=$(echo "${instance_details}" | awk '{print $6}') echo "Every time your instance is restarted, its public IP address changes" connect_to_instance "$key_file_name" "$public_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 echo "You can allocate an Elastic IP address and associate it with your instance" echo "to keep a consistent IP address even when your instance restarts." local result result=$(ec2_allocate_address -d vpc) || { errecho "Failed to allocate an address. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" "$instance_id" return 1 } local elastic_ip allocation_id elastic_ip=$(echo "$result" | awk '{print $1}') allocation_id=$(echo "$result" | awk '{print $2}') echo "Allocated static Elastic IP address: $elastic_ip" local association_id association_id=$(ec2_associate_address -i "$instance_id" -a "$allocation_id") || { errecho "Failed to associate an address. This demo will exit." clean_up "$key_name" "$key_file_name" "$security_group_id" "$instance_id" "$allocation_id" return 1 } echo "Associated your Elastic IP with your instance." echo "You can now use SSH to connect to your instance by using the Elastic IP." connect_to_instance "$key_file_name" "$elastic_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 echo "Let's stop and start your instance to see what changes." echo "Stopping your instance and waiting until it's stopped..." ec2_stop_instances -i "$instance_id" ec2_wait_for_instance_stopped -i "$instance_id" echo "Your instance is stopped. Restarting..." ec2_start_instances -i "$instance_id" ec2_wait_for_instance_running -i "$instance_id" echo "Your instance is running again." local instance_details instance_details="$(ec2_describe_instances -i "${instance_id}")" print_instance_details "${instance_details}" echo "Because you have associated an Elastic IP with your instance, you can" echo "connect by using a consistent IP address after the instance restarts." connect_to_instance "$key_file_name" "$elastic_ip" echo -n "Press Enter when you're ready to continue the demo: " get_input echo_repeat "*" 88 echo_repeat "*" 88 if yes_no_input "Do you want to delete the resources created in this demo: (y/n) "; then clean_up "$key_name" "$key_file_name" "$security_group_id" "$instance_id" \ "$allocation_id" "$association_id" else echo "The following resources were not deleted." echo "Key pair: $key_name" echo "Key file: $key_file_name" echo "Security group: $security_group_id" echo "Instance: $instance_id" echo "Elastic IP address: $elastic_ip" fi } ############################################################################### # function clean_up # # This function cleans up the created resources. # $1 - The name of the ec2 key pair to delete. # $2 - The name of the key file to delete. # $3 - The ID of the security group to delete. # $4 - The ID of the instance to terminate. # $5 - The ID of the elastic IP address to release. # $6 - The ID of the elastic IP address to disassociate. # # Returns: # 0 - If successful. # 1 - If an error occurred. ############################################################################### function clean_up() { local result=0 local key_pair_name=$1 local key_file_name=$2 local security_group_id=$3 local instance_id=$4 local allocation_id=$5 local association_id=$6 if [ -n "$association_id" ]; then # bashsupport disable=BP2002 if (ec2_disassociate_address -a "$association_id"); then echo "Disassociated elastic IP address with ID $association_id" else errecho "The elastic IP address disassociation failed." result=1 fi fi if [ -n "$allocation_id" ]; then # bashsupport disable=BP2002 if (ec2_release_address -a "$allocation_id"); then echo "Released elastic IP address with ID $allocation_id" else errecho "The elastic IP address release failed." result=1 fi fi if [ -n "$instance_id" ]; then # bashsupport disable=BP2002 if (ec2_terminate_instances -i "$instance_id"); then echo "Started terminating instance with ID $instance_id" ec2_wait_for_instance_terminated -i "$instance_id" else errecho "The instance terminate failed." result=1 fi fi if [ -n "$security_group_id" ]; then # bashsupport disable=BP2002 if (ec2_delete_security_group -i "$security_group_id"); then echo "Deleted security group with ID $security_group_id" else errecho "The security group delete failed." result=1 fi fi if [ -n "$key_pair_name" ]; then # bashsupport disable=BP2002 if (ec2_delete_keypair -n "$key_pair_name"); then echo "Deleted key pair named $key_pair_name" else errecho "The key pair delete failed." result=1 fi fi if [ -n "$key_file_name" ]; then rm -f "$key_file_name" fi return $result } ############################################################################### # function ssm_get_parameters_by_path # # This function retrieves one or more parameters from the AWS Systems Manager Parameter Store # by specifying a parameter path. # # Parameters: # -p parameter_path - The path of the parameter(s) to retrieve. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ssm_get_parameters_by_path() { local parameter_path response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ssm_get_parameters_by_path" echo "Retrieves one or more parameters from the AWS Systems Manager Parameter Store by specifying a parameter path." echo " -p parameter_path - The path of the parameter(s) to retrieve." echo "" } # Retrieve the calling parameters. while getopts "p:h" option; do case "${option}" in p) parameter_path="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$parameter_path" ]]; then errecho "ERROR: You must provide a parameter path with the -p parameter." usage return 1 fi response=$(aws ssm get-parameters-by-path \ --path "$parameter_path" \ --query "Parameters[*].[Name, Value]" \ --output text) || { aws_cli_error_log $? errecho "ERROR: AWS reports get-parameters-by-path operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function print_instance_details # # This function prints the details of an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # instance_details - The instance details in the format "InstanceId ImageId InstanceType KeyName VpcId PublicIpAddress State.Name". # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function print_instance_details() { local instance_details="$1" if [[ -z "${instance_details}" ]]; then echo "Error: Missing required instance details argument." return 1 fi local instance_id image_id instance_type key_name vpc_id public_ip state instance_id=$(echo "${instance_details}" | awk '{print $1}') image_id=$(echo "${instance_details}" | awk '{print $2}') instance_type=$(echo "${instance_details}" | awk '{print $3}') key_name=$(echo "${instance_details}" | awk '{print $4}') vpc_id=$(echo "${instance_details}" | awk '{print $5}') public_ip=$(echo "${instance_details}" | awk '{print $6}') state=$(echo "${instance_details}" | awk '{print $7}') echo " ID: ${instance_id}" echo " Image ID: ${image_id}" echo " Instance type: ${instance_type}" echo " Key name: ${key_name}" echo " VPC ID: ${vpc_id}" echo " Public IP: ${public_ip}" echo " State: ${state}" return 0 } ############################################################################### # function connect_to_instance # # This function displays the public IP address of an Amazon Elastic Compute Cloud (Amazon EC2) instance and prompts the user to connect to the instance via SSH. # # Parameters: # $1 - The name of the key file used to connect to the instance. # $2 - The public IP address of the instance. # # Returns: # None ############################################################################### function connect_to_instance() { local key_file_name="$1" local public_ip="$2" # Validate the input parameters if [[ -z "$key_file_name" ]]; then echo "ERROR: You must provide a key file name as the first argument." >&2 return 1 fi if [[ -z "$public_ip" ]]; then echo "ERROR: You must provide a public IP address as the second argument." >&2 return 1 fi # Display the public IP address and connection command echo "To connect, run the following command:" echo " ssh -i ${key_file_name} ec2-user@${public_ip}" # Prompt the user to connect to the instance if yes_no_input "Do you want to connect now? (y/n) "; then echo "After you have connected, you can return to this example by typing 'exit'" ssh -i "${key_file_name}" ec2-user@"${public_ip}" fi } ############################################################################### # function get_input # # This function gets user input from the command line. # # Outputs: # User input to stdout. # # Returns: # 0 ############################################################################### function get_input() { if [ -z "${mock_input+x}" ]; then read -r get_input_result else if [ "$mock_input_array_index" -lt ${#mock_input_array[@]} ]; then get_input_result="${mock_input_array[$mock_input_array_index]}" # bashsupport disable=BP2001 # shellcheck disable=SC2206 ((mock_input_array_index++)) echo -n "$get_input_result" else echo "MOCK_INPUT_ARRAY has no more elements" 1>&2 return 1 fi fi return 0 } ############################################################################### # function yes_no_input # # This function requests a yes/no answer from the user, following to a prompt. # # Parameters: # $1 - The prompt. # # Returns: # 0 - If yes. # 1 - If no. ############################################################################### function yes_no_input() { if [ -z "$1" ]; then echo "Internal error yes_no_input" return 1 fi local index=0 local response="N" while [[ $index -lt 10 ]]; do index=$((index + 1)) echo -n "$1" if ! get_input; then return 1 fi response=$(echo "$get_input_result" | tr '[:upper:]' '[:lower:]') if [ "$response" = "y" ] || [ "$response" = "n" ]; then break else echo -e "\nPlease enter or 'y' or 'n'." fi done echo if [ "$response" = "y" ]; then return 0 else return 1 fi } ############################################################################### # function integer_input # # This function prompts the user to enter an integer within a specified range # and validates the input. # # Parameters: # $1 - The prompt message to display to the user. # $2 - The minimum value of the accepted range. # $3 - The maximum value of the accepted range. # # Returns: # The valid integer input from the user. # If the input is invalid or out of range, the function will continue # prompting the user until a valid input is provided. ############################################################################### function integer_input() { local prompt="$1" local min_value="$2" local max_value="$3" local input="" while true; do # Display the prompt message and wait for user input echo -n "$prompt" if ! get_input; then return 1 fi input="$get_input_result" # Check if the input is a valid integer if [[ "$input" =~ ^-?[0-9]+$ ]]; then # Check if the input is within the specified range if ((input >= min_value && input <= max_value)); then return 0 else echo "Error: Input, $input, must be between $min_value and $max_value." fi else echo "Error: Invalid input- $input. Please enter an integer." fi done } ############################################################################### # function new_line_and_tab_to_list # # This function takes a string input containing newlines and tabs, and # converts it into a list (array) of elements. # # Parameters: # $1 - The input string containing newlines and tabs. # # Returns: # The resulting list (array) is stored in the global variable # 'list_result'. ############################################################################### function new_line_and_tab_to_list() { local input=$1 export list_result list_result=() mapfile -t lines <<<"$input" local line for line in "${lines[@]}"; do IFS=$'\t' read -ra parameters <<<"$line" list_result+=("${parameters[@]}") done } ############################################################################### # function echo_repeat # # This function prints a string 'n' times to stdout. # # Parameters: # $1 - The string. # $2 - Number of times to print the string. # # Outputs: # String 'n' times to stdout. # # Returns: # 0 ############################################################################### function echo_repeat() { local end=$2 for ((i = 0; i < end; i++)); do echo -n "$1" done echo }

The DynamoDB functions used in this scenario.

############################################################################### # function ec2_create_keypair # # This function creates an Amazon Elastic Compute Cloud (Amazon EC2) ED25519 or 2048-bit RSA key pair # and writes it to a file. # # Parameters: # -n key_pair_name - A key pair name. # -f file_path - File to store the key pair. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_create_keypair() { local key_pair_name file_path response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_create_keypair" echo "Creates an Amazon Elastic Compute Cloud (Amazon EC2) ED25519 or 2048-bit RSA key pair" echo " and writes it to a file." echo " -n key_pair_name - A key pair name." echo " -f file_path - File to store the key pair." echo "" } # Retrieve the calling parameters. while getopts "n:f:h" option; do case "${option}" in n) key_pair_name="${OPTARG}" ;; f) file_path="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key name with the -n parameter." usage return 1 fi if [[ -z "$file_path" ]]; then errecho "ERROR: You must provide a file path with the -f parameter." usage return 1 fi response=$(aws ec2 create-key-pair \ --key-name "$key_pair_name" \ --query 'KeyMaterial' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports create-access-key operation failed.$response" return 1 } if [[ -n "$file_path" ]]; then echo "$response" >"$file_path" fi return 0 } ############################################################################### # function ec2_describe_key_pairs # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) key pairs. # # Parameters: # -h - Display help. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_key_pairs() { local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_key_pairs" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) key pairs." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "h" option; do case "${option}" in h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local response response=$(aws ec2 describe-key-pairs \ --query 'KeyPairs[*].[KeyName, KeyFingerprint]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-key-pairs operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_create_security_group # # This function creates an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -n security_group_name - The name of the security group. # -d security_group_description - The description of the security group. # # Returns: # The ID of the created security group, or an error message if the operation fails. # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_create_security_group() { local security_group_name security_group_description response # Function to display usage information function usage() { echo "function ec2_create_security_group" echo "Creates an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -n security_group_name - The name of the security group." echo " -d security_group_description - The description of the security group." echo "" } # Parse the command-line arguments while getopts "n:d:h" option; do case "${option}" in n) security_group_name="${OPTARG}" ;; d) security_group_description="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$security_group_name" ]]; then errecho "ERROR: You must provide a security group name with the -n parameter." return 1 fi if [[ -z "$security_group_description" ]]; then errecho "ERROR: You must provide a security group description with the -d parameter." return 1 fi # Create the security group response=$(aws ec2 create-security-group \ --group-name "$security_group_name" \ --description "$security_group_description" \ --query "GroupId" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports create-security-group operation failed." errecho "$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_describe_security_groups # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) security groups. # # Parameters: # -g security_group_id - The ID of the security group to describe (optional). # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_security_groups() { local security_group_id response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_security_groups" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) security groups." echo " -g security_group_id - The ID of the security group to describe (optional)." echo "" } # Retrieve the calling parameters. while getopts "g:h" option; do case "${option}" in g) security_group_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local query="SecurityGroups[*].[GroupName, GroupId, VpcId, IpPermissions[*].[IpProtocol, FromPort, ToPort, IpRanges[*].CidrIp]]" if [[ -n "$security_group_id" ]]; then response=$(aws ec2 describe-security-groups --group-ids "$security_group_id" --query "${query}" --output text) else response=$(aws ec2 describe-security-groups --query "${query}" --output text) fi local error_code=${?} if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code errecho "ERROR: AWS reports describe-security-groups operation failed.$response" return 1 fi echo "$response" return 0 } ############################################################################### # function ec2_authorize_security_group_ingress # # This function authorizes an ingress rule for an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -g security_group_id - The ID of the security group. # -i ip_address - The IP address or CIDR block to authorize. # -p protocol - The protocol to authorize (e.g., tcp, udp, icmp). # -f from_port - The start of the port range to authorize. # -t to_port - The end of the port range to authorize. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_authorize_security_group_ingress() { local security_group_id ip_address protocol from_port to_port response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_authorize_security_group_ingress" echo "Authorizes an ingress rule for an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -g security_group_id - The ID of the security group." echo " -i ip_address - The IP address or CIDR block to authorize." echo " -p protocol - The protocol to authorize (e.g., tcp, udp, icmp)." echo " -f from_port - The start of the port range to authorize." echo " -t to_port - The end of the port range to authorize." echo "" } # Retrieve the calling parameters. while getopts "g:i:p:f:t:h" option; do case "${option}" in g) security_group_id="${OPTARG}" ;; i) ip_address="${OPTARG}" ;; p) protocol="${OPTARG}" ;; f) from_port="${OPTARG}" ;; t) to_port="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -g parameter." usage return 1 fi if [[ -z "$ip_address" ]]; then errecho "ERROR: You must provide an IP address or CIDR block with the -i parameter." usage return 1 fi if [[ -z "$protocol" ]]; then errecho "ERROR: You must provide a protocol with the -p parameter." usage return 1 fi if [[ -z "$from_port" ]]; then errecho "ERROR: You must provide a start port with the -f parameter." usage return 1 fi if [[ -z "$to_port" ]]; then errecho "ERROR: You must provide an end port with the -t parameter." usage return 1 fi response=$(aws ec2 authorize-security-group-ingress \ --group-id "$security_group_id" \ --cidr "${ip_address}/32" \ --protocol "$protocol" \ --port "$from_port-$to_port" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports authorize-security-group-ingress operation failed.$response" return 1 } return 0 } ############################################################################### # function ec2_describe_images # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) images. # # Parameters: # -i image_ids - A space-separated list of image IDs (optional). # -h - Display help. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_images() { local image_ids response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_images" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) images." echo " -i image_ids - A space-separated list of image IDs (optional)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) image_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local aws_cli_args=() if [[ -n "$image_ids" ]]; then # shellcheck disable=SC2206 aws_cli_args+=("--image-ids" $image_ids) fi response=$(aws ec2 describe-images \ "${aws_cli_args[@]}" \ --query 'Images[*].[Description,Architecture,ImageId]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-images operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # ec2_describe_instance_types # # This function describes EC2 instance types filtered by processor architecture # and optionally by instance type. It takes the following arguments: # # -a, --architecture ARCHITECTURE Specify the processor architecture (e.g., x86_64) # -t, --type INSTANCE_TYPE Comma-separated list of instance types (e.g., t2.micro) # -h, --help Show the usage help # # The function prints the instance type and supported architecture for each # matching instance type. ############################################################################### function ec2_describe_instance_types() { local architecture="" local instance_types="" # bashsupport disable=BP5008 function usage() { echo "Usage: ec2_describe_instance_types [-a|--architecture ARCHITECTURE] [-t|--type INSTANCE_TYPE] [-h|--help]" echo " -a, --architecture ARCHITECTURE Specify the processor architecture (e.g., x86_64)" echo " -t, --type INSTANCE_TYPE Comma-separated list of instance types (e.g., t2.micro)" echo " -h, --help Show this help message" } while [[ $# -gt 0 ]]; do case "$1" in -a | --architecture) architecture="$2" shift 2 ;; -t | --type) instance_types="$2" shift 2 ;; -h | --help) usage return 0 ;; *) echo "Unknown argument: $1" return 1 ;; esac done if [[ -z "$architecture" ]]; then errecho "Error: Architecture not specified." usage return 1 fi if [[ -z "$instance_types" ]]; then errecho "Error: Instance type not specified." usage return 1 fi local tmp_json_file="temp_ec2.json" echo -n '[ { "Name": "processor-info.supported-architecture", "Values": [' >"$tmp_json_file" local items IFS=',' read -ra items <<<"$architecture" local array_size array_size=${#items[@]} for i in $(seq 0 $((array_size - 1))); do echo -n '"'"${items[$i]}"'"' >>"$tmp_json_file" if [[ $i -lt $((array_size - 1)) ]]; then echo -n ',' >>"$tmp_json_file" fi done echo -n ']}, { "Name": "instance-type", "Values": [' >>"$tmp_json_file" IFS=',' read -ra items <<<"$instance_types" local array_size array_size=${#items[@]} for i in $(seq 0 $((array_size - 1))); do echo -n '"'"${items[$i]}"'"' >>"$tmp_json_file" if [[ $i -lt $((array_size - 1)) ]]; then echo -n ',' >>"$tmp_json_file" fi done echo -n ']}]' >>"$tmp_json_file" local response response=$(aws ec2 describe-instance-types --filters file://"$tmp_json_file" \ --query 'InstanceTypes[*].[InstanceType]' --output text) local error_code=$? rm "$tmp_json_file" if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code echo "ERROR: AWS reports describe-instance-types operation failed." return 1 fi echo "$response" return 0 } ############################################################################### # function ec2_run_instances # # This function launches one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i image_id - The ID of the Amazon Machine Image (AMI) to use. # -t instance_type - The instance type to use (e.g., t2.micro). # -k key_pair_name - The name of the key pair to use. # -s security_group_id - The ID of the security group to use. # -c count - The number of instances to launch (default: 1). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_run_instances() { local image_id instance_type key_pair_name security_group_id count response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_run_instances" echo "Launches one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i image_id - The ID of the Amazon Machine Image (AMI) to use." echo " -t instance_type - The instance type to use (e.g., t2.micro)." echo " -k key_pair_name - The name of the key pair to use." echo " -s security_group_id - The ID of the security group to use." echo " -c count - The number of instances to launch (default: 1)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:t:k:s:c:h" option; do case "${option}" in i) image_id="${OPTARG}" ;; t) instance_type="${OPTARG}" ;; k) key_pair_name="${OPTARG}" ;; s) security_group_id="${OPTARG}" ;; c) count="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$image_id" ]]; then errecho "ERROR: You must provide an Amazon Machine Image (AMI) ID with the -i parameter." usage return 1 fi if [[ -z "$instance_type" ]]; then errecho "ERROR: You must provide an instance type with the -t parameter." usage return 1 fi if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key pair name with the -k parameter." usage return 1 fi if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -s parameter." usage return 1 fi if [[ -z "$count" ]]; then count=1 fi response=$(aws ec2 run-instances \ --image-id "$image_id" \ --instance-type "$instance_type" \ --key-name "$key_pair_name" \ --security-group-ids "$security_group_id" \ --count "$count" \ --query 'Instances[*].[InstanceId]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports run-instances operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_describe_instances # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID of the instance to describe (optional). # -q query - The query to filter the response (optional). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_instances() { local instance_id query response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_instances" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID of the instance to describe (optional)." echo " -q query - The query to filter the response (optional)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:q:h" option; do case "${option}" in i) instance_id="${OPTARG}" ;; q) query="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local aws_cli_args=() if [[ -n "$instance_id" ]]; then # shellcheck disable=SC2206 aws_cli_args+=("--instance-ids" $instance_id) fi local query_arg="" if [[ -n "$query" ]]; then query_arg="--query '$query'" else query_arg="--query Reservations[*].Instances[*].[InstanceId,ImageId,InstanceType,KeyName,VpcId,PublicIpAddress,State.Name]" fi # shellcheck disable=SC2086 response=$(aws ec2 describe-instances \ "${aws_cli_args[@]}" \ $query_arg \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-instances operation failed.$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_stop_instances # # This function stops one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID(s) of the instance(s) to stop (comma-separated). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_stop_instances() { local instance_ids local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_stop_instances" echo "Stops one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID(s) of the instance(s) to stop (comma-separated)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$instance_ids" ]]; then errecho "ERROR: You must provide one or more instance IDs with the -i parameter." usage return 1 fi response=$(aws ec2 stop-instances \ --instance-ids "${instance_ids}") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports stop-instances operation failed with $response." return 1 } return 0 } ############################################################################### # function ec2_start_instances # # This function starts one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID(s) of the instance(s) to start (comma-separated). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_start_instances() { local instance_ids local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_start_instances" echo "Starts one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID(s) of the instance(s) to start (comma-separated)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$instance_ids" ]]; then errecho "ERROR: You must provide one or more instance IDs with the -i parameter." usage return 1 fi response=$(aws ec2 start-instances \ --instance-ids "${instance_ids}") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports start-instances operation failed with $response." return 1 } return 0 } ############################################################################### # function ec2_allocate_address # # This function allocates an Elastic IP address for use with Amazon Elastic Compute Cloud (Amazon EC2) instances in a specific AWS Region. # # Parameters: # -d domain - The domain for the Elastic IP address (either 'vpc' or 'standard'). # # Returns: # The allocated Elastic IP address, or an error message if the operation fails. # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_allocate_address() { local domain response # Function to display usage information function usage() { echo "function ec2_allocate_address" echo "Allocates an Elastic IP address for use with Amazon Elastic Compute Cloud (Amazon EC2) instances in a specific AWS Region." echo " -d domain - The domain for the Elastic IP address (either 'vpc' or 'standard')." echo "" } # Parse the command-line arguments while getopts "d:h" option; do case "${option}" in d) domain="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$domain" ]]; then errecho "ERROR: You must provide a domain with the -d parameter (either 'vpc' or 'standard')." return 1 fi if [[ "$domain" != "vpc" && "$domain" != "standard" ]]; then errecho "ERROR: Invalid domain value. Must be either 'vpc' or 'standard'." return 1 fi # Allocate the Elastic IP address response=$(aws ec2 allocate-address \ --domain "$domain" \ --query "[PublicIp,AllocationId]" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports allocate-address operation failed." errecho "$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_associate_address # # This function associates an Elastic IP address with an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a allocation_id - The allocation ID of the Elastic IP address to associate. # -i instance_id - The ID of the EC2 instance to associate the Elastic IP address with. # # Returns: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_associate_address() { local allocation_id instance_id response # Function to display usage information function usage() { echo "function ec2_associate_address" echo "Associates an Elastic IP address with an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a allocation_id - The allocation ID of the Elastic IP address to associate." echo " -i instance_id - The ID of the EC2 instance to associate the Elastic IP address with." echo "" } # Parse the command-line arguments while getopts "a:i:h" option; do case "${option}" in a) allocation_id="${OPTARG}" ;; i) instance_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$allocation_id" ]]; then errecho "ERROR: You must provide an allocation ID with the -a parameter." return 1 fi if [[ -z "$instance_id" ]]; then errecho "ERROR: You must provide an instance ID with the -i parameter." return 1 fi # Associate the Elastic IP address response=$(aws ec2 associate-address \ --allocation-id "$allocation_id" \ --instance-id "$instance_id" \ --query "AssociationId" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports associate-address operation failed." errecho "$response" return 1 } echo "$response" return 0 } ############################################################################### # function ec2_disassociate_address # # This function disassociates an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a association_id - The association ID that represents the association of the Elastic IP address with an instance. # # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_disassociate_address() { local association_id response # Function to display usage information function usage() { echo "function ec2_disassociate_address" echo "Disassociates an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a association_id - The association ID that represents the association of the Elastic IP address with an instance." echo "" } # Parse the command-line arguments while getopts "a:h" option; do case "${option}" in a) association_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$association_id" ]]; then errecho "ERROR: You must provide an association ID with the -a parameter." return 1 fi response=$(aws ec2 disassociate-address \ --association-id "$association_id") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports disassociate-address operation failed." errecho "$response" return 1 } return 0 } ############################################################################### # function ec2_release_address # # This function releases an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a allocation_id - The allocation ID of the Elastic IP address to release. # # Returns: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_release_address() { local allocation_id response # Function to display usage information function usage() { echo "function ec2_release_address" echo "Releases an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a allocation_id - The allocation ID of the Elastic IP address to release." echo "" } # Parse the command-line arguments while getopts "a:h" option; do case "${option}" in a) allocation_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$allocation_id" ]]; then errecho "ERROR: You must provide an allocation ID with the -a parameter." return 1 fi response=$(aws ec2 release-address \ --allocation-id "$allocation_id") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports release-address operation failed." errecho "$response" return 1 } return 0 } ############################################################################### # function ec2_terminate_instances # # This function terminates one or more Amazon Elastic Compute Cloud (Amazon EC2) # instances using the AWS CLI. # # Parameters: # -i instance_ids - A space-separated list of instance IDs. # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_terminate_instances() { local instance_ids response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_terminate_instances" echo "Terminates one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_ids - A space-separated list of instance IDs." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Check if instance ID is provided if [[ -z "${instance_ids}" ]]; then echo "Error: Missing required instance IDs parameter." usage return 1 fi # shellcheck disable=SC2086 response=$(aws ec2 terminate-instances \ "--instance-ids" $instance_ids \ --query 'TerminatingInstances[*].[InstanceId,CurrentState.Name]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports terminate-instances operation failed.$response" return 1 } return 0 } ############################################################################### # function ec2_delete_security_group # # This function deletes an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -i security_group_id - The ID of the security group to delete. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_delete_security_group() { local security_group_id response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_delete_security_group" echo "Deletes an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -i security_group_id - The ID of the security group to delete." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) security_group_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -i parameter." usage return 1 fi response=$(aws ec2 delete-security-group --group-id "$security_group_id" --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports delete-security-group operation failed.$response" return 1 } return 0 } ############################################################################### # function ec2_delete_keypair # # This function deletes an Amazon EC2 ED25519 or 2048-bit RSA key pair. # # Parameters: # -n key_pair_name - A key pair name. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_delete_keypair() { local key_pair_name response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_delete_keypair" echo "Deletes an Amazon EC2 ED25519 or 2048-bit RSA key pair." echo " -n key_pair_name - A key pair name." echo "" } # Retrieve the calling parameters. while getopts "n:h" option; do case "${option}" in n) key_pair_name="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key pair name with the -n parameter." usage return 1 fi response=$(aws ec2 delete-key-pair \ --key-name "$key_pair_name") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports delete-key-pair operation failed.$response" return 1 } return 0 }

The utility functions used in this scenario.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

Actions

The following code example shows how to use AllocateAddress.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_allocate_address # # This function allocates an Elastic IP address for use with Amazon Elastic Compute Cloud (Amazon EC2) instances in a specific AWS Region. # # Parameters: # -d domain - The domain for the Elastic IP address (either 'vpc' or 'standard'). # # Returns: # The allocated Elastic IP address, or an error message if the operation fails. # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_allocate_address() { local domain response # Function to display usage information function usage() { echo "function ec2_allocate_address" echo "Allocates an Elastic IP address for use with Amazon Elastic Compute Cloud (Amazon EC2) instances in a specific AWS Region." echo " -d domain - The domain for the Elastic IP address (either 'vpc' or 'standard')." echo "" } # Parse the command-line arguments while getopts "d:h" option; do case "${option}" in d) domain="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$domain" ]]; then errecho "ERROR: You must provide a domain with the -d parameter (either 'vpc' or 'standard')." return 1 fi if [[ "$domain" != "vpc" && "$domain" != "standard" ]]; then errecho "ERROR: Invalid domain value. Must be either 'vpc' or 'standard'." return 1 fi # Allocate the Elastic IP address response=$(aws ec2 allocate-address \ --domain "$domain" \ --query "[PublicIp,AllocationId]" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports allocate-address operation failed." errecho "$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use AssociateAddress.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_associate_address # # This function associates an Elastic IP address with an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a allocation_id - The allocation ID of the Elastic IP address to associate. # -i instance_id - The ID of the EC2 instance to associate the Elastic IP address with. # # Returns: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_associate_address() { local allocation_id instance_id response # Function to display usage information function usage() { echo "function ec2_associate_address" echo "Associates an Elastic IP address with an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a allocation_id - The allocation ID of the Elastic IP address to associate." echo " -i instance_id - The ID of the EC2 instance to associate the Elastic IP address with." echo "" } # Parse the command-line arguments while getopts "a:i:h" option; do case "${option}" in a) allocation_id="${OPTARG}" ;; i) instance_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$allocation_id" ]]; then errecho "ERROR: You must provide an allocation ID with the -a parameter." return 1 fi if [[ -z "$instance_id" ]]; then errecho "ERROR: You must provide an instance ID with the -i parameter." return 1 fi # Associate the Elastic IP address response=$(aws ec2 associate-address \ --allocation-id "$allocation_id" \ --instance-id "$instance_id" \ --query "AssociationId" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports associate-address operation failed." errecho "$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use AuthorizeSecurityGroupIngress.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_authorize_security_group_ingress # # This function authorizes an ingress rule for an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -g security_group_id - The ID of the security group. # -i ip_address - The IP address or CIDR block to authorize. # -p protocol - The protocol to authorize (e.g., tcp, udp, icmp). # -f from_port - The start of the port range to authorize. # -t to_port - The end of the port range to authorize. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_authorize_security_group_ingress() { local security_group_id ip_address protocol from_port to_port response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_authorize_security_group_ingress" echo "Authorizes an ingress rule for an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -g security_group_id - The ID of the security group." echo " -i ip_address - The IP address or CIDR block to authorize." echo " -p protocol - The protocol to authorize (e.g., tcp, udp, icmp)." echo " -f from_port - The start of the port range to authorize." echo " -t to_port - The end of the port range to authorize." echo "" } # Retrieve the calling parameters. while getopts "g:i:p:f:t:h" option; do case "${option}" in g) security_group_id="${OPTARG}" ;; i) ip_address="${OPTARG}" ;; p) protocol="${OPTARG}" ;; f) from_port="${OPTARG}" ;; t) to_port="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -g parameter." usage return 1 fi if [[ -z "$ip_address" ]]; then errecho "ERROR: You must provide an IP address or CIDR block with the -i parameter." usage return 1 fi if [[ -z "$protocol" ]]; then errecho "ERROR: You must provide a protocol with the -p parameter." usage return 1 fi if [[ -z "$from_port" ]]; then errecho "ERROR: You must provide a start port with the -f parameter." usage return 1 fi if [[ -z "$to_port" ]]; then errecho "ERROR: You must provide an end port with the -t parameter." usage return 1 fi response=$(aws ec2 authorize-security-group-ingress \ --group-id "$security_group_id" \ --cidr "${ip_address}/32" \ --protocol "$protocol" \ --port "$from_port-$to_port" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports authorize-security-group-ingress operation failed.$response" return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use CreateKeyPair.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_create_keypair # # This function creates an Amazon Elastic Compute Cloud (Amazon EC2) ED25519 or 2048-bit RSA key pair # and writes it to a file. # # Parameters: # -n key_pair_name - A key pair name. # -f file_path - File to store the key pair. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_create_keypair() { local key_pair_name file_path response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_create_keypair" echo "Creates an Amazon Elastic Compute Cloud (Amazon EC2) ED25519 or 2048-bit RSA key pair" echo " and writes it to a file." echo " -n key_pair_name - A key pair name." echo " -f file_path - File to store the key pair." echo "" } # Retrieve the calling parameters. while getopts "n:f:h" option; do case "${option}" in n) key_pair_name="${OPTARG}" ;; f) file_path="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key name with the -n parameter." usage return 1 fi if [[ -z "$file_path" ]]; then errecho "ERROR: You must provide a file path with the -f parameter." usage return 1 fi response=$(aws ec2 create-key-pair \ --key-name "$key_pair_name" \ --query 'KeyMaterial' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports create-access-key operation failed.$response" return 1 } if [[ -n "$file_path" ]]; then echo "$response" >"$file_path" fi return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }
  • For API details, see CreateKeyPair in AWS CLI Command Reference.

The following code example shows how to use CreateSecurityGroup.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_create_security_group # # This function creates an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -n security_group_name - The name of the security group. # -d security_group_description - The description of the security group. # # Returns: # The ID of the created security group, or an error message if the operation fails. # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_create_security_group() { local security_group_name security_group_description response # Function to display usage information function usage() { echo "function ec2_create_security_group" echo "Creates an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -n security_group_name - The name of the security group." echo " -d security_group_description - The description of the security group." echo "" } # Parse the command-line arguments while getopts "n:d:h" option; do case "${option}" in n) security_group_name="${OPTARG}" ;; d) security_group_description="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$security_group_name" ]]; then errecho "ERROR: You must provide a security group name with the -n parameter." return 1 fi if [[ -z "$security_group_description" ]]; then errecho "ERROR: You must provide a security group description with the -d parameter." return 1 fi # Create the security group response=$(aws ec2 create-security-group \ --group-name "$security_group_name" \ --description "$security_group_description" \ --query "GroupId" \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports create-security-group operation failed." errecho "$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DeleteKeyPair.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_delete_keypair # # This function deletes an Amazon EC2 ED25519 or 2048-bit RSA key pair. # # Parameters: # -n key_pair_name - A key pair name. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_delete_keypair() { local key_pair_name response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_delete_keypair" echo "Deletes an Amazon EC2 ED25519 or 2048-bit RSA key pair." echo " -n key_pair_name - A key pair name." echo "" } # Retrieve the calling parameters. while getopts "n:h" option; do case "${option}" in n) key_pair_name="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key pair name with the -n parameter." usage return 1 fi response=$(aws ec2 delete-key-pair \ --key-name "$key_pair_name") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports delete-key-pair operation failed.$response" return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }
  • For API details, see DeleteKeyPair in AWS CLI Command Reference.

The following code example shows how to use DeleteSecurityGroup.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_delete_security_group # # This function deletes an Amazon Elastic Compute Cloud (Amazon EC2) security group. # # Parameters: # -i security_group_id - The ID of the security group to delete. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_delete_security_group() { local security_group_id response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_delete_security_group" echo "Deletes an Amazon Elastic Compute Cloud (Amazon EC2) security group." echo " -i security_group_id - The ID of the security group to delete." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) security_group_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -i parameter." usage return 1 fi response=$(aws ec2 delete-security-group --group-id "$security_group_id" --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports delete-security-group operation failed.$response" return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DescribeImages.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_describe_images # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) images. # # Parameters: # -i image_ids - A space-separated list of image IDs (optional). # -h - Display help. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_images() { local image_ids response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_images" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) images." echo " -i image_ids - A space-separated list of image IDs (optional)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) image_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local aws_cli_args=() if [[ -n "$image_ids" ]]; then # shellcheck disable=SC2206 aws_cli_args+=("--image-ids" $image_ids) fi response=$(aws ec2 describe-images \ "${aws_cli_args[@]}" \ --query 'Images[*].[Description,Architecture,ImageId]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-images operation failed.$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DescribeInstanceTypes.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # ec2_describe_instance_types # # This function describes EC2 instance types filtered by processor architecture # and optionally by instance type. It takes the following arguments: # # -a, --architecture ARCHITECTURE Specify the processor architecture (e.g., x86_64) # -t, --type INSTANCE_TYPE Comma-separated list of instance types (e.g., t2.micro) # -h, --help Show the usage help # # The function prints the instance type and supported architecture for each # matching instance type. ############################################################################### function ec2_describe_instance_types() { local architecture="" local instance_types="" # bashsupport disable=BP5008 function usage() { echo "Usage: ec2_describe_instance_types [-a|--architecture ARCHITECTURE] [-t|--type INSTANCE_TYPE] [-h|--help]" echo " -a, --architecture ARCHITECTURE Specify the processor architecture (e.g., x86_64)" echo " -t, --type INSTANCE_TYPE Comma-separated list of instance types (e.g., t2.micro)" echo " -h, --help Show this help message" } while [[ $# -gt 0 ]]; do case "$1" in -a | --architecture) architecture="$2" shift 2 ;; -t | --type) instance_types="$2" shift 2 ;; -h | --help) usage return 0 ;; *) echo "Unknown argument: $1" return 1 ;; esac done if [[ -z "$architecture" ]]; then errecho "Error: Architecture not specified." usage return 1 fi if [[ -z "$instance_types" ]]; then errecho "Error: Instance type not specified." usage return 1 fi local tmp_json_file="temp_ec2.json" echo -n '[ { "Name": "processor-info.supported-architecture", "Values": [' >"$tmp_json_file" local items IFS=',' read -ra items <<<"$architecture" local array_size array_size=${#items[@]} for i in $(seq 0 $((array_size - 1))); do echo -n '"'"${items[$i]}"'"' >>"$tmp_json_file" if [[ $i -lt $((array_size - 1)) ]]; then echo -n ',' >>"$tmp_json_file" fi done echo -n ']}, { "Name": "instance-type", "Values": [' >>"$tmp_json_file" IFS=',' read -ra items <<<"$instance_types" local array_size array_size=${#items[@]} for i in $(seq 0 $((array_size - 1))); do echo -n '"'"${items[$i]}"'"' >>"$tmp_json_file" if [[ $i -lt $((array_size - 1)) ]]; then echo -n ',' >>"$tmp_json_file" fi done echo -n ']}]' >>"$tmp_json_file" local response response=$(aws ec2 describe-instance-types --filters file://"$tmp_json_file" \ --query 'InstanceTypes[*].[InstanceType]' --output text) local error_code=$? rm "$tmp_json_file" if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code echo "ERROR: AWS reports describe-instance-types operation failed." return 1 fi echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DescribeInstances.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_describe_instances # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID of the instance to describe (optional). # -q query - The query to filter the response (optional). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_instances() { local instance_id query response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_instances" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID of the instance to describe (optional)." echo " -q query - The query to filter the response (optional)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:q:h" option; do case "${option}" in i) instance_id="${OPTARG}" ;; q) query="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local aws_cli_args=() if [[ -n "$instance_id" ]]; then # shellcheck disable=SC2206 aws_cli_args+=("--instance-ids" $instance_id) fi local query_arg="" if [[ -n "$query" ]]; then query_arg="--query '$query'" else query_arg="--query Reservations[*].Instances[*].[InstanceId,ImageId,InstanceType,KeyName,VpcId,PublicIpAddress,State.Name]" fi # shellcheck disable=SC2086 response=$(aws ec2 describe-instances \ "${aws_cli_args[@]}" \ $query_arg \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-instances operation failed.$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DescribeKeyPairs.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_describe_key_pairs # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) key pairs. # # Parameters: # -h - Display help. # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_key_pairs() { local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_key_pairs" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) key pairs." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "h" option; do case "${option}" in h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local response response=$(aws ec2 describe-key-pairs \ --query 'KeyPairs[*].[KeyName, KeyFingerprint]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports describe-key-pairs operation failed.$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DescribeSecurityGroups.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_describe_security_groups # # This function describes one or more Amazon Elastic Compute Cloud (Amazon EC2) security groups. # # Parameters: # -g security_group_id - The ID of the security group to describe (optional). # # And: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_describe_security_groups() { local security_group_id response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_describe_security_groups" echo "Describes one or more Amazon Elastic Compute Cloud (Amazon EC2) security groups." echo " -g security_group_id - The ID of the security group to describe (optional)." echo "" } # Retrieve the calling parameters. while getopts "g:h" option; do case "${option}" in g) security_group_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 local query="SecurityGroups[*].[GroupName, GroupId, VpcId, IpPermissions[*].[IpProtocol, FromPort, ToPort, IpRanges[*].CidrIp]]" if [[ -n "$security_group_id" ]]; then response=$(aws ec2 describe-security-groups --group-ids "$security_group_id" --query "${query}" --output text) else response=$(aws ec2 describe-security-groups --query "${query}" --output text) fi local error_code=${?} if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code errecho "ERROR: AWS reports describe-security-groups operation failed.$response" return 1 fi echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use DisassociateAddress.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_disassociate_address # # This function disassociates an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a association_id - The association ID that represents the association of the Elastic IP address with an instance. # # And: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_disassociate_address() { local association_id response # Function to display usage information function usage() { echo "function ec2_disassociate_address" echo "Disassociates an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a association_id - The association ID that represents the association of the Elastic IP address with an instance." echo "" } # Parse the command-line arguments while getopts "a:h" option; do case "${option}" in a) association_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$association_id" ]]; then errecho "ERROR: You must provide an association ID with the -a parameter." return 1 fi response=$(aws ec2 disassociate-address \ --association-id "$association_id") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports disassociate-address operation failed." errecho "$response" return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use ReleaseAddress.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_release_address # # This function releases an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance. # # Parameters: # -a allocation_id - The allocation ID of the Elastic IP address to release. # # Returns: # 0 - If successful. # 1 - If it fails. # ############################################################################### function ec2_release_address() { local allocation_id response # Function to display usage information function usage() { echo "function ec2_release_address" echo "Releases an Elastic IP address from an Amazon Elastic Compute Cloud (Amazon EC2) instance." echo " -a allocation_id - The allocation ID of the Elastic IP address to release." echo "" } # Parse the command-line arguments while getopts "a:h" option; do case "${option}" in a) allocation_id="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Validate the input parameters if [[ -z "$allocation_id" ]]; then errecho "ERROR: You must provide an allocation ID with the -a parameter." return 1 fi response=$(aws ec2 release-address \ --allocation-id "$allocation_id") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports release-address operation failed." errecho "$response" return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use RunInstances.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_run_instances # # This function launches one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i image_id - The ID of the Amazon Machine Image (AMI) to use. # -t instance_type - The instance type to use (e.g., t2.micro). # -k key_pair_name - The name of the key pair to use. # -s security_group_id - The ID of the security group to use. # -c count - The number of instances to launch (default: 1). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_run_instances() { local image_id instance_type key_pair_name security_group_id count response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_run_instances" echo "Launches one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i image_id - The ID of the Amazon Machine Image (AMI) to use." echo " -t instance_type - The instance type to use (e.g., t2.micro)." echo " -k key_pair_name - The name of the key pair to use." echo " -s security_group_id - The ID of the security group to use." echo " -c count - The number of instances to launch (default: 1)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:t:k:s:c:h" option; do case "${option}" in i) image_id="${OPTARG}" ;; t) instance_type="${OPTARG}" ;; k) key_pair_name="${OPTARG}" ;; s) security_group_id="${OPTARG}" ;; c) count="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$image_id" ]]; then errecho "ERROR: You must provide an Amazon Machine Image (AMI) ID with the -i parameter." usage return 1 fi if [[ -z "$instance_type" ]]; then errecho "ERROR: You must provide an instance type with the -t parameter." usage return 1 fi if [[ -z "$key_pair_name" ]]; then errecho "ERROR: You must provide a key pair name with the -k parameter." usage return 1 fi if [[ -z "$security_group_id" ]]; then errecho "ERROR: You must provide a security group ID with the -s parameter." usage return 1 fi if [[ -z "$count" ]]; then count=1 fi response=$(aws ec2 run-instances \ --image-id "$image_id" \ --instance-type "$instance_type" \ --key-name "$key_pair_name" \ --security-group-ids "$security_group_id" \ --count "$count" \ --query 'Instances[*].[InstanceId]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports run-instances operation failed.$response" return 1 } echo "$response" return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }
  • For API details, see RunInstances in AWS CLI Command Reference.

The following code example shows how to use StartInstances.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_start_instances # # This function starts one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID(s) of the instance(s) to start (comma-separated). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_start_instances() { local instance_ids local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_start_instances" echo "Starts one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID(s) of the instance(s) to start (comma-separated)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$instance_ids" ]]; then errecho "ERROR: You must provide one or more instance IDs with the -i parameter." usage return 1 fi response=$(aws ec2 start-instances \ --instance-ids "${instance_ids}") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports start-instances operation failed with $response." return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

The following code example shows how to use StopInstances.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_stop_instances # # This function stops one or more Amazon Elastic Compute Cloud (Amazon EC2) instances. # # Parameters: # -i instance_id - The ID(s) of the instance(s) to stop (comma-separated). # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_stop_instances() { local instance_ids local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_stop_instances" echo "Stops one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_id - The ID(s) of the instance(s) to stop (comma-separated)." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 if [[ -z "$instance_ids" ]]; then errecho "ERROR: You must provide one or more instance IDs with the -i parameter." usage return 1 fi response=$(aws ec2 stop-instances \ --instance-ids "${instance_ids}") || { aws_cli_error_log ${?} errecho "ERROR: AWS reports stop-instances operation failed with $response." return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }
  • For API details, see StopInstances in AWS CLI Command Reference.

The following code example shows how to use TerminateInstances.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the AWS Code Examples Repository.

############################################################################### # function ec2_terminate_instances # # This function terminates one or more Amazon Elastic Compute Cloud (Amazon EC2) # instances using the AWS CLI. # # Parameters: # -i instance_ids - A space-separated list of instance IDs. # -h - Display help. # # Returns: # 0 - If successful. # 1 - If it fails. ############################################################################### function ec2_terminate_instances() { local instance_ids response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function ec2_terminate_instances" echo "Terminates one or more Amazon Elastic Compute Cloud (Amazon EC2) instances." echo " -i instance_ids - A space-separated list of instance IDs." echo " -h - Display help." echo "" } # Retrieve the calling parameters. while getopts "i:h" option; do case "${option}" in i) instance_ids="${OPTARG}" ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done export OPTIND=1 # Check if instance ID is provided if [[ -z "${instance_ids}" ]]; then echo "Error: Missing required instance IDs parameter." usage return 1 fi # shellcheck disable=SC2086 response=$(aws ec2 terminate-instances \ "--instance-ids" $instance_ids \ --query 'TerminatingInstances[*].[InstanceId,CurrentState.Name]' \ --output text) || { aws_cli_error_log ${?} errecho "ERROR: AWS reports terminate-instances operation failed.$response" return 1 } return 0 }

The utility functions used in this example.

############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################## # function aws_cli_error_log() # # This function is used to log the error messages from the AWS CLI. # # The function expects the following argument: # $1 - The error code returned by the AWS CLI. # # Returns: # 0: - Success. # ############################################################################## function aws_cli_error_log() { local err_code=$1 errecho "Error code : $err_code" if [ "$err_code" == 1 ]; then errecho " One or more S3 transfers failed." elif [ "$err_code" == 2 ]; then errecho " Command line failed to parse." elif [ "$err_code" == 130 ]; then errecho " Process received SIGINT." elif [ "$err_code" == 252 ]; then errecho " Command syntax invalid." elif [ "$err_code" == 253 ]; then errecho " The system environment or configuration was invalid." elif [ "$err_code" == 254 ]; then errecho " The service returned an error." elif [ "$err_code" == 255 ]; then errecho " 255 is a catch-all error." fi return 0 }

Scenarios

The following code example shows how to:

  • Create the VPC infrastructure

  • Set up logging

  • Create the ECS cluster

  • Configure IAM roles

  • Create the service with Service Connect

  • Verify the deployment

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # ECS Service Connect Tutorial Script v4 - Modified to use Default VPC # This script creates an ECS cluster with Service Connect and deploys an nginx service # Uses the default VPC to avoid VPC limits set -e # Exit on any error # Configuration SCRIPT_NAME="ECS Service Connect Tutorial" LOG_FILE="ecs-service-connect-tutorial-v4-default-vpc.log" REGION=${AWS_DEFAULT_REGION:-${AWS_REGION:-$(aws configure get region 2>/dev/null)}} if [ -z "$REGION" ]; then echo "ERROR: No AWS region configured." echo "Set one with: aws configure set region us-east-1" exit 1 fi ENV_PREFIX="tutorial" CLUSTER_NAME="${ENV_PREFIX}-cluster" NAMESPACE_NAME="service-connect" # Generate random suffix for unique resource names RANDOM_SUFFIX=$(openssl rand -hex 6) # Arrays to track created resources for cleanup declare -a CREATED_RESOURCES=() # Logging function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } # Error handling function handle_error() { log "ERROR: Script failed at line $1" log "Attempting to clean up resources..." cleanup_resources exit 1 } # Set up error handling trap 'handle_error $LINENO' ERR # Function to add resource to tracking array track_resource() { CREATED_RESOURCES+=("$1") log "Tracking resource: $1" } # Function to check if command output contains actual errors check_for_errors() { local output="$1" local command_name="$2" # Check for specific AWS CLI error patterns, not just any occurrence of "error" if echo "$output" | grep -qi "An error occurred\|InvalidParameterException\|AccessDenied\|ValidationException\|ResourceNotFoundException"; then log "ERROR in $command_name: $output" return 1 fi return 0 } # Function to get AWS account ID get_account_id() { ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) log "Using AWS Account ID: $ACCOUNT_ID" } # Function to wait for resources to be ready wait_for_resource() { local resource_type="$1" local resource_id="$2" case "$resource_type" in "cluster") log "Waiting for cluster $resource_id to be active..." local attempt=1 local max_attempts=30 while [ $attempt -le $max_attempts ]; do local status=$(aws ecs describe-clusters --clusters "$resource_id" --query 'clusters[0].status' --output text) if [ "$status" = "ACTIVE" ]; then log "Cluster is now active" return 0 fi log "Cluster status: $status (attempt $attempt/$max_attempts)" sleep 10 ((attempt++)) done log "ERROR: Cluster did not become active within expected time" return 1 ;; "service") log "Waiting for service $resource_id to be stable..." aws ecs wait services-stable --cluster "$CLUSTER_NAME" --services "$resource_id" ;; "nat-gateway") log "Waiting for NAT Gateway $resource_id to be available..." aws ec2 wait nat-gateway-available --nat-gateway-ids "$resource_id" ;; esac } # Function to use default VPC infrastructure setup_default_vpc_infrastructure() { log "Using default VPC infrastructure..." # Get default VPC VPC_ID=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query 'Vpcs[0].VpcId' --output text) if [[ "$VPC_ID" == "None" || -z "$VPC_ID" ]]; then log "ERROR: No default VPC found. Please create a default VPC first." exit 1 fi log "Using default VPC: $VPC_ID" # Get default subnets SUBNETS=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" "Name=default-for-az,Values=true" --query 'Subnets[].SubnetId' --output text) SUBNET_ARRAY=($SUBNETS) if [ ${#SUBNET_ARRAY[@]} -lt 2 ]; then log "ERROR: Need at least 2 subnets for ECS Service Connect. Found: ${#SUBNET_ARRAY[@]}" exit 1 fi PUBLIC_SUBNET1=${SUBNET_ARRAY[0]} PUBLIC_SUBNET2=${SUBNET_ARRAY[1]} log "Using subnets: $PUBLIC_SUBNET1, $PUBLIC_SUBNET2" # Create security group for ECS tasks SG_OUTPUT=$(aws ec2 create-security-group \ --group-name "${ENV_PREFIX}-ecs-sg-${RANDOM_SUFFIX}" \ --description "Security group for ECS Service Connect tutorial" \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-ecs-service-connect}]' 2>&1) check_for_errors "$SG_OUTPUT" "create-security-group" SECURITY_GROUP_ID=$(echo "$SG_OUTPUT" | grep -o '"GroupId": "[^"]*"' | cut -d'"' -f4) track_resource "SG:$SECURITY_GROUP_ID" log "Created security group: $SECURITY_GROUP_ID" # Add inbound rules to security group aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 80 \ --cidr 0.0.0.0/0 >/dev/null 2>&1 || true aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 443 \ --cidr 0.0.0.0/0 >/dev/null 2>&1 || true log "Default VPC infrastructure setup completed" } # Function to create CloudWatch log groups create_log_groups() { log "Creating CloudWatch log groups..." # Create log group for nginx container aws logs create-log-group --log-group-name "/ecs/service-connect-nginx" --tags project=doc-smith,tutorial=amazon-ecs-service-connect 2>&1 | grep -v "ResourceAlreadyExistsException" || { if [ ${PIPESTATUS[0]} -eq 0 ]; then log "Log group /ecs/service-connect-nginx created" track_resource "LOG_GROUP:/ecs/service-connect-nginx" else log "Log group /ecs/service-connect-nginx already exists" fi } # Create log group for service connect proxy aws logs create-log-group --log-group-name "/ecs/service-connect-proxy" --tags project=doc-smith,tutorial=amazon-ecs-service-connect 2>&1 | grep -v "ResourceAlreadyExistsException" || { if [ ${PIPESTATUS[0]} -eq 0 ]; then log "Log group /ecs/service-connect-proxy created" track_resource "LOG_GROUP:/ecs/service-connect-proxy" else log "Log group /ecs/service-connect-proxy already exists" fi } } # Function to create ECS cluster with Service Connect create_ecs_cluster() { log "Creating ECS cluster with Service Connect..." CLUSTER_OUTPUT=$(aws ecs create-cluster \ --cluster-name "$CLUSTER_NAME" \ --service-connect-defaults namespace="$NAMESPACE_NAME" \ --tags key=Environment,value=tutorial key=project,value=doc-smith key=tutorial,value=amazon-ecs-service-connect 2>&1) check_for_errors "$CLUSTER_OUTPUT" "create-cluster" track_resource "CLUSTER:$CLUSTER_NAME" log "Created ECS cluster: $CLUSTER_NAME" wait_for_resource "cluster" "$CLUSTER_NAME" # Track the Service Connect namespace that gets created # Wait a moment for the namespace to be created sleep 5 NAMESPACE_ID=$(aws servicediscovery list-namespaces \ --filters Name=TYPE,Values=HTTP \ --query "Namespaces[?Name=='$NAMESPACE_NAME'].Id" --output text 2>/dev/null || echo "") if [[ -n "$NAMESPACE_ID" && "$NAMESPACE_ID" != "None" ]]; then track_resource "NAMESPACE:$NAMESPACE_ID" log "Service Connect namespace created: $NAMESPACE_ID" fi } # Function to create IAM roles create_iam_roles() { log "Creating IAM roles..." # Check if ecsTaskExecutionRole exists if aws iam get-role --role-name ecsTaskExecutionRole >/dev/null 2>&1; then log "IAM role ecsTaskExecutionRole exists" else log "Creating ecsTaskExecutionRole..." aws iam create-role \ --role-name ecsTaskExecutionRole \ --assume-role-policy-document '{ "Version":"2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": {"Service": "ecs-tasks.amazonaws.com"}, "Action": "sts:AssumeRole" }] }' >/dev/null 2>&1 aws iam attach-role-policy \ --role-name ecsTaskExecutionRole \ --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy >/dev/null 2>&1 track_resource "ROLE:ecsTaskExecutionRole" aws iam tag-role --role-name ecsTaskExecutionRole --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-ecs-service-connect log "Created ecsTaskExecutionRole" sleep 10 fi # Check if ecsTaskRole exists, create if not if aws iam get-role --role-name ecsTaskRole >/dev/null 2>&1; then log "IAM role ecsTaskRole exists" else log "IAM role ecsTaskRole does not exist, will create it" # Create trust policy for ECS tasks cat > /tmp/ecs-task-trust-policy.json << EOF { "Version":"2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF aws iam create-role \ --role-name ecsTaskRole \ --assume-role-policy-document file:///tmp/ecs-task-trust-policy.json >/dev/null track_resource "IAM_ROLE:ecsTaskRole" aws iam tag-role --role-name ecsTaskRole --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-ecs-service-connect log "Created ecsTaskRole" # Wait for role to be available sleep 10 fi } # Function to create task definition create_task_definition() { log "Creating task definition..." # Create task definition JSON cat > /tmp/task-definition.json << EOF { "family": "service-connect-nginx", "networkMode": "awsvpc", "requiresCompatibilities": ["FARGATE"], "cpu": "256", "memory": "512", "executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole", "taskRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskRole", "containerDefinitions": [ { "name": "nginx", "image": "public.ecr.aws/docker/library/nginx:latest", "portMappings": [ { "containerPort": 80, "protocol": "tcp", "name": "nginx-port" } ], "essential": true, "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/service-connect-nginx", "awslogs-region": "${REGION}", "awslogs-stream-prefix": "ecs" } } } ] } EOF TASK_DEF_OUTPUT=$(aws ecs register-task-definition --cli-input-json file:///tmp/task-definition.json 2>&1) check_for_errors "$TASK_DEF_OUTPUT" "register-task-definition" TASK_DEF_ARN=$(echo "$TASK_DEF_OUTPUT" | grep -o '"taskDefinitionArn": "[^"]*"' | cut -d'"' -f4) track_resource "TASK_DEF:service-connect-nginx" log "Created task definition: $TASK_DEF_ARN" # Clean up temporary file rm -f /tmp/task-definition.json } # Function to create ECS service with Service Connect create_ecs_service() { log "Creating ECS service with Service Connect..." # Create service definition JSON cat > /tmp/service-definition.json << EOF { "serviceName": "service-connect-nginx-service", "cluster": "${CLUSTER_NAME}", "taskDefinition": "service-connect-nginx", "desiredCount": 1, "launchType": "FARGATE", "networkConfiguration": { "awsvpcConfiguration": { "subnets": ["${PUBLIC_SUBNET1}", "${PUBLIC_SUBNET2}"], "securityGroups": ["${SECURITY_GROUP_ID}"], "assignPublicIp": "ENABLED" } }, "serviceConnectConfiguration": { "enabled": true, "namespace": "${NAMESPACE_NAME}", "services": [ { "portName": "nginx-port", "discoveryName": "nginx", "clientAliases": [ { "port": 80, "dnsName": "nginx" } ] } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/service-connect-proxy", "awslogs-region": "${REGION}", "awslogs-stream-prefix": "ecs-service-connect" } } }, "tags": [ { "key": "Environment", "value": "tutorial" }, { "key": "project", "value": "doc-smith" }, { "key": "tutorial", "value": "amazon-ecs-service-connect" } ] } EOF SERVICE_OUTPUT=$(aws ecs create-service --cli-input-json file:///tmp/service-definition.json 2>&1) check_for_errors "$SERVICE_OUTPUT" "create-service" track_resource "SERVICE:service-connect-nginx-service" log "Created ECS service: service-connect-nginx-service" wait_for_resource "service" "service-connect-nginx-service" # Clean up temporary file rm -f /tmp/service-definition.json } # Function to verify deployment verify_deployment() { log "Verifying deployment..." # Check service status SERVICE_STATUS=$(aws ecs describe-services \ --cluster "$CLUSTER_NAME" \ --services "service-connect-nginx-service" \ --query 'services[0].status' --output text) log "Service status: $SERVICE_STATUS" # Check running tasks RUNNING_COUNT=$(aws ecs describe-services \ --cluster "$CLUSTER_NAME" \ --services "service-connect-nginx-service" \ --query 'services[0].runningCount' --output text) log "Running tasks: $RUNNING_COUNT" # Get task ARN TASK_ARN=$(aws ecs list-tasks \ --cluster "$CLUSTER_NAME" \ --service-name "service-connect-nginx-service" \ --query 'taskArns[0]' --output text) if [[ "$TASK_ARN" != "None" && -n "$TASK_ARN" ]]; then log "Task ARN: $TASK_ARN" # Try to get task IP address TASK_IP=$(aws ecs describe-tasks \ --cluster "$CLUSTER_NAME" \ --tasks "$TASK_ARN" \ --query 'tasks[0].attachments[0].details[?name==`privateIPv4Address`].value' \ --output text 2>/dev/null || echo "") if [[ -n "$TASK_IP" && "$TASK_IP" != "None" ]]; then log "Task IP address: $TASK_IP" else log "Could not retrieve task IP address" fi fi # Check Service Connect namespace NAMESPACE_STATUS=$(aws servicediscovery list-namespaces \ --filters Name=TYPE,Values=HTTP \ --query "Namespaces[?Name=='$NAMESPACE_NAME'].Id" --output text 2>/dev/null || echo "") if [[ -n "$NAMESPACE_STATUS" && "$NAMESPACE_STATUS" != "None" ]]; then log "Service Connect namespace '$NAMESPACE_NAME' is active" else log "Service Connect namespace '$NAMESPACE_NAME' not found or not active" fi # Display Service Connect configuration log "Service Connect configuration:" aws ecs describe-services \ --cluster "$CLUSTER_NAME" \ --services "service-connect-nginx-service" \ --query 'services[0].serviceConnectConfiguration' 2>/dev/null || true } # Function to display created resources display_resources() { echo "" echo "===========================================" echo "CREATED RESOURCES" echo "===========================================" for resource in "${CREATED_RESOURCES[@]}"; do echo "- $resource" done echo "===========================================" echo "" } # Function to cleanup resources cleanup_resources() { log "Starting cleanup process..." # Delete resources in reverse order of creation for ((i=${#CREATED_RESOURCES[@]}-1; i>=0; i--)); do resource="${CREATED_RESOURCES[i]}" resource_type=$(echo "$resource" | cut -d':' -f1) resource_id=$(echo "$resource" | cut -d':' -f2) log "Cleaning up $resource_type: $resource_id" case "$resource_type" in "SERVICE") aws ecs update-service --cluster "$CLUSTER_NAME" --service "$resource_id" --desired-count 0 2>&1 | grep -qi "error" && log "Warning: Failed to scale down service $resource_id" aws ecs wait services-stable --cluster "$CLUSTER_NAME" --services "$resource_id" 2>/dev/null || true aws ecs delete-service --cluster "$CLUSTER_NAME" --service "$resource_id" --force 2>&1 | grep -qi "error" && log "Warning: Failed to delete service $resource_id" ;; "TASK_DEF") TASK_DEF_ARNS=$(aws ecs list-task-definitions --family-prefix "$resource_id" --query 'taskDefinitionArns' --output text 2>/dev/null) for arn in $TASK_DEF_ARNS; do aws ecs deregister-task-definition --task-definition "$arn" >/dev/null 2>&1 || true done ;; "ROLE") aws iam detach-role-policy --role-name "$resource_id" --policy-arn "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 2>/dev/null || true aws iam delete-role --role-name "$resource_id" 2>&1 | grep -qi "error" && log "Warning: Failed to delete role $resource_id" ;; "IAM_ROLE") aws iam detach-role-policy --role-name "$resource_id" --policy-arn "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 2>/dev/null || true aws iam delete-role --role-name "$resource_id" 2>&1 | grep -qi "error" && log "Warning: Failed to delete role $resource_id" ;; "CLUSTER") aws ecs delete-cluster --cluster "$resource_id" 2>&1 | grep -qi "error" && log "Warning: Failed to delete cluster $resource_id" ;; "SG") for attempt in 1 2 3 4 5; do if aws ec2 delete-security-group --group-id "$resource_id" 2>/dev/null; then break fi log "Security group $resource_id still has dependencies, retrying in 30s ($attempt/5)..." sleep 30 done ;; "LOG_GROUP") aws logs delete-log-group --log-group-name "$resource_id" 2>&1 | grep -qi "error" && log "Warning: Failed to delete log group $resource_id" ;; "NAMESPACE") # First, delete any services in the namespace NAMESPACE_SERVICES=$(aws servicediscovery list-services \ --filters Name=NAMESPACE_ID,Values="$resource_id" \ --query 'Services[].Id' --output text 2>/dev/null || echo "") if [[ -n "$NAMESPACE_SERVICES" && "$NAMESPACE_SERVICES" != "None" ]]; then for service_id in $NAMESPACE_SERVICES; do aws servicediscovery delete-service --id "$service_id" >/dev/null 2>&1 || true sleep 2 done fi # Then delete the namespace aws servicediscovery delete-namespace --id "$resource_id" >/dev/null 2>&1 || true ;; esac sleep 2 # Brief pause between deletions done # Clean up temporary files rm -f /tmp/ecs-task-trust-policy.json rm -f /tmp/task-definition.json rm -f /tmp/service-definition.json log "Cleanup completed" } # Main execution main() { log "Starting $SCRIPT_NAME v4 (Default VPC)" log "Region: $REGION" log "Log file: $LOG_FILE" # Get AWS account ID get_account_id # Setup infrastructure using default VPC setup_default_vpc_infrastructure # Create CloudWatch log groups create_log_groups # Create ECS cluster create_ecs_cluster # Create IAM roles create_iam_roles # Create task definition create_task_definition # Create ECS service create_ecs_service # Verify deployment verify_deployment log "Tutorial completed successfully!" # Display created resources display_resources # Ask user if they want to clean up echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " CLEANUP_CHOICE="y" if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then cleanup_resources log "All resources have been cleaned up" else log "Resources left intact. You can clean them up later by running the cleanup function." echo "" echo "To clean up resources later, you can use the AWS CLI commands or the AWS Management Console." echo "Remember to delete resources in the correct order to avoid dependency issues." fi } # Make script executable and run chmod +x "$0" main "$@"

The following code example shows how to:

  • Create a VPC

  • Create subnets

  • Configure internet connectivity

  • Create a NAT Gateway

  • Configure subnet settings

  • Create security groups

  • Verify your VPC configuration

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # VPC Creation Script # This script creates a VPC with public and private subnets, internet gateway, NAT gateway, and security groups # Set up logging LOG_FILE="vpc_creation.log" exec > >(tee -a "$LOG_FILE") 2>&1 # Function to handle errors handle_error() { echo "ERROR: $1" echo "Resources created before error:" for resource in "${CREATED_RESOURCES[@]}" do echo "- $resource" done echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources in reverse order..." # Reverse the array to delete in reverse order of creation for ((i=${#CREATED_RESOURCES[@]}-1; i>=0; i--)) do resource="${CREATED_RESOURCES[$i]}" resource_type=$(echo "$resource" | cut -d':' -f1) resource_id=$(echo "$resource" | cut -d':' -f2) case "$resource_type" in "INSTANCE") echo "Terminating EC2 instance: $resource_id" aws ec2 terminate-instances --instance-ids "$resource_id" || echo "Failed to terminate instance: $resource_id" # Wait for instance to terminate echo "Waiting for instance to terminate..." aws ec2 wait instance-terminated --instance-ids "$resource_id" || echo "Failed to wait for instance termination: $resource_id" ;; "KEY_PAIR") echo "Deleting key pair: $resource_id" aws ec2 delete-key-pair --key-name "$resource_id" || echo "Failed to delete key pair: $resource_id" # Remove the .pem file if it exists if [ -f "${resource_id}.pem" ]; then rm -f "${resource_id}.pem" fi ;; "NAT_GATEWAY") echo "Deleting NAT Gateway: $resource_id" aws ec2 delete-nat-gateway --nat-gateway-id "$resource_id" || echo "Failed to delete NAT Gateway: $resource_id" # NAT Gateway deletion takes time, wait for it to complete echo "Waiting for NAT Gateway to be deleted..." aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$resource_id" || echo "Failed to wait for NAT Gateway deletion: $resource_id" ;; "EIP") echo "Releasing Elastic IP: $resource_id" aws ec2 release-address --allocation-id "$resource_id" || echo "Failed to release Elastic IP: $resource_id" ;; "ROUTE_TABLE_ASSOCIATION") echo "Disassociating Route Table: $resource_id" aws ec2 disassociate-route-table --association-id "$resource_id" || echo "Failed to disassociate Route Table: $resource_id" ;; "ROUTE_TABLE") echo "Deleting Route Table: $resource_id" aws ec2 delete-route-table --route-table-id "$resource_id" || echo "Failed to delete Route Table: $resource_id" ;; "INTERNET_GATEWAY") echo "Detaching Internet Gateway: $resource_id from VPC: $VPC_ID" aws ec2 detach-internet-gateway --internet-gateway-id "$resource_id" --vpc-id "$VPC_ID" || echo "Failed to detach Internet Gateway: $resource_id" echo "Deleting Internet Gateway: $resource_id" aws ec2 delete-internet-gateway --internet-gateway-id "$resource_id" || echo "Failed to delete Internet Gateway: $resource_id" ;; "SECURITY_GROUP") echo "Deleting Security Group: $resource_id" aws ec2 delete-security-group --group-id "$resource_id" || echo "Failed to delete Security Group: $resource_id" ;; "SUBNET") echo "Deleting Subnet: $resource_id" aws ec2 delete-subnet --subnet-id "$resource_id" || echo "Failed to delete Subnet: $resource_id" ;; "VPC") echo "Deleting VPC: $resource_id" aws ec2 delete-vpc --vpc-id "$resource_id" || echo "Failed to delete VPC: $resource_id" ;; esac done } # Initialize array to track created resources CREATED_RESOURCES=() echo "Starting VPC creation script at $(date)" # Verify AWS CLI configuration echo "Verifying AWS CLI configuration..." aws configure list || handle_error "AWS CLI is not properly configured" # Verify identity and permissions echo "Verifying identity and permissions..." if ! aws sts get-caller-identity; then echo "ERROR: Unable to verify AWS identity. This could be due to:" echo " - Expired credentials" echo " - Missing or invalid AWS credentials" echo " - Insufficient permissions" echo "" echo "Please run 'aws configure' to update your credentials or check your IAM permissions." exit 1 fi # Create VPC echo "Creating VPC with CIDR block 10.0.0.0/16..." VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=MyVPC},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' --query 'Vpc.VpcId' --output text) if [ -z "$VPC_ID" ]; then handle_error "Failed to create VPC" fi CREATED_RESOURCES+=("VPC:$VPC_ID") echo "VPC created with ID: $VPC_ID" # Enable DNS support and hostnames echo "Enabling DNS support and hostnames for VPC..." aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-support || handle_error "Failed to enable DNS support" aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames || handle_error "Failed to enable DNS hostnames" # Get available Availability Zones echo "Getting available Availability Zones..." AZ1=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[0].ZoneName' --output text) AZ2=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[1].ZoneName' --output text) if [ -z "$AZ1" ] || [ -z "$AZ2" ]; then handle_error "Failed to get Availability Zones" fi echo "Using Availability Zones: $AZ1 and $AZ2" # Create public subnets echo "Creating public subnet in $AZ1..." PUBLIC_SUBNET_AZ1=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.0.0/24 \ --availability-zone "$AZ1" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-AZ1},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PUBLIC_SUBNET_AZ1" ]; then handle_error "Failed to create public subnet in AZ1" fi CREATED_RESOURCES+=("SUBNET:$PUBLIC_SUBNET_AZ1") echo "Public subnet created in $AZ1 with ID: $PUBLIC_SUBNET_AZ1" echo "Creating public subnet in $AZ2..." PUBLIC_SUBNET_AZ2=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.1.0/24 \ --availability-zone "$AZ2" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-AZ2},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PUBLIC_SUBNET_AZ2" ]; then handle_error "Failed to create public subnet in AZ2" fi CREATED_RESOURCES+=("SUBNET:$PUBLIC_SUBNET_AZ2") echo "Public subnet created in $AZ2 with ID: $PUBLIC_SUBNET_AZ2" # Create private subnets echo "Creating private subnet in $AZ1..." PRIVATE_SUBNET_AZ1=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.2.0/24 \ --availability-zone "$AZ1" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Private-Subnet-AZ1},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PRIVATE_SUBNET_AZ1" ]; then handle_error "Failed to create private subnet in AZ1" fi CREATED_RESOURCES+=("SUBNET:$PRIVATE_SUBNET_AZ1") echo "Private subnet created in $AZ1 with ID: $PRIVATE_SUBNET_AZ1" echo "Creating private subnet in $AZ2..." PRIVATE_SUBNET_AZ2=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.3.0/24 \ --availability-zone "$AZ2" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Private-Subnet-AZ2},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PRIVATE_SUBNET_AZ2" ]; then handle_error "Failed to create private subnet in AZ2" fi CREATED_RESOURCES+=("SUBNET:$PRIVATE_SUBNET_AZ2") echo "Private subnet created in $AZ2 with ID: $PRIVATE_SUBNET_AZ2" # Create Internet Gateway echo "Creating Internet Gateway..." IGW_ID=$(aws ec2 create-internet-gateway \ --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=MyIGW},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'InternetGateway.InternetGatewayId' \ --output text) if [ -z "$IGW_ID" ]; then handle_error "Failed to create Internet Gateway" fi CREATED_RESOURCES+=("INTERNET_GATEWAY:$IGW_ID") echo "Internet Gateway created with ID: $IGW_ID" # Attach Internet Gateway to VPC echo "Attaching Internet Gateway to VPC..." aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" || handle_error "Failed to attach Internet Gateway to VPC" # Create public route table echo "Creating public route table..." PUBLIC_RT=$(aws ec2 create-route-table \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Public-RT},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'RouteTable.RouteTableId' \ --output text) if [ -z "$PUBLIC_RT" ]; then handle_error "Failed to create public route table" fi CREATED_RESOURCES+=("ROUTE_TABLE:$PUBLIC_RT") echo "Public route table created with ID: $PUBLIC_RT" # Add route to Internet Gateway echo "Adding route to Internet Gateway in public route table..." aws ec2 create-route --route-table-id "$PUBLIC_RT" --destination-cidr-block 0.0.0.0/0 --gateway-id "$IGW_ID" || handle_error "Failed to add route to Internet Gateway" # Associate public subnets with public route table echo "Associating public subnet in $AZ1 with public route table..." PUBLIC_RT_ASSOC_1=$(aws ec2 associate-route-table --route-table-id "$PUBLIC_RT" --subnet-id "$PUBLIC_SUBNET_AZ1" --query 'AssociationId' --output text) if [ -z "$PUBLIC_RT_ASSOC_1" ]; then handle_error "Failed to associate public subnet in AZ1 with public route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PUBLIC_RT_ASSOC_1") echo "Associating public subnet in $AZ2 with public route table..." PUBLIC_RT_ASSOC_2=$(aws ec2 associate-route-table --route-table-id "$PUBLIC_RT" --subnet-id "$PUBLIC_SUBNET_AZ2" --query 'AssociationId' --output text) if [ -z "$PUBLIC_RT_ASSOC_2" ]; then handle_error "Failed to associate public subnet in AZ2 with public route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PUBLIC_RT_ASSOC_2") # Create private route table echo "Creating private route table..." PRIVATE_RT=$(aws ec2 create-route-table \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Private-RT},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'RouteTable.RouteTableId' \ --output text) if [ -z "$PRIVATE_RT" ]; then handle_error "Failed to create private route table" fi CREATED_RESOURCES+=("ROUTE_TABLE:$PRIVATE_RT") echo "Private route table created with ID: $PRIVATE_RT" # Associate private subnets with private route table echo "Associating private subnet in $AZ1 with private route table..." PRIVATE_RT_ASSOC_1=$(aws ec2 associate-route-table --route-table-id "$PRIVATE_RT" --subnet-id "$PRIVATE_SUBNET_AZ1" --query 'AssociationId' --output text) if [ -z "$PRIVATE_RT_ASSOC_1" ]; then handle_error "Failed to associate private subnet in AZ1 with private route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PRIVATE_RT_ASSOC_1") echo "Associating private subnet in $AZ2 with private route table..." PRIVATE_RT_ASSOC_2=$(aws ec2 associate-route-table --route-table-id "$PRIVATE_RT" --subnet-id "$PRIVATE_SUBNET_AZ2" --query 'AssociationId' --output text) if [ -z "$PRIVATE_RT_ASSOC_2" ]; then handle_error "Failed to associate private subnet in AZ2 with private route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PRIVATE_RT_ASSOC_2") # Allocate Elastic IP for NAT Gateway echo "Allocating Elastic IP for NAT Gateway..." EIP_ALLOC=$(aws ec2 allocate-address --domain vpc --tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' --query 'AllocationId' --output text) if [ -z "$EIP_ALLOC" ]; then handle_error "Failed to allocate Elastic IP" fi CREATED_RESOURCES+=("EIP:$EIP_ALLOC") echo "Elastic IP allocated with ID: $EIP_ALLOC" # Create NAT Gateway echo "Creating NAT Gateway in public subnet in $AZ1..." NAT_GW=$(aws ec2 create-nat-gateway \ --subnet-id "$PUBLIC_SUBNET_AZ1" \ --allocation-id "$EIP_ALLOC" \ --tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=MyNATGateway},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'NatGateway.NatGatewayId' \ --output text) if [ -z "$NAT_GW" ]; then handle_error "Failed to create NAT Gateway" fi CREATED_RESOURCES+=("NAT_GATEWAY:$NAT_GW") echo "NAT Gateway created with ID: $NAT_GW" # Wait for NAT Gateway to be available echo "Waiting for NAT Gateway to be available..." aws ec2 wait nat-gateway-available --nat-gateway-ids "$NAT_GW" || handle_error "NAT Gateway did not become available" # Add route to NAT Gateway in private route table echo "Adding route to NAT Gateway in private route table..." aws ec2 create-route --route-table-id "$PRIVATE_RT" --destination-cidr-block 0.0.0.0/0 --nat-gateway-id "$NAT_GW" || handle_error "Failed to add route to NAT Gateway" # Enable auto-assign public IP for instances in public subnets echo "Enabling auto-assign public IP for instances in public subnet in $AZ1..." aws ec2 modify-subnet-attribute --subnet-id "$PUBLIC_SUBNET_AZ1" --map-public-ip-on-launch || handle_error "Failed to enable auto-assign public IP for public subnet in AZ1" echo "Enabling auto-assign public IP for instances in public subnet in $AZ2..." aws ec2 modify-subnet-attribute --subnet-id "$PUBLIC_SUBNET_AZ2" --map-public-ip-on-launch || handle_error "Failed to enable auto-assign public IP for public subnet in AZ2" # Create security group for web servers echo "Creating security group for web servers..." WEB_SG=$(aws ec2 create-security-group \ --group-name "WebServerSG-$(date +%s)" \ --description "Security group for web servers" \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'GroupId' \ --output text) if [ -z "$WEB_SG" ]; then handle_error "Failed to create security group for web servers" fi CREATED_RESOURCES+=("SECURITY_GROUP:$WEB_SG") echo "Security group for web servers created with ID: $WEB_SG" # Allow HTTP and HTTPS traffic echo "Allowing HTTP traffic to web servers security group..." aws ec2 authorize-security-group-ingress --group-id "$WEB_SG" --protocol tcp --port 80 --cidr 0.0.0.0/0 || handle_error "Failed to allow HTTP traffic" echo "Allowing HTTPS traffic to web servers security group..." aws ec2 authorize-security-group-ingress --group-id "$WEB_SG" --protocol tcp --port 443 --cidr 0.0.0.0/0 || handle_error "Failed to allow HTTPS traffic" # Note: In a production environment, you should restrict the source IP ranges for security echo "NOTE: In a production environment, you should restrict the source IP ranges for HTTP and HTTPS traffic" # Create security group for database servers echo "Creating security group for database servers..." DB_SG=$(aws ec2 create-security-group \ --group-name "DBServerSG-$(date +%s)" \ --description "Security group for database servers" \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'GroupId' \ --output text) if [ -z "$DB_SG" ]; then handle_error "Failed to create security group for database servers" fi CREATED_RESOURCES+=("SECURITY_GROUP:$DB_SG") echo "Security group for database servers created with ID: $DB_SG" # Allow MySQL/Aurora traffic from web servers only echo "Allowing MySQL/Aurora traffic from web servers to database servers..." aws ec2 authorize-security-group-ingress --group-id "$DB_SG" --protocol tcp --port 3306 --source-group "$WEB_SG" || handle_error "Failed to allow MySQL/Aurora traffic" # Verify VPC configuration echo "Verifying VPC configuration..." echo "VPC:" aws ec2 describe-vpcs --vpc-id "$VPC_ID" || handle_error "Failed to describe VPC" echo "Subnets:" aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe subnets" echo "Route tables:" aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe route tables" echo "Internet gateway:" aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" || handle_error "Failed to describe Internet Gateway" echo "NAT gateway:" aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe NAT Gateway" echo "Security groups:" aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe security groups" echo "" # Summary of created resources echo "VPC creation completed successfully!" echo "Summary of created resources:" echo "- VPC: $VPC_ID" echo "- Public Subnet in $AZ1: $PUBLIC_SUBNET_AZ1" echo "- Public Subnet in $AZ2: $PUBLIC_SUBNET_AZ2" echo "- Private Subnet in $AZ1: $PRIVATE_SUBNET_AZ1" echo "- Private Subnet in $AZ2: $PRIVATE_SUBNET_AZ2" echo "- Internet Gateway: $IGW_ID" echo "- Public Route Table: $PUBLIC_RT" echo "- Private Route Table: $PRIVATE_RT" echo "- Elastic IP: $EIP_ALLOC" echo "- NAT Gateway: $NAT_GW" echo "- Web Servers Security Group: $WEB_SG" echo "- Database Servers Security Group: $DB_SG" # Deploy EC2 instances echo "" echo "Deploying EC2 instances..." # Create key pair for SSH access KEY_NAME="vpc-tutorial-key-$(date +%s)" echo "Creating key pair $KEY_NAME..." aws ec2 create-key-pair --key-name "$KEY_NAME" --tag-specifications 'ResourceType=key-pair,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' --query 'KeyMaterial' --output text > "${KEY_NAME}.pem" || handle_error "Failed to create key pair" chmod 400 "${KEY_NAME}.pem" echo "Key pair saved to ${KEY_NAME}.pem" CREATED_RESOURCES+=("KEY_PAIR:$KEY_NAME") # Get latest Amazon Linux 2 AMI echo "Getting latest Amazon Linux 2 AMI..." AMI_ID=$(aws ec2 describe-images --owners amazon \ --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" "Name=state,Values=available" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text) || handle_error "Failed to get AMI" echo "Using AMI: $AMI_ID" # Launch web server in public subnet echo "Launching web server in public subnet..." WEB_INSTANCE=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --count 1 \ --instance-type t2.micro \ --key-name "$KEY_NAME" \ --security-group-ids "$WEB_SG" \ --subnet-id "$PUBLIC_SUBNET_AZ1" \ --associate-public-ip-address \ --user-data '#!/bin/bash yum update -y yum install -y httpd systemctl start httpd systemctl enable httpd echo "<h1>Hello from $(hostname -f) in the public subnet</h1>" > /var/www/html/index.html' \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=WebServer},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'Instances[0].InstanceId' \ --output text) || handle_error "Failed to launch web server" echo "Web server instance created with ID: $WEB_INSTANCE" CREATED_RESOURCES+=("INSTANCE:$WEB_INSTANCE") # Wait for web server to be running echo "Waiting for web server to be running..." aws ec2 wait instance-running --instance-ids "$WEB_INSTANCE" # Get web server public IP WEB_PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$WEB_INSTANCE" \ --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) echo "Web server public IP: $WEB_PUBLIC_IP" echo "You can access the web server at: http://$WEB_PUBLIC_IP" # Launch database server in private subnet echo "Launching database server in private subnet..." DB_INSTANCE=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --count 1 \ --instance-type t2.micro \ --key-name "$KEY_NAME" \ --security-group-ids "$DB_SG" \ --subnet-id "$PRIVATE_SUBNET_AZ1" \ --user-data '#!/bin/bash yum update -y yum install -y mariadb-server systemctl start mariadb systemctl enable mariadb' \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=DBServer},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-gs}]' \ --query 'Instances[0].InstanceId' \ --output text) || handle_error "Failed to launch database server" echo "Database server instance created with ID: $DB_INSTANCE" CREATED_RESOURCES+=("INSTANCE:$DB_INSTANCE") # Wait for database server to be running echo "Waiting for database server to be running..." aws ec2 wait instance-running --instance-ids "$DB_INSTANCE" # Get database server private IP DB_PRIVATE_IP=$(aws ec2 describe-instances --instance-ids "$DB_INSTANCE" \ --query 'Reservations[0].Instances[0].PrivateIpAddress' --output text) echo "Database server private IP: $DB_PRIVATE_IP" echo "EC2 instances deployed successfully!" echo "- Web Server (Public): $WEB_INSTANCE ($WEB_PUBLIC_IP)" echo "- Database Server (Private): $DB_INSTANCE ($DB_PRIVATE_IP)" echo "" echo "Note: To connect to the web server: ssh -i ${KEY_NAME}.pem ec2-user@$WEB_PUBLIC_IP" echo "To connect to the database server, you must first connect to the web server, then use it as a bastion host." echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Cleaning up resources..." cleanup_resources echo "All resources have been cleaned up." else echo "Resources will not be cleaned up. You can manually clean them up later." fi echo "Script completed at $(date)"

The following code example shows how to:

  • Create the cluster

  • Create a task definition

  • Create the service

  • Clean up

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Amazon ECS Fargate Tutorial Script - Version 5 # This script creates an ECS cluster, task definition, and service using Fargate launch type # Fixed version with proper resource dependency handling during cleanup # Security improvements applied set -e # Exit on any error # Initialize logging LOG_FILE="ecs-fargate-tutorial-v5.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting Amazon ECS Fargate tutorial at $(date)" echo "Log file: $LOG_FILE" # Generate random identifier for unique resource names RANDOM_ID=$(openssl rand -hex 6) CLUSTER_NAME="fargate-cluster-${RANDOM_ID}" SERVICE_NAME="fargate-service-${RANDOM_ID}" TASK_FAMILY="sample-fargate-${RANDOM_ID}" SECURITY_GROUP_NAME="ecs-fargate-sg-${RANDOM_ID}" # Array to track created resources for cleanup CREATED_RESOURCES=() # Function to log and execute commands with input validation execute_command() { local cmd="$1" local description="$2" # Validate that cmd is not empty if [[ -z "$cmd" ]]; then echo "ERROR: Command is empty" return 1 fi echo "" echo "==========================================" echo "EXECUTING: $description" echo "COMMAND: $cmd" echo "==========================================" local output local exit_code set +e # Temporarily disable exit on error output=$(eval "$cmd" 2>&1) exit_code=$? set -e # Re-enable exit on error if [[ $exit_code -eq 0 ]]; then echo "SUCCESS: $description" echo "OUTPUT: $output" return 0 else echo "FAILED: $description" echo "EXIT CODE: $exit_code" echo "OUTPUT: $output" return 1 fi } # Function to check for actual AWS API errors in command output check_for_aws_errors() { local output="$1" local description="$2" # Look for specific AWS error patterns, not just the word "error" if echo "$output" | grep -qi "An error occurred\|InvalidParameter\|AccessDenied\|ResourceNotFound\|ValidationException"; then echo "AWS API ERROR detected in output for: $description" echo "Output: $output" return 1 fi return 0 } # Function to safely extract JSON values safe_json_extract() { local json="$1" local key="$2" local value value=$(echo "$json" | grep -o "\"$key\": \"[^\"]*\"" | cut -d'"' -f4 2>/dev/null || echo "") echo "$value" } # Function to wait for network interfaces to be cleaned up wait_for_network_interfaces_cleanup() { local security_group_id="$1" local max_attempts=30 local attempt=1 # Validate security group ID format if [[ ! "$security_group_id" =~ ^sg-[a-z0-9]{8,17}$ ]]; then echo "ERROR: Invalid security group ID format: $security_group_id" return 1 fi echo "Waiting for network interfaces to be cleaned up..." while [[ $attempt -le $max_attempts ]]; do echo "Attempt $attempt/$max_attempts: Checking for dependent network interfaces..." # Check if there are any network interfaces still using this security group local eni_count eni_count=$(aws ec2 describe-network-interfaces \ --filters "Name=group-id,Values=$security_group_id" \ --query "length(NetworkInterfaces)" \ --output text 2>/dev/null || echo "0") if [[ "$eni_count" == "0" ]]; then echo "No network interfaces found using security group $security_group_id" return 0 else echo "Found $eni_count network interface(s) still using security group $security_group_id" echo "Waiting 10 seconds before next check..." sleep 10 ((attempt++)) fi done echo "WARNING: Network interfaces may still be attached after $max_attempts attempts" echo "This is normal and the security group deletion will be retried" return 1 } # Function to retry security group deletion with exponential backoff retry_security_group_deletion() { local security_group_id="$1" local max_attempts=10 local attempt=1 local wait_time=5 # Validate security group ID format if [[ ! "$security_group_id" =~ ^sg-[a-z0-9]{8,17}$ ]]; then echo "ERROR: Invalid security group ID format: $security_group_id" return 1 fi while [[ $attempt -le $max_attempts ]]; do echo "Attempt $attempt/$max_attempts: Trying to delete security group $security_group_id" if execute_command "aws ec2 delete-security-group --group-id '$security_group_id'" "Delete security group (attempt $attempt)"; then echo "Successfully deleted security group $security_group_id" return 0 else if [[ $attempt -eq $max_attempts ]]; then echo "FAILED: Could not delete security group $security_group_id after $max_attempts attempts" echo "This may be due to network interfaces that are still being cleaned up by AWS" echo "You can manually delete it later using: aws ec2 delete-security-group --group-id $security_group_id" return 1 else echo "Waiting $wait_time seconds before retry..." sleep $wait_time wait_time=$((wait_time * 2)) # Exponential backoff ((attempt++)) fi fi done } # Function to cleanup resources with proper dependency handling cleanup_resources() { echo "" echo "===========================================" echo "CLEANUP PROCESS" echo "===========================================" echo "The following resources were created:" for resource in "${CREATED_RESOURCES[@]}"; do echo " - $resource" done echo "" echo "Auto-confirming cleanup of all created resources..." CLEANUP_CHOICE="y" if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Starting cleanup process..." # Step 1: Scale service to 0 tasks first, then delete service if [[ " ${CREATED_RESOURCES[*]} " =~ " ECS Service: $SERVICE_NAME " ]]; then echo "" echo "Step 1: Scaling service to 0 tasks..." if execute_command "aws ecs update-service --cluster '$CLUSTER_NAME' --service '$SERVICE_NAME' --desired-count 0" "Scale service to 0 tasks"; then echo "Waiting for service to stabilize after scaling to 0..." execute_command "aws ecs wait services-stable --cluster '$CLUSTER_NAME' --services '$SERVICE_NAME'" "Wait for service to stabilize" echo "Deleting service..." execute_command "aws ecs delete-service --cluster '$CLUSTER_NAME' --service '$SERVICE_NAME'" "Delete ECS service" else echo "WARNING: Failed to scale service. Attempting to delete anyway..." execute_command "aws ecs delete-service --cluster '$CLUSTER_NAME' --service '$SERVICE_NAME' --force" "Force delete ECS service" fi fi # Step 2: Wait a bit for tasks to fully terminate echo "" echo "Step 2: Waiting for tasks to fully terminate..." sleep 15 # Step 3: Delete cluster if [[ " ${CREATED_RESOURCES[*]} " =~ " ECS Cluster: $CLUSTER_NAME " ]]; then echo "" echo "Step 3: Deleting cluster..." execute_command "aws ecs delete-cluster --cluster '$CLUSTER_NAME'" "Delete ECS cluster" fi # Step 4: Wait for network interfaces to be cleaned up, then delete security group if [[ -n "$SECURITY_GROUP_ID" && "$SECURITY_GROUP_ID" != "None" ]]; then echo "" echo "Step 4: Cleaning up security group..." # First, wait for network interfaces to be cleaned up wait_for_network_interfaces_cleanup "$SECURITY_GROUP_ID" # Then retry security group deletion with backoff retry_security_group_deletion "$SECURITY_GROUP_ID" fi # Step 5: Clean up task definition (deregister all revisions) if [[ " ${CREATED_RESOURCES[*]} " =~ " Task Definition: $TASK_FAMILY " ]]; then echo "" echo "Step 5: Deregistering task definition revisions..." # Get all revisions of the task definition local revisions revisions=$(aws ecs list-task-definitions --family-prefix "$TASK_FAMILY" --query "taskDefinitionArns" --output text 2>/dev/null || echo "") if [[ -n "$revisions" && "$revisions" != "None" ]]; then for revision_arn in $revisions; do echo "Deregistering task definition: $revision_arn" execute_command "aws ecs deregister-task-definition --task-definition '$revision_arn'" "Deregister task definition $revision_arn" || true done else echo "No task definition revisions found to deregister" fi fi echo "" echo "===========================================" echo "CLEANUP COMPLETED" echo "===========================================" echo "All resources have been cleaned up successfully!" else echo "Cleanup skipped. Resources remain active." echo "" echo "To clean up manually later, use the following commands in order:" echo "1. Scale service to 0: aws ecs update-service --cluster $CLUSTER_NAME --service $SERVICE_NAME --desired-count 0" echo "2. Wait for stability: aws ecs wait services-stable --cluster $CLUSTER_NAME --services $SERVICE_NAME" echo "3. Delete service: aws ecs delete-service --cluster $CLUSTER_NAME --service $SERVICE_NAME" echo "4. Delete cluster: aws ecs delete-cluster --cluster $CLUSTER_NAME" echo "5. Wait 2-3 minutes, then delete security group: aws ec2 delete-security-group --group-id $SECURITY_GROUP_ID" if [[ " ${CREATED_RESOURCES[*]} " =~ " Task Definition: $TASK_FAMILY " ]]; then echo "6. Deregister task definitions: aws ecs list-task-definitions --family-prefix $TASK_FAMILY" echo " Then for each ARN: aws ecs deregister-task-definition --task-definition <ARN>" fi fi } # Trap to handle script interruption trap cleanup_resources EXIT echo "Using random identifier: $RANDOM_ID" echo "Cluster name: $CLUSTER_NAME" echo "Service name: $SERVICE_NAME" echo "Task family: $TASK_FAMILY" # Step 1: Ensure ECS task execution role exists echo "" echo "===========================================" echo "STEP 1: VERIFY ECS TASK EXECUTION ROLE" echo "===========================================" ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) if [[ ! "$ACCOUNT_ID" =~ ^[0-9]{12}$ ]]; then echo "ERROR: Invalid AWS Account ID retrieved: $ACCOUNT_ID" exit 1 fi EXECUTION_ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole" # Check if role exists if aws iam get-role --role-name ecsTaskExecutionRole >/dev/null 2>&1; then echo "ECS task execution role already exists" else echo "Creating ECS task execution role..." # Create trust policy with strict validation cat > trust-policy.json << 'EOF' { "Version":"2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ecs-tasks.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF # Validate JSON before using if ! jq empty trust-policy.json 2>/dev/null; then echo "ERROR: Invalid JSON in trust policy" rm -f trust-policy.json exit 1 fi execute_command "aws iam create-role --role-name ecsTaskExecutionRole --assume-role-policy-document file://trust-policy.json" "Create ECS task execution role" aws iam tag-role --role-name ecsTaskExecutionRole --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-ecs-fargate-linux execute_command "aws iam attach-role-policy --role-name ecsTaskExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" "Attach ECS task execution policy" # Clean up temporary file securely shred -vfz -n 3 trust-policy.json 2>/dev/null || rm -f trust-policy.json CREATED_RESOURCES+=("IAM Role: ecsTaskExecutionRole") fi # Step 2: Create ECS cluster echo "" echo "===========================================" echo "STEP 2: CREATE ECS CLUSTER" echo "===========================================" CLUSTER_OUTPUT=$(execute_command "aws ecs create-cluster --cluster-name '$CLUSTER_NAME' --tags key=project,value=doc-smith key=tutorial,value=amazon-ecs-fargate-linux" "Create ECS cluster") check_for_aws_errors "$CLUSTER_OUTPUT" "Create ECS cluster" CREATED_RESOURCES+=("ECS Cluster: $CLUSTER_NAME") # Step 3: Create task definition echo "" echo "===========================================" echo "STEP 3: CREATE TASK DEFINITION" echo "===========================================" # Create task definition JSON with validated inputs cat > task-definition.json << EOF { "family": "$TASK_FAMILY", "networkMode": "awsvpc", "requiresCompatibilities": ["FARGATE"], "cpu": "256", "memory": "512", "executionRoleArn": "$EXECUTION_ROLE_ARN", "containerDefinitions": [ { "name": "fargate-app", "image": "public.ecr.aws/docker/library/httpd:2.4-alpine", "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "essential": true, "entryPoint": ["sh", "-c"], "command": [ "/bin/sh -c \"echo '<html> <head> <title>Amazon ECS Sample App</title> <style>body {margin-top: 40px; background-color: #333;} </style> </head><body> <div style=color:white;text-align:center> <h1>Amazon ECS Sample App</h1> <h2>Congratulations!</h2> <p>Your application is now running on a container in Amazon ECS.</p> </div></body></html>' > /usr/local/apache2/htdocs/index.html && httpd-foreground\"" ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/fargate-sample", "awslogs-region": "us-east-1", "awslogs-stream-prefix": "ecs" } } } ] } EOF # Validate JSON before using if ! jq empty task-definition.json 2>/dev/null; then echo "ERROR: Invalid JSON in task definition" rm -f task-definition.json exit 1 fi TASK_DEF_OUTPUT=$(execute_command "aws ecs register-task-definition --cli-input-json file://task-definition.json" "Register task definition") check_for_aws_errors "$TASK_DEF_OUTPUT" "Register task definition" # Clean up temporary file securely shred -vfz -n 3 task-definition.json 2>/dev/null || rm -f task-definition.json CREATED_RESOURCES+=("Task Definition: $TASK_FAMILY") # Step 4: Set up networking echo "" echo "===========================================" echo "STEP 4: SET UP NETWORKING" echo "===========================================" # Get default VPC ID VPC_ID=$(aws ec2 describe-vpcs --filters "Name=is-default,Values=true" --query "Vpcs[0].VpcId" --output text) if [[ "$VPC_ID" == "None" || -z "$VPC_ID" ]]; then echo "ERROR: No default VPC found. Please create a default VPC or specify a custom VPC." exit 1 fi # Validate VPC ID format if [[ ! "$VPC_ID" =~ ^vpc-[a-z0-9]{8,17}$ ]]; then echo "ERROR: Invalid VPC ID format: $VPC_ID" exit 1 fi echo "Using default VPC: $VPC_ID" # Create security group with restricted access # Note: This allows HTTP access from anywhere for demo purposes # In production, restrict source to specific IP ranges or security groups SECURITY_GROUP_OUTPUT=$(execute_command "aws ec2 create-security-group --group-name '$SECURITY_GROUP_NAME' --description 'Security group for ECS Fargate tutorial - HTTP access' --vpc-id '$VPC_ID' --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-ecs-fargate-linux}]'" "Create security group") check_for_aws_errors "$SECURITY_GROUP_OUTPUT" "Create security group" SECURITY_GROUP_ID=$(echo "$SECURITY_GROUP_OUTPUT" | grep -o '"GroupId": "[^"]*"' | head -1 | cut -d'"' -f4) if [[ -z "$SECURITY_GROUP_ID" || "$SECURITY_GROUP_ID" == "None" ]]; then SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --group-names "$SECURITY_GROUP_NAME" --filters "Name=vpc-id,Values=$VPC_ID" --query "SecurityGroups[0].GroupId" --output text) fi # Validate security group ID format if [[ ! "$SECURITY_GROUP_ID" =~ ^sg-[a-z0-9]{8,17}$ ]]; then echo "ERROR: Invalid security group ID format: $SECURITY_GROUP_ID" exit 1 fi echo "Created security group: $SECURITY_GROUP_ID" CREATED_RESOURCES+=("Security Group: $SECURITY_GROUP_ID") # Add HTTP inbound rule # WARNING: This allows HTTP access from anywhere (0.0.0.0/0) # In production environments, restrict this to specific IP ranges execute_command "aws ec2 authorize-security-group-ingress --group-id '$SECURITY_GROUP_ID' --protocol tcp --port 80 --cidr 0.0.0.0/0" "Add HTTP inbound rule to security group" # Get subnet IDs from default VPC echo "Getting subnet IDs from default VPC..." SUBNET_IDS_RAW=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query "Subnets[*].SubnetId" --output text) if [[ -z "$SUBNET_IDS_RAW" ]]; then echo "ERROR: No subnets found in default VPC" exit 1 fi # Convert to proper comma-separated format, handling both spaces and tabs SUBNET_IDS_COMMA=$(echo "$SUBNET_IDS_RAW" | tr -s '[:space:]' ',' | sed 's/,$//') echo "Raw subnet IDs: $SUBNET_IDS_RAW" echo "Formatted subnet IDs: $SUBNET_IDS_COMMA" # Validate subnet IDs format if [[ ! "$SUBNET_IDS_COMMA" =~ ^subnet-[a-z0-9]+(,subnet-[a-z0-9]+)*$ ]]; then echo "ERROR: Invalid subnet ID format: $SUBNET_IDS_COMMA" exit 1 fi # Step 5: Create ECS service echo "" echo "===========================================" echo "STEP 5: CREATE ECS SERVICE" echo "===========================================" # Create the service with proper JSON formatting for network configuration SERVICE_CMD="aws ecs create-service --cluster '$CLUSTER_NAME' --service-name '$SERVICE_NAME' --task-definition '$TASK_FAMILY' --desired-count 1 --launch-type FARGATE --network-configuration '{\"awsvpcConfiguration\":{\"subnets\":[\"$(echo "$SUBNET_IDS_COMMA" | sed 's/,/","/g')\"],\"securityGroups\":[\"$SECURITY_GROUP_ID\"],\"assignPublicIp\":\"ENABLED\"}}' --tags key=project,value=doc-smith key=tutorial,value=amazon-ecs-fargate-linux" echo "Service creation command: $SERVICE_CMD" SERVICE_OUTPUT=$(execute_command "$SERVICE_CMD" "Create ECS service") check_for_aws_errors "$SERVICE_OUTPUT" "Create ECS service" CREATED_RESOURCES+=("ECS Service: $SERVICE_NAME") # Step 6: Wait for service to stabilize and get public IP echo "" echo "===========================================" echo "STEP 6: WAIT FOR SERVICE AND GET PUBLIC IP" echo "===========================================" echo "Waiting for service to stabilize (this may take a few minutes)..." execute_command "aws ecs wait services-stable --cluster '$CLUSTER_NAME' --services '$SERVICE_NAME'" "Wait for service to stabilize" # Get task ARN TASK_ARN=$(aws ecs list-tasks --cluster "$CLUSTER_NAME" --service-name "$SERVICE_NAME" --query "taskArns[0]" --output text) if [[ "$TASK_ARN" == "None" || -z "$TASK_ARN" ]]; then echo "ERROR: No running tasks found for service" exit 1 fi echo "Task ARN: $TASK_ARN" # Get network interface ID ENI_ID=$(aws ecs describe-tasks --cluster "$CLUSTER_NAME" --tasks "$TASK_ARN" --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" --output text) if [[ "$ENI_ID" == "None" || -z "$ENI_ID" ]]; then echo "ERROR: Could not retrieve network interface ID" exit 1 fi # Validate ENI ID format if [[ ! "$ENI_ID" =~ ^eni-[a-z0-9]{8,17}$ ]]; then echo "ERROR: Invalid network interface ID format: $ENI_ID" exit 1 fi echo "Network Interface ID: $ENI_ID" # Get public IP PUBLIC_IP=$(aws ec2 describe-network-interfaces --network-interface-ids "$ENI_ID" --query "NetworkInterfaces[0].Association.PublicIp" --output text) if [[ "$PUBLIC_IP" == "None" || -z "$PUBLIC_IP" ]]; then echo "WARNING: No public IP assigned to the task" echo "The task may be in a private subnet or public IP assignment failed" else # Validate IP format if [[ ! "$PUBLIC_IP" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then echo "ERROR: Invalid IP address format: $PUBLIC_IP" PUBLIC_IP="" else echo "" echo "===========================================" echo "SUCCESS! APPLICATION IS RUNNING" echo "===========================================" echo "Your application is available at: http://$PUBLIC_IP" echo "You can test it by opening this URL in your browser" echo "" fi fi # Display service information echo "" echo "===========================================" echo "SERVICE INFORMATION" echo "===========================================" execute_command "aws ecs describe-services --cluster '$CLUSTER_NAME' --services '$SERVICE_NAME'" "Get service details" echo "" echo "===========================================" echo "TUTORIAL COMPLETED SUCCESSFULLY" echo "===========================================" echo "Resources created:" for resource in "${CREATED_RESOURCES[@]}"; do echo " - $resource" done if [[ -n "$PUBLIC_IP" && "$PUBLIC_IP" != "None" ]]; then echo "" echo "Application URL: http://$PUBLIC_IP" fi echo "" echo "Script completed at $(date)"

The following code example shows how to:

  • Create an ECS cluster

  • Create and monitor a service

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # ECS EC2 Launch Type Tutorial Script - UPDATED VERSION # This script demonstrates creating an ECS cluster, launching a container instance, # registering a task definition, and creating a service using the EC2 launch type. # Updated to match the tutorial draft with nginx web server and service creation. # # - UPDATED: Changed from sleep task to nginx web server with service set -e # Exit on any error # Configuration SCRIPT_NAME="ecs-ec2-tutorial" LOG_FILE="${SCRIPT_NAME}-$(date +%Y%m%d-%H%M%S).log" CLUSTER_NAME="tutorial-cluster-$(openssl rand -hex 4)" TASK_FAMILY="nginx-task-$(openssl rand -hex 4)" SERVICE_NAME="nginx-service-$(openssl rand -hex 4)" KEY_PAIR_NAME="ecs-tutorial-key-$(openssl rand -hex 4)" SECURITY_GROUP_NAME="ecs-tutorial-sg-$(openssl rand -hex 4)" # Tags PROJECT_TAG="doc-smith" TUTORIAL_TAG="ecs-ec2" # Get current AWS region dynamically AWS_REGION=$(aws configure get region || echo "us-east-1") # Resource tracking arrays CREATED_RESOURCES=() CLEANUP_ORDER=() # Logging function log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } # Error handling function handle_error() { local exit_code=$? log "ERROR: Script failed with exit code $exit_code" log "ERROR: Last command: $BASH_COMMAND" echo "" echo "===========================================" echo "ERROR OCCURRED - ATTEMPTING CLEANUP" echo "===========================================" echo "Resources created before error:" for resource in "${CREATED_RESOURCES[@]}"; do echo " - $resource" done cleanup_resources exit $exit_code } # Set error trap trap handle_error ERR # FIXED: Enhanced cleanup function with proper error handling and logging cleanup_resources() { log "Starting cleanup process..." local cleanup_errors=0 # Delete service first (this will stop tasks automatically) if [[ -n "${SERVICE_ARN:-}" ]]; then log "Updating service to desired count 0: $SERVICE_NAME" if ! aws ecs update-service --cluster "$CLUSTER_NAME" --service "$SERVICE_NAME" --desired-count 0 2>>"$LOG_FILE"; then log "WARNING: Failed to update service desired count to 0" ((cleanup_errors++)) else log "Waiting for service tasks to stop..." sleep 30 # Give time for tasks to stop fi log "Deleting service: $SERVICE_NAME" if ! aws ecs delete-service --cluster "$CLUSTER_NAME" --service "$SERVICE_NAME" 2>>"$LOG_FILE"; then log "WARNING: Failed to delete service $SERVICE_NAME" ((cleanup_errors++)) fi fi # Stop and delete any remaining tasks if [[ -n "${TASK_ARN:-}" ]]; then log "Stopping task: $TASK_ARN" if ! aws ecs stop-task --cluster "$CLUSTER_NAME" --task "$TASK_ARN" --reason "Tutorial cleanup" 2>>"$LOG_FILE"; then log "WARNING: Failed to stop task $TASK_ARN" ((cleanup_errors++)) else log "Waiting for task to stop..." if ! aws ecs wait tasks-stopped --cluster "$CLUSTER_NAME" --tasks "$TASK_ARN" 2>>"$LOG_FILE"; then log "WARNING: Task stop wait failed for $TASK_ARN" ((cleanup_errors++)) fi fi fi # Deregister task definition if [[ -n "${TASK_DEFINITION_ARN:-}" ]]; then log "Deregistering task definition: $TASK_DEFINITION_ARN" if ! aws ecs deregister-task-definition --task-definition "$TASK_DEFINITION_ARN" 2>>"$LOG_FILE"; then log "WARNING: Failed to deregister task definition $TASK_DEFINITION_ARN" ((cleanup_errors++)) fi fi # Terminate EC2 instance if [[ -n "${INSTANCE_ID:-}" ]]; then log "Terminating EC2 instance: $INSTANCE_ID" if ! aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" 2>>"$LOG_FILE"; then log "WARNING: Failed to terminate instance $INSTANCE_ID" ((cleanup_errors++)) else log "Waiting for instance to terminate..." if ! aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" 2>>"$LOG_FILE"; then log "WARNING: Instance termination wait failed for $INSTANCE_ID" ((cleanup_errors++)) fi fi fi # Delete security group with retry logic if [[ -n "${SECURITY_GROUP_ID:-}" ]]; then log "Deleting security group: $SECURITY_GROUP_ID" local retry_count=0 local max_retries=3 while [[ $retry_count -lt $max_retries ]]; do if aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID" 2>>"$LOG_FILE"; then log "Successfully deleted security group" break else ((retry_count++)) if [[ $retry_count -lt $max_retries ]]; then log "Retry $retry_count/$max_retries: Waiting 10 seconds before retrying security group deletion..." sleep 10 else log "ERROR: Failed to delete security group after $max_retries attempts" ((cleanup_errors++)) fi fi done fi # Delete key pair if [[ -n "${KEY_PAIR_NAME:-}" ]]; then log "Deleting key pair: $KEY_PAIR_NAME" if ! aws ec2 delete-key-pair --key-name "$KEY_PAIR_NAME" 2>>"$LOG_FILE"; then log "WARNING: Failed to delete key pair $KEY_PAIR_NAME" ((cleanup_errors++)) fi rm -f "${KEY_PAIR_NAME}.pem" 2>>"$LOG_FILE" || log "WARNING: Failed to remove local key file" fi # Delete ECS cluster if [[ -n "${CLUSTER_NAME:-}" ]]; then log "Deleting ECS cluster: $CLUSTER_NAME" if ! aws ecs delete-cluster --cluster "$CLUSTER_NAME" 2>>"$LOG_FILE"; then log "WARNING: Failed to delete cluster $CLUSTER_NAME" ((cleanup_errors++)) fi fi if [[ $cleanup_errors -eq 0 ]]; then log "Cleanup completed successfully" else log "Cleanup completed with $cleanup_errors warnings/errors. Check log file for details." fi } # Function to check prerequisites check_prerequisites() { log "Checking prerequisites..." # Check AWS CLI if ! command -v aws &> /dev/null; then log "ERROR: AWS CLI is not installed" exit 1 fi # Check AWS credentials if ! aws sts get-caller-identity &> /dev/null; then log "ERROR: AWS credentials not configured" exit 1 fi # Get caller identity CALLER_IDENTITY=$(aws sts get-caller-identity --output text --query 'Account') log "AWS Account: $CALLER_IDENTITY" log "AWS Region: $AWS_REGION" # Check for default VPC DEFAULT_VPC=$(aws ec2 describe-vpcs --filters "Name=is-default,Values=true" --query 'Vpcs[0].VpcId' --output text) if [[ "$DEFAULT_VPC" == "None" ]]; then log "ERROR: No default VPC found. Please create a VPC first." exit 1 fi log "Using default VPC: $DEFAULT_VPC" # Get default subnet DEFAULT_SUBNET=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$DEFAULT_VPC" "Name=default-for-az,Values=true" --query 'Subnets[0].SubnetId' --output text) if [[ "$DEFAULT_SUBNET" == "None" ]]; then log "ERROR: No default subnet found" exit 1 fi log "Using default subnet: $DEFAULT_SUBNET" log "Prerequisites check completed successfully" } # Function to create ECS cluster create_cluster() { log "Creating ECS cluster: $CLUSTER_NAME" CLUSTER_ARN=$(aws ecs create-cluster --cluster-name "$CLUSTER_NAME" --tags key=project,value=$PROJECT_TAG key=tutorial,value=$TUTORIAL_TAG --query 'cluster.clusterArn' --output text) if [[ -z "$CLUSTER_ARN" ]]; then log "ERROR: Failed to create cluster" exit 1 fi log "Created cluster: $CLUSTER_ARN" CREATED_RESOURCES+=("ECS Cluster: $CLUSTER_NAME") } # Function to create key pair create_key_pair() { log "Creating EC2 key pair: $KEY_PAIR_NAME" # FIXED: Set secure umask before key creation umask 077 aws ec2 create-key-pair --key-name "$KEY_PAIR_NAME" --query 'KeyMaterial' --output text > "${KEY_PAIR_NAME}.pem" chmod 400 "${KEY_PAIR_NAME}.pem" umask 022 # Reset umask log "Created key pair: $KEY_PAIR_NAME" CREATED_RESOURCES+=("EC2 Key Pair: $KEY_PAIR_NAME") aws ec2 create-tags --resources "$KEY_PAIR_NAME" --tags Key=project,Value=$PROJECT_TAG Key=tutorial,Value=$TUTORIAL_TAG 2>>"$LOG_FILE" || log "WARNING: Failed to tag key pair" } # Function to create security group create_security_group() { log "Creating security group: $SECURITY_GROUP_NAME" SECURITY_GROUP_ID=$(aws ec2 create-security-group \ --group-name "$SECURITY_GROUP_NAME" \ --description "ECS tutorial security group" \ --vpc-id "$DEFAULT_VPC" \ --tag-specifications "ResourceType=security-group,Tags=[{Key=project,Value=$PROJECT_TAG},{Key=tutorial,Value=$TUTORIAL_TAG}]" \ --query 'GroupId' --output text) if [[ -z "$SECURITY_GROUP_ID" ]]; then log "ERROR: Failed to create security group" exit 1 fi # Add HTTP access rule for nginx web server with restricted CIDR # SECURITY FIX: Restrict access to specific CIDR if available, otherwise document the risk log "WARNING: Security group allows HTTP (port 80) from 0.0.0.0/0 - restrict this in production" aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 80 \ --cidr "0.0.0.0/0" log "Created security group: $SECURITY_GROUP_ID" log "Added HTTP access on port 80" CREATED_RESOURCES+=("Security Group: $SECURITY_GROUP_ID") } # Function to get ECS optimized AMI get_ecs_ami() { log "Getting ECS-optimized AMI ID..." ECS_AMI_ID=$(aws ssm get-parameters \ --names /aws/service/ecs/optimized-ami/amazon-linux-2/recommended \ --query 'Parameters[0].Value' --output text | jq -r '.image_id') if [[ -z "$ECS_AMI_ID" ]]; then log "ERROR: Failed to get ECS-optimized AMI ID" exit 1 fi log "ECS-optimized AMI ID: $ECS_AMI_ID" } # Function to create IAM role for ECS instance (if it doesn't exist) ensure_ecs_instance_role() { log "Checking for ecsInstanceRole..." if ! aws iam get-role --role-name ecsInstanceRole &> /dev/null; then log "Creating ecsInstanceRole..." # Create trust policy cat > ecs-instance-trust-policy.json << 'EOF' { "Version":"2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF # SECURITY FIX: Validate JSON before using if ! jq empty ecs-instance-trust-policy.json 2>/dev/null; then log "ERROR: Invalid JSON in trust policy" rm -f ecs-instance-trust-policy.json exit 1 fi # Create role aws iam create-role \ --role-name ecsInstanceRole \ --assume-role-policy-document file://ecs-instance-trust-policy.json aws iam tag-role --role-name ecsInstanceRole --tags Key=project,Value=$PROJECT_TAG Key=tutorial,Value=$TUTORIAL_TAG # Attach managed policy aws iam attach-role-policy \ --role-name ecsInstanceRole \ --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role # Create instance profile aws iam create-instance-profile --instance-profile-name ecsInstanceRole # Add role to instance profile aws iam add-role-to-instance-profile \ --instance-profile-name ecsInstanceRole \ --role-name ecsInstanceRole # FIXED: Enhanced wait for role to be ready log "Waiting for IAM role to be ready..." aws iam wait role-exists --role-name ecsInstanceRole sleep 30 # Additional buffer for eventual consistency rm -f ecs-instance-trust-policy.json log "Created ecsInstanceRole" CREATED_RESOURCES+=("IAM Role: ecsInstanceRole") else log "ecsInstanceRole already exists" fi } # Function to launch container instance launch_container_instance() { log "Launching ECS container instance..." # Create user data script cat > ecs-user-data.sh << EOF #!/bin/bash echo ECS_CLUSTER=$CLUSTER_NAME >> /etc/ecs/ecs.config EOF # SECURITY FIX: Validate user data script before use if ! bash -n ecs-user-data.sh 2>/dev/null; then log "ERROR: Invalid user data script" rm -f ecs-user-data.sh exit 1 fi INSTANCE_ID=$(aws ec2 run-instances \ --image-id "$ECS_AMI_ID" \ --instance-type t3.micro \ --key-name "$KEY_PAIR_NAME" \ --security-group-ids "$SECURITY_GROUP_ID" \ --subnet-id "$DEFAULT_SUBNET" \ --iam-instance-profile Name=ecsInstanceRole \ --user-data file://ecs-user-data.sh \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=ecs-tutorial-instance},{Key=project,Value=$PROJECT_TAG},{Key=tutorial,Value=$TUTORIAL_TAG}]" \ --monitoring Enabled=false \ --metadata-options HttpTokens=required,HttpPutResponseHopLimit=1 \ --query 'Instances[0].InstanceId' --output text) if [[ -z "$INSTANCE_ID" ]]; then log "ERROR: Failed to launch EC2 instance" rm -f ecs-user-data.sh exit 1 fi log "Launched EC2 instance: $INSTANCE_ID" log "Instance metadata options: IMDSv2 enforced with hop limit 1" CREATED_RESOURCES+=("EC2 Instance: $INSTANCE_ID") # Wait for instance to be running log "Waiting for instance to be running..." aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" # Wait for ECS agent to register log "Waiting for ECS agent to register with cluster..." local max_attempts=30 local attempt=0 while [[ $attempt -lt $max_attempts ]]; do CONTAINER_INSTANCES=$(aws ecs list-container-instances --cluster "$CLUSTER_NAME" --query 'containerInstanceArns' --output text) if [[ -n "$CONTAINER_INSTANCES" && "$CONTAINER_INSTANCES" != "None" ]]; then log "Container instance registered successfully" break fi attempt=$((attempt + 1)) log "Waiting for container instance registration... (attempt $attempt/$max_attempts)" sleep 10 done if [[ $attempt -eq $max_attempts ]]; then log "ERROR: Container instance failed to register within expected time" rm -f ecs-user-data.sh exit 1 fi rm -f ecs-user-data.sh } # Function to register task definition register_task_definition() { log "Creating task definition..." # Create nginx task definition JSON matching the tutorial cat > task-definition.json << 'EOF' { "family": "TASK_FAMILY_PLACEHOLDER", "containerDefinitions": [ { "name": "nginx", "image": "public.ecr.aws/docker/library/nginx:latest", "cpu": 256, "memory": 512, "essential": true, "portMappings": [ { "containerPort": 80, "hostPort": 80, "protocol": "tcp" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/nginx-task", "awslogs-region": "REGION_PLACEHOLDER", "awslogs-stream-prefix": "ecs" } } } ], "requiresCompatibilities": ["EC2"], "networkMode": "bridge", "tags": [ { "key": "project", "value": "PROJECT_TAG_PLACEHOLDER" }, { "key": "tutorial", "value": "TUTORIAL_TAG_PLACEHOLDER" } ] } EOF # Replace placeholders securely sed -i "s|TASK_FAMILY_PLACEHOLDER|$TASK_FAMILY|g" task-definition.json sed -i "s|REGION_PLACEHOLDER|$AWS_REGION|g" task-definition.json sed -i "s|PROJECT_TAG_PLACEHOLDER|$PROJECT_TAG|g" task-definition.json sed -i "s|TUTORIAL_TAG_PLACEHOLDER|$TUTORIAL_TAG|g" task-definition.json # FIXED: Validate JSON before registration if ! jq empty task-definition.json 2>/dev/null; then log "ERROR: Invalid JSON in task definition" rm -f task-definition.json exit 1 fi TASK_DEFINITION_ARN=$(aws ecs register-task-definition \ --cli-input-json file://task-definition.json \ --query 'taskDefinition.taskDefinitionArn' --output text) if [[ -z "$TASK_DEFINITION_ARN" ]]; then log "ERROR: Failed to register task definition" rm -f task-definition.json exit 1 fi log "Registered task definition: $TASK_DEFINITION_ARN" log "Task definition includes CloudWatch Logs configuration for monitoring" CREATED_RESOURCES+=("Task Definition: $TASK_DEFINITION_ARN") rm -f task-definition.json } # Function to create service create_service() { log "Creating ECS service..." SERVICE_ARN=$(aws ecs create-service \ --cluster "$CLUSTER_NAME" \ --service-name "$SERVICE_NAME" \ --task-definition "$TASK_FAMILY" \ --desired-count 1 \ --tags key=project,value=$PROJECT_TAG key=tutorial,value=$TUTORIAL_TAG \ --query 'service.serviceArn' --output text) if [[ -z "$SERVICE_ARN" ]]; then log "ERROR: Failed to create service" exit 1 fi log "Created service: $SERVICE_ARN" CREATED_RESOURCES+=("ECS Service: $SERVICE_NAME") # Wait for service to be stable log "Waiting for service to be stable..." aws ecs wait services-stable --cluster "$CLUSTER_NAME" --services "$SERVICE_NAME" log "Service is now stable and running" # Get the task ARN for monitoring TASK_ARN=$(aws ecs list-tasks --cluster "$CLUSTER_NAME" --service-name "$SERVICE_NAME" --query 'taskArns[0]' --output text) if [[ -n "$TASK_ARN" && "$TASK_ARN" != "None" ]]; then log "Service task: $TASK_ARN" CREATED_RESOURCES+=("ECS Task: $TASK_ARN") fi } # Function to demonstrate monitoring and testing demonstrate_monitoring() { log "Demonstrating monitoring capabilities..." # List services log "Listing services in cluster:" aws ecs list-services --cluster "$CLUSTER_NAME" --output table # Describe service log "Service details:" aws ecs describe-services --cluster "$CLUSTER_NAME" --services "$SERVICE_NAME" --output table --query 'services[0].{ServiceName:serviceName,Status:status,RunningCount:runningCount,DesiredCount:desiredCount,TaskDefinition:taskDefinition}' # List tasks log "Listing tasks in service:" aws ecs list-tasks --cluster "$CLUSTER_NAME" --service-name "$SERVICE_NAME" --output table # Describe task if [[ -n "$TASK_ARN" && "$TASK_ARN" != "None" ]]; then log "Task details:" aws ecs describe-tasks --cluster "$CLUSTER_NAME" --tasks "$TASK_ARN" --output table --query 'tasks[0].{TaskArn:taskArn,LastStatus:lastStatus,DesiredStatus:desiredStatus,CreatedAt:createdAt}' fi # List container instances log "Container instances in cluster:" aws ecs list-container-instances --cluster "$CLUSTER_NAME" --output table # Describe container instance CONTAINER_INSTANCE_ARN=$(aws ecs list-container-instances --cluster "$CLUSTER_NAME" --query 'containerInstanceArns[0]' --output text) if [[ -n "$CONTAINER_INSTANCE_ARN" && "$CONTAINER_INSTANCE_ARN" != "None" ]]; then log "Container instance details:" aws ecs describe-container-instances --cluster "$CLUSTER_NAME" --container-instances "$CONTAINER_INSTANCE_ARN" --output table --query 'containerInstances[0].{Arn:containerInstanceArn,Status:status,RunningTasks:runningTasksCount,PendingTasks:pendingTasksCount}' fi # Test the nginx web server log "Testing nginx web server..." PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$INSTANCE_ID" --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) if [[ -n "$PUBLIC_IP" && "$PUBLIC_IP" != "None" ]]; then log "Container instance public IP: $PUBLIC_IP" log "Testing HTTP connection to nginx..." # Wait a moment for nginx to be fully ready sleep 10 if curl -s --connect-timeout 10 "http://$PUBLIC_IP" | grep -q "Welcome to nginx"; then log "SUCCESS: Nginx web server is responding correctly" echo "" echo "===========================================" echo "WEB SERVER TEST SUCCESSFUL" echo "===========================================" echo "You can access your nginx web server at: http://$PUBLIC_IP" echo "The nginx welcome page should be visible in your browser." else log "WARNING: Nginx web server may not be fully ready yet. Try accessing http://$PUBLIC_IP in a few minutes." fi else log "WARNING: Could not retrieve public IP address" fi } # Main execution main() { log "Starting ECS EC2 Launch Type Tutorial (UPDATED VERSION)" log "Log file: $LOG_FILE" log "Security improvements: IMDSv2 enforced, JSON validation, input sanitization, CloudWatch Logs configured" check_prerequisites create_cluster create_key_pair create_security_group get_ecs_ami ensure_ecs_instance_role launch_container_instance register_task_definition create_service demonstrate_monitoring log "Tutorial completed successfully!" echo "" echo "===========================================" echo "TUTORIAL COMPLETED SUCCESSFULLY" echo "===========================================" echo "Resources created:" for resource in "${CREATED_RESOURCES[@]}"; do echo " - $resource" done echo "" echo "The nginx service will continue running and maintain the desired task count." echo "You can monitor the service status using:" echo " aws ecs describe-services --cluster $CLUSTER_NAME --services $SERVICE_NAME" echo "" if [[ -n "${PUBLIC_IP:-}" ]]; then echo "Access your web server at: http://$PUBLIC_IP" echo "" fi echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" log "Auto-confirming cleanup - proceeding with resource cleanup" cleanup_resources log "All resources have been cleaned up" } # Run main function main "$@"

The following code example shows how to:

  • Set up networking components

  • Create a DB subnet group

  • Create a DB instance

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Script to create an Amazon RDS DB instance # This script follows the tutorial at https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_CreateDBInstance.html # Set up logging LOG_FILE="rds_creation_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting RDS DB instance creation script - $(date)" echo "All actions will be logged to $LOG_FILE" echo "==============================================" # Function to check for errors in command output check_error() { local output=$1 local cmd=$2 if echo "$output" | grep -i "error" > /dev/null; then echo "ERROR: Command failed: $cmd" echo "$output" cleanup_on_error exit 1 fi } # Function to clean up resources on error cleanup_on_error() { echo "Error encountered. Attempting to clean up resources..." if [ -n "$DB_INSTANCE_ID" ]; then echo "Deleting DB instance $DB_INSTANCE_ID..." aws rds delete-db-instance --db-instance-identifier "$DB_INSTANCE_ID" --skip-final-snapshot echo "Waiting for DB instance to be deleted..." aws rds wait db-instance-deleted --db-instance-identifier "$DB_INSTANCE_ID" fi if [ -n "$DB_SUBNET_GROUP_NAME" ] && [ "$CREATED_SUBNET_GROUP" = "true" ]; then echo "Deleting DB subnet group $DB_SUBNET_GROUP_NAME..." aws rds delete-db-subnet-group --db-subnet-group-name "$DB_SUBNET_GROUP_NAME" fi if [ -n "$SECURITY_GROUP_ID" ] && [ "$CREATED_SECURITY_GROUP" = "true" ]; then echo "Deleting security group $SECURITY_GROUP_ID..." aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID" fi echo "Cleanup completed." } # Generate a random identifier for resources RANDOM_ID=$(openssl rand -hex 4) DB_INSTANCE_ID="mydb-${RANDOM_ID}" DB_SUBNET_GROUP_NAME="mydbsubnet-${RANDOM_ID}" SECURITY_GROUP_NAME="mydbsg-${RANDOM_ID}" # Track created resources CREATED_SECURITY_GROUP="false" CREATED_SUBNET_GROUP="false" # Array to store created resources for display declare -a CREATED_RESOURCES echo "Step 1: Checking for default VPC..." VPC_OUTPUT=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true") check_error "$VPC_OUTPUT" "aws ec2 describe-vpcs" # Extract VPC ID VPC_ID=$(echo "$VPC_OUTPUT" | grep -o '"VpcId": "[^"]*' | cut -d'"' -f4) if [ -z "$VPC_ID" ]; then echo "No default VPC found. Please create a VPC before running this script." exit 1 fi echo "Using VPC: $VPC_ID" echo "Step 2: Getting subnets from the VPC..." SUBNET_OUTPUT=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID") check_error "$SUBNET_OUTPUT" "aws ec2 describe-subnets" # Extract subnet IDs (we need at least 2 in different AZs) SUBNET_IDS=($(echo "$SUBNET_OUTPUT" | grep -o '"SubnetId": "[^"]*' | cut -d'"' -f4)) if [ ${#SUBNET_IDS[@]} -lt 2 ]; then echo "Error: Need at least 2 subnets in different AZs. Found ${#SUBNET_IDS[@]} subnets." exit 1 fi echo "Found ${#SUBNET_IDS[@]} subnets: ${SUBNET_IDS[*]}" echo "Step 3: Creating security group for RDS..." SG_OUTPUT=$(aws ec2 create-security-group \ --group-name "$SECURITY_GROUP_NAME" \ --description "Security group for RDS database access" \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=rds-gs}]') check_error "$SG_OUTPUT" "aws ec2 create-security-group" SECURITY_GROUP_ID=$(echo "$SG_OUTPUT" | grep -o '"GroupId": "[^"]*' | cut -d'"' -f4) CREATED_SECURITY_GROUP="true" CREATED_RESOURCES+=("Security Group: $SECURITY_GROUP_ID ($SECURITY_GROUP_NAME)") echo "Created security group: $SECURITY_GROUP_ID" echo "Step 4: Adding inbound rule to security group..." # Note: In a production environment, you should restrict this to specific IP ranges # We're using the local machine's IP address for this example MY_IP=$(curl -s https://checkip.amazonaws.com) check_error "$MY_IP" "curl -s https://checkip.amazonaws.com" INGRESS_OUTPUT=$(aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 3306 \ --cidr "${MY_IP}/32") check_error "$INGRESS_OUTPUT" "aws ec2 authorize-security-group-ingress" echo "Added inbound rule to allow MySQL connections from ${MY_IP}/32" echo "Step 5: Creating DB subnet group..." # Select the first two subnets for the DB subnet group SUBNET1=${SUBNET_IDS[0]} SUBNET2=${SUBNET_IDS[1]} SUBNET_GROUP_OUTPUT=$(aws rds create-db-subnet-group \ --db-subnet-group-name "$DB_SUBNET_GROUP_NAME" \ --db-subnet-group-description "Subnet group for RDS tutorial" \ --subnet-ids "$SUBNET1" "$SUBNET2" \ --tags Key=project,Value=doc-smith Key=tutorial,Value=rds-gs) check_error "$SUBNET_GROUP_OUTPUT" "aws rds create-db-subnet-group" CREATED_SUBNET_GROUP="true" CREATED_RESOURCES+=("DB Subnet Group: $DB_SUBNET_GROUP_NAME") echo "Created DB subnet group: $DB_SUBNET_GROUP_NAME" echo "Step 6: Creating a secure password in AWS Secrets Manager..." SECRET_NAME="rds-db-credentials-${RANDOM_ID}" SECRET_OUTPUT=$(aws secretsmanager create-secret \ --name "$SECRET_NAME" \ --description "RDS DB credentials for $DB_INSTANCE_ID" \ --secret-string '{"username":"adminuser","password":"'"$(openssl rand -base64 16)"'"}' \ --tags Key=project,Value=doc-smith Key=tutorial,Value=rds-gs) check_error "$SECRET_OUTPUT" "aws secretsmanager create-secret" SECRET_ARN=$(echo "$SECRET_OUTPUT" | grep -o '"ARN": "[^"]*' | cut -d'"' -f4) CREATED_RESOURCES+=("Secret: $SECRET_ARN ($SECRET_NAME)") echo "Created secret: $SECRET_NAME" echo "Step 7: Retrieving the username and password from the secret..." SECRET_VALUE_OUTPUT=$(aws secretsmanager get-secret-value --secret-id "$SECRET_NAME" --query 'SecretString' --output text) check_error "$SECRET_VALUE_OUTPUT" "aws secretsmanager get-secret-value" DB_USERNAME=$(echo "$SECRET_VALUE_OUTPUT" | grep -o '"username":"[^"]*' | cut -d'"' -f4) DB_PASSWORD=$(echo "$SECRET_VALUE_OUTPUT" | grep -o '"password":"[^"]*' | cut -d'"' -f4) echo "Retrieved database credentials" echo "Step 8: Creating RDS DB instance..." echo "This may take several minutes..." DB_OUTPUT=$(aws rds create-db-instance \ --db-instance-identifier "$DB_INSTANCE_ID" \ --db-instance-class db.t3.micro \ --engine mysql \ --master-username "$DB_USERNAME" \ --master-user-password "$DB_PASSWORD" \ --allocated-storage 20 \ --vpc-security-group-ids "$SECURITY_GROUP_ID" \ --db-subnet-group-name "$DB_SUBNET_GROUP_NAME" \ --backup-retention-period 7 \ --no-publicly-accessible \ --no-multi-az \ --tags Key=project,Value=doc-smith Key=tutorial,Value=rds-gs) check_error "$DB_OUTPUT" "aws rds create-db-instance" CREATED_RESOURCES+=("DB Instance: $DB_INSTANCE_ID") echo "DB instance creation initiated: $DB_INSTANCE_ID" echo "Waiting for DB instance to become available..." echo "This may take 5-10 minutes..." aws rds wait db-instance-available --db-instance-identifier "$DB_INSTANCE_ID" DB_STATUS=$? if [ $DB_STATUS -ne 0 ]; then echo "Error waiting for DB instance to become available" cleanup_on_error exit 1 fi echo "DB instance is now available!" echo "Step 9: Getting connection information..." ENDPOINT_INFO=$(aws rds describe-db-instances \ --db-instance-identifier "$DB_INSTANCE_ID" \ --query 'DBInstances[0].[Endpoint.Address,Endpoint.Port,MasterUsername]' \ --output text) check_error "$ENDPOINT_INFO" "aws rds describe-db-instances" DB_ENDPOINT=$(echo "$ENDPOINT_INFO" | awk '{print $1}') DB_PORT=$(echo "$ENDPOINT_INFO" | awk '{print $2}') DB_USER=$(echo "$ENDPOINT_INFO" | awk '{print $3}') echo "==============================================" echo "DB Instance successfully created!" echo "==============================================" echo "Connection Information:" echo " Endpoint: $DB_ENDPOINT" echo " Port: $DB_PORT" echo " Username: $DB_USER" echo " Password: [Stored in AWS Secrets Manager - $SECRET_NAME]" echo "" echo "To connect using the mysql client:" echo "mysql -h $DB_ENDPOINT -P $DB_PORT -u $DB_USER -p" echo "==============================================" echo "" echo "Resources created:" for resource in "${CREATED_RESOURCES[@]}"; do echo " - $resource" done echo "" # Ask user if they want to clean up resources echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ $CLEANUP_CHOICE =~ ^[Yy] ]]; then echo "Starting cleanup process..." echo "Step 1: Deleting DB instance $DB_INSTANCE_ID..." aws rds delete-db-instance --db-instance-identifier "$DB_INSTANCE_ID" --skip-final-snapshot echo "Waiting for DB instance to be deleted..." aws rds wait db-instance-deleted --db-instance-identifier "$DB_INSTANCE_ID" echo "Step 2: Deleting secret $SECRET_NAME..." aws secretsmanager delete-secret --secret-id "$SECRET_NAME" --force-delete-without-recovery echo "Step 3: Deleting DB subnet group $DB_SUBNET_GROUP_NAME..." aws rds delete-db-subnet-group --db-subnet-group-name "$DB_SUBNET_GROUP_NAME" echo "Step 4: Deleting security group $SECURITY_GROUP_ID..." aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID" echo "Cleanup completed successfully!" else echo "Skipping cleanup. Resources will remain in your AWS account." echo "To clean up later, you'll need to delete these resources manually." fi echo "Script completed successfully!"

The following code example shows how to:

  • Create an EBS volume

  • Check volume status

  • Create an EC2 instance (optional)

  • Attach a volume to an EC2 instance

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Script to create and manage Amazon EBS volumes # This script demonstrates how to create an EBS volume and attach it to an EC2 instance # It can also create a test EC2 instance if needed # Set up logging LOG_FILE="ebs-volume-creation.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting EBS volume creation script at $(date)" echo "==============================================" # Function to handle errors handle_error() { echo "ERROR: $1" echo "Resources created:" if [ -n "$VOLUME_ID" ]; then echo "- EBS Volume: $VOLUME_ID" fi if [ -n "$INSTANCE_ID" ]; then echo "- EC2 Instance: $INSTANCE_ID" fi if [ -n "$SG_ID" ]; then echo "- Security Group: $SG_ID" fi echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "An error occurred. Do you want to clean up created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then cleanup_resources else echo "Resources were not cleaned up. You will need to delete them manually." fi exit 1 } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources..." if [ -n "$VOLUME_ID" ] && [ "$ATTACHED" = true ]; then echo "Detaching volume $VOLUME_ID..." aws ec2 detach-volume --volume-id "$VOLUME_ID" # Wait for volume to be detached echo "Waiting for volume to be detached..." aws ec2 wait volume-available --volume-ids "$VOLUME_ID" fi if [ -n "$VOLUME_ID" ]; then echo "Deleting volume $VOLUME_ID..." aws ec2 delete-volume --volume-id "$VOLUME_ID" fi if [ -n "$INSTANCE_ID" ] && [ "$CREATED_INSTANCE" = true ]; then echo "Terminating instance $INSTANCE_ID..." aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" echo "Waiting for instance to terminate..." aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" fi # Clean up security group if created if [ -n "$SG_ID" ] && [ "$CREATED_INSTANCE" = true ]; then echo "Deleting security group $SG_ID..." # Wait a bit for instance termination to complete sleep 10 aws ec2 delete-security-group --group-id "$SG_ID" 2>/dev/null || echo "Security group may have dependencies, delete manually if needed" fi echo "Cleanup completed." } # Function to get available instance type get_available_instance_type() { local region=$1 # Try instance types in order of preference (cheapest first) local instance_types=("t3.nano" "t3.micro" "t2.micro" "t2.nano") for instance_type in "${instance_types[@]}"; do local available=$(aws ec2 describe-instance-type-offerings \ --region "$region" \ --filters "Name=instance-type,Values=$instance_type" \ --query "length(InstanceTypeOfferings)" \ --output text) if [ "$available" -gt 0 ]; then echo "$instance_type" return 0 fi done # If none of the preferred types are available, get any available type local fallback_type=$(aws ec2 describe-instance-type-offerings \ --region "$region" \ --query "InstanceTypeOfferings[0].InstanceType" \ --output text) if [ "$fallback_type" != "None" ] && [ -n "$fallback_type" ]; then echo "$fallback_type" return 0 fi return 1 } # Get current region REGION=$(aws configure get region) if [ -z "$REGION" ]; then REGION=$(aws ec2 describe-availability-zones --query "AvailabilityZones[0].RegionName" --output text) fi echo "Using region: $REGION" # Get available Availability Zones echo "Retrieving available Availability Zones..." AZ=$(aws ec2 describe-availability-zones --filters "Name=state,Values=available" --query "AvailabilityZones[0].ZoneName" --output text) if [ -z "$AZ" ]; then handle_error "Failed to retrieve Availability Zones" fi echo "Using Availability Zone: $AZ" # Create a gp3 volume echo "Creating a 10 GiB gp3 volume in $AZ..." VOLUME_ID=$(aws ec2 create-volume \ --volume-type gp3 \ --size 10 \ --availability-zone "$AZ" \ --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=EBSTutorialVolume},{Key=Purpose,Value=Tutorial},{Key=project,Value=doc-smith},{Key=tutorial,Value=ebs-gs-volumes}]' \ --query 'VolumeId' \ --output text) if [ -z "$VOLUME_ID" ]; then handle_error "Failed to create EBS volume" fi echo "Volume created with ID: $VOLUME_ID" # Wait for volume to become available echo "Waiting for volume to become available..." aws ec2 wait volume-available --volume-ids "$VOLUME_ID" if [ $? -ne 0 ]; then handle_error "Volume did not become available" fi # Check volume details echo "Retrieving volume details..." aws ec2 describe-volumes --volume-ids "$VOLUME_ID" # Ask if user wants to attach the volume to an instance echo "" echo "===========================================" echo "VOLUME ATTACHMENT" echo "===========================================" echo "Do you want to attach this volume to an EC2 instance? (y/n): " read -r ATTACH_CHOICE ATTACHED=false CREATED_INSTANCE=false INSTANCE_ID="" SG_ID="" if [[ "$ATTACH_CHOICE" =~ ^[Yy]$ ]]; then # List available instances in the same AZ echo "Retrieving EC2 instances in $AZ..." INSTANCES_COUNT=$(aws ec2 describe-instances \ --filters "Name=availability-zone,Values=$AZ" "Name=instance-state-name,Values=running" \ --query "length(Reservations[].Instances[])" \ --output text) # Check if there are any running instances in the AZ if [ "$INSTANCES_COUNT" -eq 0 ]; then echo "No running instances found in $AZ." echo "" echo "Would you like to create a test EC2 instance? (y/n): " read -r CREATE_INSTANCE_CHOICE if [[ "$CREATE_INSTANCE_CHOICE" =~ ^[Yy]$ ]]; then # Get available instance type echo "Finding available instance type for region $REGION..." INSTANCE_TYPE=$(get_available_instance_type "$REGION") if [ $? -ne 0 ] || [ -z "$INSTANCE_TYPE" ]; then handle_error "No suitable instance type found in region $REGION" fi echo "Using instance type: $INSTANCE_TYPE" # Get the latest Amazon Linux 2 AMI echo "Finding the latest Amazon Linux 2 AMI..." AMI_ID=$(aws ec2 describe-images \ --owners amazon \ --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" "Name=state,Values=available" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" \ --output text) if [ -z "$AMI_ID" ]; then handle_error "Failed to find a suitable AMI" fi echo "Using AMI: $AMI_ID" # Check if a default VPC exists DEFAULT_VPC_ID=$(aws ec2 describe-vpcs \ --filters "Name=isDefault,Values=true" \ --query "Vpcs[0].VpcId" \ --output text) if [ "$DEFAULT_VPC_ID" = "None" ] || [ -z "$DEFAULT_VPC_ID" ]; then handle_error "No default VPC found. Please create a VPC and subnet before running this script." fi # Get a subnet in the selected AZ SUBNET_ID=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=$DEFAULT_VPC_ID" "Name=availability-zone,Values=$AZ" \ --query "Subnets[0].SubnetId" \ --output text) if [ -z "$SUBNET_ID" ] || [ "$SUBNET_ID" = "None" ]; then handle_error "No subnet found in $AZ. Please create a subnet before running this script." fi echo "Using subnet: $SUBNET_ID" # Create a security group that allows SSH SG_NAME="EBSTutorialSG-$(date +%s)" SG_ID=$(aws ec2 create-security-group \ --group-name "$SG_NAME" \ --description "Security group for EBS tutorial" \ --vpc-id "$DEFAULT_VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=ebs-gs-volumes}]' \ --query "GroupId" \ --output text) if [ -z "$SG_ID" ]; then handle_error "Failed to create security group" fi echo "Created security group: $SG_ID" # Add a rule to allow SSH aws ec2 authorize-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port 22 \ --cidr 0.0.0.0/0 echo "Added SSH rule to security group" # Create the instance echo "Creating EC2 instance in $AZ with instance type $INSTANCE_TYPE..." INSTANCE_ID=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --instance-type "$INSTANCE_TYPE" \ --subnet-id "$SUBNET_ID" \ --security-group-ids "$SG_ID" \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=EBSTutorialInstance},{Key=Purpose,Value=Tutorial},{Key=project,Value=doc-smith},{Key=tutorial,Value=ebs-gs-volumes}]' \ --query "Instances[0].InstanceId" \ --output text) if [ -z "$INSTANCE_ID" ]; then handle_error "Failed to create EC2 instance" fi CREATED_INSTANCE=true echo "Instance created with ID: $INSTANCE_ID" # Wait for the instance to be running echo "Waiting for instance to be running..." aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" # Wait a bit more for the instance to initialize echo "Waiting for instance initialization (30 seconds)..." sleep 30 else echo "Skipping instance creation and volume attachment." INSTANCE_ID="" fi else # Display available instances echo "Available instances in $AZ:" aws ec2 describe-instances \ --filters "Name=availability-zone,Values=$AZ" "Name=instance-state-name,Values=running" \ --query "Reservations[*].Instances[*].[InstanceId,Tags[?Key=='Name'].Value|[0],InstanceType]" \ --output table # Ask for instance ID echo "" echo "Enter the instance ID to attach the volume to (or press Enter to skip): " read -r INSTANCE_ID fi if [ -n "$INSTANCE_ID" ]; then # Attach volume to the instance echo "Attaching volume $VOLUME_ID to instance $INSTANCE_ID..." ATTACH_RESULT=$(aws ec2 attach-volume \ --volume-id "$VOLUME_ID" \ --instance-id "$INSTANCE_ID" \ --device "/dev/sdf" \ --query 'State' \ --output text) if [ $? -ne 0 ] || [ -z "$ATTACH_RESULT" ]; then handle_error "Failed to attach volume to instance" fi ATTACHED=true echo "Volume attached successfully. Device: /dev/sdf" # Verify attachment echo "Verifying attachment..." aws ec2 describe-volumes \ --volume-ids "$VOLUME_ID" \ --query "Volumes[0].Attachments" else echo "Skipping volume attachment." fi else echo "Skipping volume attachment." fi # Display summary of created resources echo "" echo "===========================================" echo "RESOURCE SUMMARY" echo "===========================================" echo "Created resources:" echo "- EBS Volume: $VOLUME_ID" if [ "$ATTACHED" = true ]; then echo " - Attached to: $INSTANCE_ID as /dev/sdf" fi if [ "$CREATED_INSTANCE" = true ]; then echo "- EC2 Instance: $INSTANCE_ID (type: $INSTANCE_TYPE)" fi if [ -n "$SG_ID" ]; then echo "- Security Group: $SG_ID" fi # Ask if user wants to clean up resources echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then cleanup_resources else echo "" echo "Resources were not cleaned up. You can manually delete them later." if [ -n "$VOLUME_ID" ]; then if [ "$ATTACHED" = true ]; then echo "To detach the volume:" echo " aws ec2 detach-volume --volume-id $VOLUME_ID" fi echo "To delete the volume:" echo " aws ec2 delete-volume --volume-id $VOLUME_ID" fi if [ "$CREATED_INSTANCE" = true ]; then echo "To terminate the instance:" echo " aws ec2 terminate-instances --instance-ids $INSTANCE_ID" fi if [ -n "$SG_ID" ]; then echo "To delete the security group (after instance termination):" echo " aws ec2 delete-security-group --group-id $SG_ID" fi fi echo "" echo "Script completed at $(date)" echo "=============================================="

The following code example shows how to:

  • Create a service network

  • Create a service

  • List available VPCs

  • List security groups for the selected VPC

  • List service associations

  • List VPC associations

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # VPC Lattice Service Network Tutorial Script # This script demonstrates how to create and manage a VPC Lattice service network set -euo pipefail # Set up logging with secure permissions LOG_FILE="vpc-lattice-tutorial.log" touch "$LOG_FILE" chmod 600 "$LOG_FILE" echo "Starting VPC Lattice tutorial script at $(date)" > "$LOG_FILE" # Function to log commands and their output log_command() { local cmd="$1" echo "$(date): Running command: $cmd" >> "$LOG_FILE" eval "$cmd" 2>&1 | tee -a "$LOG_FILE" return "${PIPESTATUS[0]}" } # Function to check for errors check_error() { if [ "$1" -ne 0 ]; then echo "ERROR: Command failed with exit code $1" | tee -a "$LOG_FILE" echo "See $LOG_FILE for details" exit "$1" fi } # Function to validate AWS CLI is available check_aws_cli() { if ! command -v aws &> /dev/null; then echo "ERROR: AWS CLI is not installed or not in PATH" | tee -a "$LOG_FILE" exit 1 fi } # Function to validate input parameters validate_input() { local input="$1" local param_name="$2" if [[ -z "$input" ]]; then echo "ERROR: $param_name is empty" | tee -a "$LOG_FILE" return 1 fi # Validate against common injection patterns if [[ "$input" =~ [\;\$\`\|\&\<\>\(\)\{\}] ]]; then echo "ERROR: $param_name contains invalid characters" | tee -a "$LOG_FILE" return 1 fi return 0 } # Function to wait for a resource to be in the desired state wait_for_resource() { local resource_type="$1" local resource_id="$2" local desired_status="$3" local command="$4" local max_attempts=30 local attempt=1 local status="" validate_input "$resource_type" "resource_type" || return 1 validate_input "$resource_id" "resource_id" || return 1 validate_input "$desired_status" "desired_status" || return 1 echo "Waiting for $resource_type $resource_id to be in state $desired_status..." | tee -a "$LOG_FILE" while [ "$attempt" -le "$max_attempts" ]; do echo "Attempt $attempt of $max_attempts..." >> "$LOG_FILE" # Run the command to get the status and capture the output status_output=$(eval "$command" 2>&1) || true echo "$status_output" >> "$LOG_FILE" # For service networks, they do not have a status field in the output # We'll consider them active if we can retrieve them if [[ "$resource_type" == "Service Network" ]]; then if [[ "$status_output" == *"$resource_id"* ]]; then echo "$resource_type $resource_id is now active" | tee -a "$LOG_FILE" return 0 fi else # For other resources, extract the status field status=$(echo "$status_output" | grep -i "status" | awk -F'"' '{print $4}' || true) echo "Current status: $status" >> "$LOG_FILE" if [[ "$status" == "$desired_status" ]]; then echo "$resource_type $resource_id is now in state $desired_status" | tee -a "$LOG_FILE" return 0 elif [[ "$status" == *"FAIL"* ]]; then echo "ERROR: $resource_type $resource_id failed to reach desired state. Current status: $status" | tee -a "$LOG_FILE" return 1 fi fi echo "Waiting for status change... (attempt $attempt/$max_attempts)" >> "$LOG_FILE" sleep 10 ((attempt++)) done echo "ERROR: Timed out waiting for $resource_type $resource_id to reach state $desired_status" | tee -a "$LOG_FILE" return 1 } # Cleanup function for trap cleanup() { local exit_code=$? echo "Script interrupted or failed. Cleaning up..." | tee -a "$LOG_FILE" exit "$exit_code" } trap cleanup EXIT INT TERM # Check prerequisites check_aws_cli # Generate a random identifier for resource names RANDOM_ID=$(openssl rand -hex 4) SERVICE_NETWORK_NAME="lattice-network-${RANDOM_ID}" SERVICE_NAME="lattice-service-${RANDOM_ID}" # Store created resources for cleanup declare -a CREATED_RESOURCES echo "=== VPC Lattice Service Network Tutorial ===" | tee -a "$LOG_FILE" echo "Random ID for this session: ${RANDOM_ID}" | tee -a "$LOG_FILE" # Step 1: Create a VPC Lattice service network echo -e "\n=== Step 1: Creating a VPC Lattice service network ===" | tee -a "$LOG_FILE" echo "Creating service network: $SERVICE_NETWORK_NAME" | tee -a "$LOG_FILE" SERVICE_NETWORK_OUTPUT=$(log_command "aws vpc-lattice create-service-network --name '$SERVICE_NETWORK_NAME' --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-vpc-lattice-gs --output json") check_error $? # Extract the service network ID using jq for safety SERVICE_NETWORK_ID=$(echo "$SERVICE_NETWORK_OUTPUT" | jq -r '.id // empty' 2>/dev/null || true) if [ -z "$SERVICE_NETWORK_ID" ]; then echo "ERROR: Failed to extract service network ID" | tee -a "$LOG_FILE" exit 1 fi validate_input "$SERVICE_NETWORK_ID" "SERVICE_NETWORK_ID" || exit 1 echo "Service network created with ID: $SERVICE_NETWORK_ID" | tee -a "$LOG_FILE" CREATED_RESOURCES+=("Service Network: $SERVICE_NETWORK_ID") # Wait for the service network to be active wait_for_resource "Service Network" "$SERVICE_NETWORK_ID" "ACTIVE" "aws vpc-lattice get-service-network --service-network-identifier '$SERVICE_NETWORK_ID' --output json" check_error $? # Step 2: Create a VPC Lattice service echo -e "\n=== Step 2: Creating a VPC Lattice service ===" | tee -a "$LOG_FILE" echo "Creating service: $SERVICE_NAME" | tee -a "$LOG_FILE" SERVICE_OUTPUT=$(log_command "aws vpc-lattice create-service --name '$SERVICE_NAME' --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-vpc-lattice-gs --output json") check_error $? # Extract the service ID using jq for safety SERVICE_ID=$(echo "$SERVICE_OUTPUT" | jq -r '.id // empty' 2>/dev/null || true) if [ -z "$SERVICE_ID" ]; then echo "ERROR: Failed to extract service ID" | tee -a "$LOG_FILE" exit 1 fi validate_input "$SERVICE_ID" "SERVICE_ID" || exit 1 echo "Service created with ID: $SERVICE_ID" | tee -a "$LOG_FILE" CREATED_RESOURCES+=("Service: $SERVICE_ID") # Wait for the service to be active wait_for_resource "Service" "$SERVICE_ID" "ACTIVE" "aws vpc-lattice get-service --service-identifier '$SERVICE_ID' --output json" check_error $? # Step 3: Associate the service with the service network echo -e "\n=== Step 3: Associating service with service network ===" | tee -a "$LOG_FILE" SERVICE_ASSOC_OUTPUT=$(log_command "aws vpc-lattice create-service-network-service-association --service-identifier '$SERVICE_ID' --service-network-identifier '$SERVICE_NETWORK_ID' --output json") check_error $? # Extract the service association ID using jq for safety SERVICE_ASSOC_ID=$(echo "$SERVICE_ASSOC_OUTPUT" | jq -r '.id // empty' 2>/dev/null || true) if [ -z "$SERVICE_ASSOC_ID" ]; then echo "ERROR: Failed to extract service association ID" | tee -a "$LOG_FILE" exit 1 fi validate_input "$SERVICE_ASSOC_ID" "SERVICE_ASSOC_ID" || exit 1 echo "Service association created with ID: $SERVICE_ASSOC_ID" | tee -a "$LOG_FILE" CREATED_RESOURCES+=("Service Association: $SERVICE_ASSOC_ID") # Wait for the service association to be active wait_for_resource "Service Association" "$SERVICE_ASSOC_ID" "ACTIVE" "aws vpc-lattice get-service-network-service-association --service-network-service-association-identifier '$SERVICE_ASSOC_ID' --output json" check_error $? # Step 4: List available VPCs to associate with the service network echo -e "\n=== Step 4: Listing available VPCs ===" | tee -a "$LOG_FILE" VPC_LIST=$(log_command "aws ec2 describe-vpcs --query 'Vpcs[*].[VpcId,Tags[?Key==\`Name\`].Value|[0]]' --output text") check_error $? echo "Available VPCs:" | tee -a "$LOG_FILE" echo "$VPC_LIST" | tee -a "$LOG_FILE" # Step 5: Auto-select first available VPC echo -e "\n=== Step 5: Associate a VPC with the service network ===" | tee -a "$LOG_FILE" VPC_ID=$(echo "$VPC_LIST" | head -n 1 | awk '{print $1}') if [ -z "$VPC_ID" ]; then echo "WARNING: No VPC ID found" | tee -a "$LOG_FILE" echo "Skipping VPC association step" | tee -a "$LOG_FILE" else validate_input "$VPC_ID" "VPC_ID" || { echo "ERROR: VPC_ID validation failed" exit 1 } echo "Auto-selected VPC: $VPC_ID" | tee -a "$LOG_FILE" # Step 6: List security groups for the selected VPC echo -e "\n=== Step 6: Listing security groups for VPC $VPC_ID ===" | tee -a "$LOG_FILE" SG_LIST=$(log_command "aws ec2 describe-security-groups --filters Name=vpc-id,Values='$VPC_ID' --query 'SecurityGroups[*].[GroupId,GroupName]' --output text") check_error $? echo "Available Security Groups for VPC $VPC_ID:" | tee -a "$LOG_FILE" echo "$SG_LIST" | tee -a "$LOG_FILE" # Step 7: Auto-select first available security group echo -e "\n=== Step 7: Select a security group for the VPC association ===" | tee -a "$LOG_FILE" SG_ID=$(echo "$SG_LIST" | head -n 1 | awk '{print $1}') if [ -z "$SG_ID" ]; then echo "WARNING: No Security Group ID found" | tee -a "$LOG_FILE" echo "Skipping VPC association step" | tee -a "$LOG_FILE" else validate_input "$SG_ID" "SG_ID" || { echo "ERROR: SG_ID validation failed" exit 1 } echo "Auto-selected Security Group: $SG_ID" | tee -a "$LOG_FILE" # Step 8: Associate the VPC with the service network echo -e "\n=== Step 8: Associating VPC with service network ===" | tee -a "$LOG_FILE" VPC_ASSOC_OUTPUT=$(log_command "aws vpc-lattice create-service-network-vpc-association --vpc-identifier '$VPC_ID' --service-network-identifier '$SERVICE_NETWORK_ID' --security-group-ids '$SG_ID' --output json") check_error $? # Extract the VPC association ID using jq for safety VPC_ASSOC_ID=$(echo "$VPC_ASSOC_OUTPUT" | jq -r '.id // empty' 2>/dev/null || true) if [ -z "$VPC_ASSOC_ID" ]; then echo "ERROR: Failed to extract VPC association ID" | tee -a "$LOG_FILE" else validate_input "$VPC_ASSOC_ID" "VPC_ASSOC_ID" || exit 1 echo "VPC association created with ID: $VPC_ASSOC_ID" | tee -a "$LOG_FILE" CREATED_RESOURCES+=("VPC Association: $VPC_ASSOC_ID") # Wait for the VPC association to be active wait_for_resource "VPC Association" "$VPC_ASSOC_ID" "ACTIVE" "aws vpc-lattice get-service-network-vpc-association --service-network-vpc-association-identifier '$VPC_ASSOC_ID' --output json" check_error $? fi fi fi # Step 9: Display information about the created resources echo -e "\n=== Step 9: Displaying information about created resources ===" | tee -a "$LOG_FILE" echo "Service Network Details:" | tee -a "$LOG_FILE" log_command "aws vpc-lattice get-service-network --service-network-identifier '$SERVICE_NETWORK_ID' --output json" echo "Service Details:" | tee -a "$LOG_FILE" log_command "aws vpc-lattice get-service --service-identifier '$SERVICE_ID' --output json" echo "Service Network Service Associations:" | tee -a "$LOG_FILE" log_command "aws vpc-lattice list-service-network-service-associations --service-network-identifier '$SERVICE_NETWORK_ID' --output json" echo "Service Network VPC Associations:" | tee -a "$LOG_FILE" log_command "aws vpc-lattice list-service-network-vpc-associations --service-network-identifier '$SERVICE_NETWORK_ID' --output json" # Step 10: Cleanup - Auto-confirm echo -e "\n=== Step 10: Resource Cleanup ===" | tee -a "$LOG_FILE" echo "Resources created in this tutorial:" | tee -a "$LOG_FILE" for resource in "${CREATED_RESOURCES[@]+"${CREATED_RESOURCES[@]}"}"; do echo "- $resource" | tee -a "$LOG_FILE" done echo "Starting cleanup process..." | tee -a "$LOG_FILE" # Delete resources in reverse order # Delete VPC association if it was created if [[ -n "${VPC_ASSOC_ID:-}" ]]; then echo "Deleting VPC association: $VPC_ASSOC_ID" | tee -a "$LOG_FILE" log_command "aws vpc-lattice delete-service-network-vpc-association --service-network-vpc-association-identifier '$VPC_ASSOC_ID'" || true # Wait for the VPC association to be deleted echo "Waiting for VPC association to be deleted..." | tee -a "$LOG_FILE" sleep 30 fi # Delete service association echo "Deleting service association: $SERVICE_ASSOC_ID" | tee -a "$LOG_FILE" log_command "aws vpc-lattice delete-service-network-service-association --service-network-service-association-identifier '$SERVICE_ASSOC_ID'" || true # Wait for the service association to be deleted echo "Waiting for service association to be deleted..." | tee -a "$LOG_FILE" sleep 30 # Delete service echo "Deleting service: $SERVICE_ID" | tee -a "$LOG_FILE" log_command "aws vpc-lattice delete-service --service-identifier '$SERVICE_ID'" || true # Wait for the service to be deleted echo "Waiting for service to be deleted..." | tee -a "$LOG_FILE" sleep 30 # Delete service network echo "Deleting service network: $SERVICE_NETWORK_ID" | tee -a "$LOG_FILE" log_command "aws vpc-lattice delete-service-network --service-network-identifier '$SERVICE_NETWORK_ID'" || true echo "Cleanup completed successfully!" | tee -a "$LOG_FILE" echo -e "\n=== Tutorial completed! ===" | tee -a "$LOG_FILE" echo "Log file: $LOG_FILE" | tee -a "$LOG_FILE"

The following code example shows how to:

  • Use ec2 CreateVpnGateway

  • Use ec2 DeleteVpnGateway

  • Use ec2 DescribeVpnGateways

  • Use directconnect CreateConnection

  • Use directconnect CreatePrivateVirtualInterface

  • Use directconnect DeleteConnection

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # AWS Direct Connect Connection Management Script - Version 6 # This script demonstrates how to create and manage AWS Direct Connect connections using the AWS CLI # This version includes fixes for user input handling and better error reporting # Set up logging LOG_FILE="directconnect-script.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "$(date): Starting AWS Direct Connect script v6" # Function to check for errors in command output check_error() { local output=$1 local command=$2 if echo "$output" | grep -i "error" > /dev/null; then echo "ERROR: Command failed: $command" echo "Output: $output" cleanup_resources exit 1 fi } # Function to wait for VGW to be available wait_for_vgw() { local vgw_id=$1 local max_attempts=30 local attempt=1 echo "Waiting for virtual private gateway $vgw_id to become available..." while [ $attempt -le $max_attempts ]; do VGW_STATE=$(aws ec2 describe-vpn-gateways --vpn-gateway-ids "$vgw_id" --query 'VpnGateways[0].State' --output text) if [ "$VGW_STATE" == "available" ]; then echo "Virtual private gateway is now available" return 0 elif [ "$VGW_STATE" == "failed" ]; then echo "Virtual private gateway failed to become available" return 1 fi echo "Attempt $attempt/$max_attempts: VGW state is $VGW_STATE, waiting 10 seconds..." sleep 10 attempt=$((attempt + 1)) done echo "Timeout waiting for VGW to become available" return 1 } # Function to wait for connection to be available wait_for_connection() { local connection_id=$1 local max_attempts=60 local attempt=1 echo "Waiting for connection $connection_id to become available..." echo "Note: This can take 30+ minutes in production as AWS provisions the physical connection" while [ $attempt -le $max_attempts ]; do CONNECTION_STATE=$(aws directconnect describe-connections --connection-id "$connection_id" --query 'connections[0].connectionState' --output text) if [ "$CONNECTION_STATE" == "available" ]; then echo "Connection is now available" return 0 elif [ "$CONNECTION_STATE" == "rejected" ] || [ "$CONNECTION_STATE" == "deleted" ]; then echo "Connection failed with state: $CONNECTION_STATE" return 1 fi echo "Attempt $attempt/$max_attempts: Connection state is $CONNECTION_STATE, waiting 30 seconds..." sleep 30 attempt=$((attempt + 1)) done echo "Timeout waiting for connection to become available" return 1 } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources..." # Delete virtual interfaces if they exist if [ -n "$PRIVATE_VIF_ID" ]; then echo "Deleting private virtual interface: $PRIVATE_VIF_ID" aws directconnect delete-virtual-interface --virtual-interface-id "$PRIVATE_VIF_ID" fi if [ -n "$PUBLIC_VIF_ID" ]; then echo "Deleting public virtual interface: $PUBLIC_VIF_ID" aws directconnect delete-virtual-interface --virtual-interface-id "$PUBLIC_VIF_ID" fi # Delete connection if it exists if [ -n "$CONNECTION_ID" ]; then echo "Deleting connection: $CONNECTION_ID" aws directconnect delete-connection --connection-id "$CONNECTION_ID" fi # Delete VGW if it exists if [ -n "$VGW_ID" ]; then echo "Deleting virtual private gateway: $VGW_ID" aws ec2 delete-vpn-gateway --vpn-gateway-id "$VGW_ID" fi } # Generate a random identifier for resource names RANDOM_ID=$(openssl rand -hex 6) CONNECTION_NAME="DxConn-${RANDOM_ID}" # Step 1: List available Direct Connect locations echo "Listing available Direct Connect locations..." LOCATIONS_OUTPUT=$(aws directconnect describe-locations) check_error "$LOCATIONS_OUTPUT" "describe-locations" echo "$LOCATIONS_OUTPUT" # Extract the first location code for demonstration purposes LOCATION_CODE=$(aws directconnect describe-locations --query 'locations[0].locationCode' --output text) if [ -z "$LOCATION_CODE" ] || [ "$LOCATION_CODE" == "None" ]; then echo "Error: Could not extract location code from the output." exit 1 fi echo "Using location: $LOCATION_CODE" # Step 2: Create a dedicated connection echo "Creating a dedicated connection at location $LOCATION_CODE with bandwidth 1Gbps..." CONNECTION_OUTPUT=$(aws directconnect create-connection \ --location "$LOCATION_CODE" \ --bandwidth "1Gbps" \ --connection-name "$CONNECTION_NAME" \ --tags key=project,value=doc-smith key=tutorial,value=aws-direct-connect-gs) check_error "$CONNECTION_OUTPUT" "create-connection" echo "$CONNECTION_OUTPUT" # Extract connection ID directly from the output CONNECTION_ID=$(echo "$CONNECTION_OUTPUT" | grep -o '"connectionId": "[^"]*' | cut -d'"' -f4) if [ -z "$CONNECTION_ID" ]; then echo "Error: Could not extract connection ID from the output." exit 1 fi echo "Connection created with ID: $CONNECTION_ID" # Step 3: Describe the connection echo "Retrieving connection details..." DESCRIBE_OUTPUT=$(aws directconnect describe-connections --connection-id "$CONNECTION_ID") check_error "$DESCRIBE_OUTPUT" "describe-connections" echo "$DESCRIBE_OUTPUT" # Step 4: Update the connection name NEW_CONNECTION_NAME="${CONNECTION_NAME}-updated" echo "Updating connection name to $NEW_CONNECTION_NAME..." UPDATE_OUTPUT=$(aws directconnect update-connection \ --connection-id "$CONNECTION_ID" \ --connection-name "$NEW_CONNECTION_NAME") check_error "$UPDATE_OUTPUT" "update-connection" echo "$UPDATE_OUTPUT" # Step 5: Check if we can download the LOA-CFA # Note: In a real scenario, the LOA-CFA might not be immediately available echo "Attempting to download the LOA-CFA (this may not be available yet)..." LOA_OUTPUT=$(aws directconnect describe-loa --connection-id "$CONNECTION_ID" 2>&1) if echo "$LOA_OUTPUT" | grep -i "error" > /dev/null; then echo "LOA-CFA not available yet. This is expected for newly created connections." echo "The LOA-CFA will be available once AWS begins provisioning your connection." else LOA_CONTENT=$(echo "$LOA_OUTPUT" | grep -o '"loaContent": "[^"]*' | cut -d'"' -f4) echo "$LOA_CONTENT" | base64 --decode > "loa-cfa-${CONNECTION_ID}.pdf" echo "LOA-CFA downloaded to loa-cfa-${CONNECTION_ID}.pdf" fi # Step 6: Create a virtual private gateway (required for private virtual interface) echo "Creating a virtual private gateway..." VGW_OUTPUT=$(aws ec2 create-vpn-gateway --type ipsec.1 \ --tag-specifications 'ResourceType=vpn-gateway,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=aws-direct-connect-gs}]') check_error "$VGW_OUTPUT" "create-vpn-gateway" echo "$VGW_OUTPUT" # Extract VGW ID directly from the output VGW_ID=$(echo "$VGW_OUTPUT" | grep -o '"VpnGatewayId": "[^"]*' | cut -d'"' -f4) if [ -z "$VGW_ID" ]; then echo "Error: Could not extract VPN gateway ID from the output." exit 1 fi echo "Virtual private gateway created with ID: $VGW_ID" # Wait for VGW to become available if ! wait_for_vgw "$VGW_ID"; then echo "Failed to wait for VGW to become available. Skipping virtual interface creation." VIF_CREATION_SKIPPED=true else VIF_CREATION_SKIPPED=false fi # Step 7: Create a private virtual interface (only if VGW is available) if [ "$VIF_CREATION_SKIPPED" = false ]; then echo "Creating a private virtual interface..." PRIVATE_VIF_OUTPUT=$(aws directconnect create-private-virtual-interface \ --connection-id "$CONNECTION_ID" \ --new-private-virtual-interface '{ "virtualInterfaceName": "PrivateVIF-'"$RANDOM_ID"'", "vlan": 100, "asn": 65000, "authKey": "'"$RANDOM_ID"'key", "amazonAddress": "192.168.1.1/30", "customerAddress": "192.168.1.2/30", "addressFamily": "ipv4", "virtualGatewayId": "'"$VGW_ID"'" }' 2>&1) if echo "$PRIVATE_VIF_OUTPUT" | grep -i "error" > /dev/null; then echo "Could not create private virtual interface. This is expected if the connection is not yet available." echo "Error: $PRIVATE_VIF_OUTPUT" PRIVATE_VIF_ID="" else echo "$PRIVATE_VIF_OUTPUT" PRIVATE_VIF_ID=$(echo "$PRIVATE_VIF_OUTPUT" | grep -o '"virtualInterfaceId": "[^"]*' | cut -d'"' -f4) echo "Private virtual interface created with ID: $PRIVATE_VIF_ID" fi else echo "Skipping private virtual interface creation due to VGW not being available" PRIVATE_VIF_ID="" fi # Step 8: Check connection state and provide guidance for public virtual interface CONNECTION_STATE=$(aws directconnect describe-connections --connection-id "$CONNECTION_ID" --query 'connections[0].connectionState' --output text) echo "Current connection state: $CONNECTION_STATE" if [ "$CONNECTION_STATE" != "available" ]; then echo "" echo "===========================================" echo "CONNECTION NOT YET AVAILABLE" echo "===========================================" echo "The connection is in '$CONNECTION_STATE' state." echo "In production, you would:" echo "1. Wait for AWS to provision the connection (can take 30+ minutes)" echo "2. Download the LOA-CFA when available" echo "3. Provide the LOA-CFA to your network provider for cross-connect" echo "4. Create virtual interfaces once connection is 'available'" echo "" # Ask if user wants to wait for connection to become available echo "" echo "===========================================" echo "CONNECTION WAIT CONFIRMATION" echo "===========================================" echo -n "Do you want to wait for the connection to become available? (y/n): " read -r WAIT_CHOICE if [[ "$WAIT_CHOICE" =~ ^[Yy]$ ]]; then if wait_for_connection "$CONNECTION_ID"; then echo "Connection is now available! You could now create virtual interfaces." else echo "Connection did not become available within the timeout period." fi else echo "Skipping wait for connection availability." fi else echo "Connection is available! Virtual interfaces can be created." fi # Step 9: List all virtual interfaces echo "Listing all virtual interfaces..." VIF_LIST_OUTPUT=$(aws directconnect describe-virtual-interfaces) check_error "$VIF_LIST_OUTPUT" "describe-virtual-interfaces" echo "$VIF_LIST_OUTPUT" # Step 10: Display important information about production usage echo "" echo "===========================================" echo "IMPORTANT PRODUCTION NOTES" echo "===========================================" echo "1. Direct Connect connections take time to be provisioned by AWS" echo "2. You cannot create virtual interfaces until the connection is 'available'" echo "3. For public virtual interfaces, you must own the public IP addresses" echo "4. LOA-CFA (Letter of Authorization) is needed for cross-connect at the facility" echo "5. This demo creates resources that incur costs (~\$300/month for 1Gbps)" echo "6. Always test connectivity before putting into production" echo "" # Step 11: Ask user if they want to clean up resources echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo -n "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then cleanup_resources echo "All resources have been cleaned up." else echo "Resources were not cleaned up. You can manually delete them later." echo "" echo "Created resources:" echo "- Connection ID: $CONNECTION_ID" if [ -n "$PRIVATE_VIF_ID" ]; then echo "- Private Virtual Interface ID: $PRIVATE_VIF_ID" fi if [ -n "$PUBLIC_VIF_ID" ]; then echo "- Public Virtual Interface ID: $PUBLIC_VIF_ID" fi echo "- Virtual Private Gateway ID: $VGW_ID" echo "" echo "Manual cleanup commands:" echo "aws directconnect delete-connection --connection-id $CONNECTION_ID" echo "aws ec2 delete-vpn-gateway --vpn-gateway-id $VGW_ID" echo "" echo "Remember: Direct Connect resources incur ongoing costs!" fi echo "$(date): Script completed"

The following code example shows how to:

  • Use ec2 AuthorizeSecurityGroupIngress

  • Use ec2 CreateKeyPair

  • Use ec2 CreateSecurityGroup

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # AWS Marketplace Buyer Getting Started Script # This script demonstrates how to search for products in AWS Marketplace, # launch an EC2 instance with a product AMI, and manage subscriptions. set -euo pipefail # Setup logging with secure permissions LOG_FILE="marketplace-tutorial.log" touch "$LOG_FILE" chmod 600 "$LOG_FILE" exec > >(tee -a "$LOG_FILE") 2>&1 echo "===================================================" echo "AWS Marketplace Buyer Getting Started Tutorial" echo "===================================================" echo "This script will:" echo "1. List available products in AWS Marketplace" echo "2. Create resources needed to launch an EC2 instance" echo "3. Launch an EC2 instance with an Amazon Linux 2 AMI" echo "4. Show how to manage and terminate the instance" echo "===================================================" echo "" # Validate AWS CLI is installed and configured if ! command -v aws &> /dev/null; then echo "ERROR: AWS CLI is not installed. Please install it first." exit 1 fi # Verify AWS credentials are configured if ! aws sts get-caller-identity &> /dev/null; then echo "ERROR: AWS credentials are not configured. Please configure them first." exit 1 fi # Validate jq is installed if ! command -v jq &> /dev/null; then echo "ERROR: jq is not installed. Please install jq for safe JSON parsing." exit 1 fi # Function to safely extract JSON values using jq extract_json_value() { local json=$1 local query=$2 echo "$json" | jq -r "$query" 2>/dev/null || { echo "ERROR: Failed to parse JSON with query: $query" >&2 return 1 } } # Function to validate AWS permissions validate_aws_permissions() { echo "Validating AWS permissions..." local identity identity=$(aws sts get-caller-identity --output json) local account_id account_id=$(extract_json_value "$identity" '.Account') || return 1 local arn arn=$(extract_json_value "$identity" '.Arn') || return 1 echo "AWS Account ID: $account_id" echo "AWS Principal ARN: $arn" echo "Note: This script requires EC2 permissions for key pair, security group, and instance management." echo "" } # Function to clean up resources cleanup_resources() { echo "" echo "===================================================" echo "CLEANING UP RESOURCES" echo "===================================================" if [ -n "${INSTANCE_ID:-}" ]; then echo "Terminating EC2 instance: $INSTANCE_ID" aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" --region "$AWS_REGION" > /dev/null 2>&1 || true echo "Waiting for instance to terminate..." aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" --region "$AWS_REGION" 2>/dev/null || true echo "Instance terminated successfully." fi if [ -n "${SECURITY_GROUP_ID:-}" ]; then echo "Waiting before deleting security group..." sleep 5 echo "Deleting security group: $SECURITY_GROUP_ID" aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID" --region "$AWS_REGION" > /dev/null 2>&1 || true echo "Security group deleted." fi if [ -n "${KEY_NAME:-}" ]; then echo "Deleting key pair: $KEY_NAME" aws ec2 delete-key-pair --key-name "$KEY_NAME" --region "$AWS_REGION" > /dev/null 2>&1 || true # Remove the local key file if it exists with secure deletion if [ -f "${KEY_NAME}.pem" ]; then if command -v shred &> /dev/null; then shred -vfz -n 3 "${KEY_NAME}.pem" 2>/dev/null || rm -f "${KEY_NAME}.pem" else rm -f "${KEY_NAME}.pem" fi echo "Local key file securely deleted." fi fi echo "Cleanup completed." } # Set trap to ensure cleanup on script exit trap cleanup_resources EXIT # Get the current AWS region AWS_REGION=$(aws configure get region || echo "us-east-1") if [ -z "$AWS_REGION" ] || [ "$AWS_REGION" = "None" ]; then AWS_REGION="us-east-1" fi echo "Using AWS Region: $AWS_REGION" echo "" # Validate permissions validate_aws_permissions # Generate random identifier for resource names using cryptographically secure method RANDOM_ID=$(head -c 6 /dev/urandom | od -An -tx1 | tr -d ' ') KEY_NAME="marketplace-key-${RANDOM_ID}" SECURITY_GROUP_NAME="marketplace-sg-${RANDOM_ID}" # Initialize variables to track created resources INSTANCE_ID="" SECURITY_GROUP_ID="" AMI_ID="" # Step 1: List available products in AWS Marketplace echo "Listing available products in AWS Marketplace..." echo "Note: In a real scenario, you would use marketplace-catalog commands to list and search for products." echo "However, this requires specific permissions and product knowledge." echo "" echo "For this tutorial, we'll use a public Amazon Linux 2 AMI instead of an actual marketplace product." echo "This is because subscribing to marketplace products requires accepting terms via the console." echo "" # Step 2: Create a key pair for SSH access echo "Creating key pair: $KEY_NAME" KEY_OUTPUT=$(aws ec2 create-key-pair \ --key-name "$KEY_NAME" \ --region "$AWS_REGION" \ --query 'KeyMaterial' \ --output text) || { echo "ERROR: Failed to create key pair" >&2 exit 1 } # Securely save the key with restricted permissions if ! echo "$KEY_OUTPUT" > "${KEY_NAME}.pem" 2>/dev/null; then echo "ERROR: Failed to write key file ${KEY_NAME}.pem" >&2 exit 1 fi chmod 600 "${KEY_NAME}.pem" || { echo "ERROR: Failed to set permissions on key file" >&2 rm -f "${KEY_NAME}.pem" exit 1 } echo "Key pair created and saved to ${KEY_NAME}.pem with secure permissions (600)" # Step 3: Create a security group echo "Creating security group: $SECURITY_GROUP_NAME" SG_OUTPUT=$(aws ec2 create-security-group \ --group-name "$SECURITY_GROUP_NAME" \ --description "Security group for AWS Marketplace tutorial" \ --region "$AWS_REGION" \ --output json) || { echo "ERROR: Failed to create security group" >&2 exit 1 } # Extract security group ID using jq for safe parsing SECURITY_GROUP_ID=$(extract_json_value "$SG_OUTPUT" '.GroupId') || exit 1 if [ -z "$SECURITY_GROUP_ID" ] || [ "$SECURITY_GROUP_ID" = "null" ]; then echo "ERROR: Could not extract security group ID" >&2 exit 1 fi echo "Security group created with ID: $SECURITY_GROUP_ID" # Add inbound rules for SSH and HTTP in parallel for better performance echo "Configuring security group rules..." { aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 22 \ --cidr 0.0.0.0/0 \ --region "$AWS_REGION" > /dev/null 2>&1 } & SSH_PID=$! { aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 80 \ --cidr 0.0.0.0/0 \ --region "$AWS_REGION" > /dev/null 2>&1 } & HTTP_PID=$! # Wait for both operations to complete wait $SSH_PID || { echo "ERROR: Failed to add SSH ingress rule" >&2 exit 1 } wait $HTTP_PID || { echo "ERROR: Failed to add HTTP ingress rule" >&2 exit 1 } echo "Security group configured with SSH and HTTP access." echo "WARNING: SSH rule allows access from any IP (0.0.0.0/0). Restrict this in production." echo "WARNING: In a production environment, you should restrict access to specific IP ranges." echo "" # Step 4: Get the latest Amazon Linux 2 AMI ID - Use pagination to optimize costs echo "Getting the latest Amazon Linux 2 AMI ID..." AMI_ID=$(aws ec2 describe-images \ --owners amazon \ --filters "Name=name,Values=amzn2-ami-hvm-2.0.*-x86_64-gp2" "Name=state,Values=available" \ --region "$AWS_REGION" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" \ --output text \ --max-results 50) || { echo "ERROR: Failed to describe images" >&2 exit 1 } if [ -z "$AMI_ID" ] || [ "$AMI_ID" = "None" ]; then echo "ERROR: Could not find a suitable AMI ID" >&2 exit 1 fi echo "Using AMI ID: $AMI_ID" echo "Note: In a real marketplace scenario, you would use the AMI ID from your subscribed product." echo "" # Step 5: Launch an EC2 instance with cost optimization echo "Launching EC2 instance with the AMI..." echo "Using t2.micro (eligible for AWS Free Tier if applicable)" INSTANCE_OUTPUT=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --instance-type t2.micro \ --key-name "$KEY_NAME" \ --security-group-ids "$SECURITY_GROUP_ID" \ --count 1 \ --region "$AWS_REGION" \ --monitoring Enabled=false \ --tag-specifications 'ResourceType=instance,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=marketplace-buyer-gs}]' \ --output json) || { echo "ERROR: Failed to launch instance" >&2 exit 1 } # Extract instance ID using jq for safe parsing INSTANCE_ID=$(extract_json_value "$INSTANCE_OUTPUT" '.Instances[0].InstanceId') || exit 1 if [ -z "$INSTANCE_ID" ] || [ "$INSTANCE_ID" = "null" ]; then echo "ERROR: Could not extract instance ID" >&2 exit 1 fi echo "Instance launched with ID: $INSTANCE_ID" # Wait for the instance to be running echo "Waiting for instance to be in running state..." aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" --region "$AWS_REGION" || { echo "ERROR: Instance failed to reach running state" >&2 exit 1 } echo "Instance is now running." echo "" # Step 6: Get instance details echo "Getting instance details..." INSTANCE_DETAILS=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --region "$AWS_REGION" \ --output json) || { echo "ERROR: Failed to describe instance" >&2 exit 1 } echo "Instance details:" extract_json_value "$INSTANCE_DETAILS" '.Reservations[0].Instances[0] | {InstanceId, State: .State.Name, PublicDnsName, PrivateIpAddress, LaunchTime, InstanceType}' || exit 1 # Display summary of created resources echo "" echo "===================================================" echo "RESOURCE SUMMARY" echo "===================================================" echo "Key Pair: $KEY_NAME" echo "Security Group: $SECURITY_GROUP_NAME (ID: $SECURITY_GROUP_ID)" echo "EC2 Instance: $INSTANCE_ID" echo "Instance Type: t2.micro (cost-optimized)" echo "AMI ID: $AMI_ID" echo "Region: $AWS_REGION" echo "" echo "COST OPTIMIZATION NOTES:" echo "- t2.micro instances are eligible for AWS Free Tier (750 hours/month for 12 months)" echo "- Detailed monitoring is disabled to reduce costs" echo "- Consider using Spot Instances for non-production workloads" echo "- Review AWS Pricing Calculator: https://calculator.aws/" echo "" echo "To connect to your instance (once it's fully initialized):" echo "ssh -i ${KEY_NAME}.pem ec2-user@<public-dns-name>" echo "Replace <public-dns-name> with the PublicDnsName from the instance details above." echo "" # Auto-confirm cleanup of resources echo "===================================================" echo "CLEANUP CONFIRMATION" echo "===================================================" echo "Cleaning up all created resources..." echo "" echo "Script completed. See $LOG_FILE for the complete log."

The following code example shows how to:

  • Create a DB subnet group

  • Create a DocumentDB cluster

  • Create a DocumentDB instance

  • Configure security and connectivity

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Amazon DocumentDB - Getting Started # This script creates a DocumentDB cluster with encrypted storage, stores the # master password in Secrets Manager, and displays connection information. set -eE ############################################################################### # Configuration ############################################################################### SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' </dev/urandom | head -c 8) CLUSTER_ID="docdb-gs-${SUFFIX}" INSTANCE_ID="${CLUSTER_ID}-inst" SUBNET_GROUP_NAME="docdb-subnet-${SUFFIX}" SECRET_NAME="docdb-secret-${SUFFIX}" MASTER_USER="docdbadmin" ENGINE_VERSION="5.0.0" INSTANCE_CLASS="db.t3.medium" DOCDB_PORT=27017 WAIT_TIMEOUT=900 TEMP_DIR=$(mktemp -d) LOG_FILE="${TEMP_DIR}/documentdb-gs.log" trap 'rm -rf "$TEMP_DIR"' EXIT CREATED_RESOURCES=() ############################################################################### # Logging ############################################################################### exec > >(tee -a "$LOG_FILE") 2>&1 echo "Log file: $LOG_FILE" echo "" ############################################################################### # Region pre-check ############################################################################### CONFIGURED_REGION=$(aws configure get region 2>/dev/null || true) if [ -z "$CONFIGURED_REGION" ] && [ -z "$AWS_DEFAULT_REGION" ] && [ -z "$AWS_REGION" ]; then echo "ERROR: No AWS region configured." echo "Run 'aws configure set region <region>' or export AWS_DEFAULT_REGION." exit 1 fi REGION="${AWS_REGION:-${AWS_DEFAULT_REGION:-$CONFIGURED_REGION}}" echo "Using region: $REGION" echo "" ############################################################################### # Error handler ############################################################################### handle_error() { echo "" echo "===========================================" echo "ERROR at $1" echo "===========================================" echo "" if [ ${#CREATED_RESOURCES[@]} -gt 0 ]; then echo "Resources created before error:" for r in "${CREATED_RESOURCES[@]}"; do echo " - $r" done echo "" echo "Attempting cleanup..." cleanup_resources fi exit 1 } trap 'handle_error "line $LINENO"' ERR ############################################################################### # Wait function ############################################################################### wait_for_status() { local resource_type="$1" local resource_id="$2" local target_status="$3" local timeout="${4:-$WAIT_TIMEOUT}" local elapsed=0 local interval=30 echo "Waiting for $resource_type '$resource_id' to reach '$target_status'..." while true; do local current_status="" if [ "$resource_type" = "cluster" ]; then current_status=$(aws docdb describe-db-clusters \ --db-cluster-identifier "$resource_id" \ --query "DBClusters[0].Status" --output text 2>&1) elif [ "$resource_type" = "instance" ]; then current_status=$(aws docdb describe-db-instances \ --db-instance-identifier "$resource_id" \ --query "DBInstances[0].DBInstanceStatus" --output text 2>&1) fi if echo "$current_status" | grep -iq "error"; then echo "ERROR checking status: $current_status" return 1 fi echo " Status: $current_status ($elapsed/${timeout}s)" if [ "$current_status" = "$target_status" ]; then echo " $resource_type '$resource_id' is now '$target_status'." return 0 fi if [ "$elapsed" -ge "$timeout" ]; then echo "ERROR: Timed out after ${timeout}s waiting for $resource_type '$resource_id'." return 1 fi sleep "$interval" elapsed=$((elapsed + interval)) done } ############################################################################### # Wait for deletion ############################################################################### wait_for_deletion() { local resource_type="$1" local resource_id="$2" local timeout="${3:-$WAIT_TIMEOUT}" local elapsed=0 local interval=30 echo "Waiting for $resource_type '$resource_id' to be deleted..." while true; do local result="" if [ "$resource_type" = "cluster" ]; then result=$(aws docdb describe-db-clusters \ --db-cluster-identifier "$resource_id" \ --query "DBClusters[0].Status" --output text 2>&1) || true elif [ "$resource_type" = "instance" ]; then result=$(aws docdb describe-db-instances \ --db-instance-identifier "$resource_id" \ --query "DBInstances[0].DBInstanceStatus" --output text 2>&1) || true fi if echo "$result" | grep -iq "DBClusterNotFoundFault\|DBInstanceNotFound\|not found"; then echo " $resource_type '$resource_id' deleted." return 0 fi echo " Still deleting... ($elapsed/${timeout}s)" if [ "$elapsed" -ge "$timeout" ]; then echo "WARNING: Timed out waiting for $resource_type '$resource_id' deletion." return 1 fi sleep "$interval" elapsed=$((elapsed + interval)) done } ############################################################################### # Cleanup ############################################################################### cleanup_resources() { echo "" echo "Cleaning up resources..." echo "" # Revoke security group ingress rule if [ -n "${SG_ID:-}" ] && [ -n "${MY_IP:-}" ]; then echo "Revoking security group ingress rule..." aws ec2 revoke-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port "$DOCDB_PORT" \ --cidr "${MY_IP}/32" 2>&1 || echo "WARNING: Failed to revoke SG ingress rule." fi # Delete instance (must be deleted before cluster) if printf '%s\n' "${CREATED_RESOURCES[@]}" | grep -q "instance:"; then echo "Deleting instance '${INSTANCE_ID}'..." aws docdb delete-db-instance \ --db-instance-identifier "$INSTANCE_ID" 2>&1 || echo "WARNING: Failed to delete instance." wait_for_deletion "instance" "$INSTANCE_ID" || true fi # Delete cluster (skip final snapshot) if printf '%s\n' "${CREATED_RESOURCES[@]}" | grep -q "cluster:"; then echo "Deleting cluster '${CLUSTER_ID}'..." aws docdb delete-db-cluster \ --db-cluster-identifier "$CLUSTER_ID" \ --skip-final-snapshot 2>&1 || echo "WARNING: Failed to delete cluster." wait_for_deletion "cluster" "$CLUSTER_ID" || true fi # Delete subnet group (must wait for cluster deletion) if printf '%s\n' "${CREATED_RESOURCES[@]}" | grep -q "subnet-group:"; then echo "Deleting subnet group '${SUBNET_GROUP_NAME}'..." aws docdb delete-db-subnet-group \ --db-subnet-group-name "$SUBNET_GROUP_NAME" 2>&1 || echo "WARNING: Failed to delete subnet group." fi # Delete secret with scheduled deletion instead of immediate deletion if printf '%s\n' "${CREATED_RESOURCES[@]}" | grep -q "secret:"; then echo "Deleting secret '${SECRET_NAME}' (scheduled for 7 days)..." aws secretsmanager delete-secret \ --secret-id "$SECRET_NAME" \ --recovery-window-in-days 7 2>&1 || echo "WARNING: Failed to delete secret." fi echo "" echo "Cleanup complete." } ############################################################################### # Step 1: Generate password and store in Secrets Manager ############################################################################### echo "===========================================" echo "Step 1: Create master password in Secrets Manager" echo "===========================================" echo "" # Generate a strong password using openssl for better randomness # Meets DocumentDB requirements: 8-100 chars, alphanumeric + special chars MASTER_PASSWORD=$(openssl rand -base64 32 | tr -d '/' | cut -c1-20) # Securely store password in temporary file with restricted permissions TEMP_PASS_FILE=$(mktemp) chmod 600 "$TEMP_PASS_FILE" echo -n "$MASTER_PASSWORD" > "$TEMP_PASS_FILE" trap "rm -f '$TEMP_PASS_FILE'; rm -rf '$TEMP_DIR'" EXIT SECRET_OUTPUT=$(aws secretsmanager create-secret \ --name "$SECRET_NAME" \ --description "DocumentDB master password for ${CLUSTER_ID}" \ --secret-string file://"$TEMP_PASS_FILE" \ --tags Key=project,Value=doc-smith Key=tutorial,Value=documentdb-gs \ --output text --query "ARN" 2>&1) # Securely clear password from memory MASTER_PASSWORD="" if echo "$SECRET_OUTPUT" | grep -iq "error"; then echo "ERROR creating secret: $SECRET_OUTPUT" exit 1 fi SECRET_ARN="$SECRET_OUTPUT" CREATED_RESOURCES+=("secret:${SECRET_NAME}") echo "Secret created: $SECRET_NAME" echo "Secret ARN: $SECRET_ARN" echo "" ############################################################################### # Step 2: Find default VPC and subnets ############################################################################### echo "===========================================" echo "Step 2: Find default VPC and subnets" echo "===========================================" echo "" VPC_ID=$(aws ec2 describe-vpcs \ --filters "Name=isDefault,Values=true" \ --query "Vpcs[0].VpcId" --output text 2>&1) if echo "$VPC_ID" | grep -iq "error"; then echo "ERROR finding default VPC: $VPC_ID" exit 1 fi if [ "$VPC_ID" = "None" ] || [ -z "$VPC_ID" ]; then echo "ERROR: No default VPC found. Create one with 'aws ec2 create-default-vpc'." exit 1 fi echo "Default VPC: $VPC_ID" # Get subnets in at least 2 different AZs (space-separated) SUBNET_INFO=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=${VPC_ID}" "Name=default-for-az,Values=true" \ --query "Subnets[*].[SubnetId,AvailabilityZone]" --output text 2>&1) if echo "$SUBNET_INFO" | grep -iq "error"; then echo "ERROR finding subnets: $SUBNET_INFO" exit 1 fi # Collect unique AZs and their subnet IDs declare -A AZ_SUBNETS while IFS=$'\t' read -r sid az; do if [ -z "${AZ_SUBNETS[$az]+x}" ]; then AZ_SUBNETS[$az]="$sid" fi done <<< "$SUBNET_INFO" AZ_COUNT=${#AZ_SUBNETS[@]} if [ "$AZ_COUNT" -lt 2 ]; then echo "ERROR: DocumentDB requires subnets in at least 2 AZs. Found $AZ_COUNT." exit 1 fi # Build space-separated subnet ID list SUBNET_IDS="" for az in "${!AZ_SUBNETS[@]}"; do if [ -n "$SUBNET_IDS" ]; then SUBNET_IDS="${SUBNET_IDS} ${AZ_SUBNETS[$az]}" else SUBNET_IDS="${AZ_SUBNETS[$az]}" fi done echo "Subnets (${AZ_COUNT} AZs): $SUBNET_IDS" echo "" ############################################################################### # Step 3: Create subnet group ############################################################################### echo "===========================================" echo "Step 3: Create DocumentDB subnet group" echo "===========================================" echo "" SUBNET_GROUP_OUTPUT=$(aws docdb create-db-subnet-group \ --db-subnet-group-name "$SUBNET_GROUP_NAME" \ --db-subnet-group-description "Subnet group for DocumentDB getting started" \ --subnet-ids $SUBNET_IDS \ --tags Key=project,Value=doc-smith Key=tutorial,Value=documentdb-gs \ --query "DBSubnetGroup.DBSubnetGroupName" --output text 2>&1) if echo "$SUBNET_GROUP_OUTPUT" | grep -iq "error"; then echo "ERROR creating subnet group: $SUBNET_GROUP_OUTPUT" exit 1 fi CREATED_RESOURCES+=("subnet-group:${SUBNET_GROUP_NAME}") echo "Subnet group created: $SUBNET_GROUP_NAME" echo "" ############################################################################### # Step 4: Create DocumentDB cluster ############################################################################### echo "===========================================" echo "Step 4: Create DocumentDB cluster" echo "===========================================" echo "" # Read password securely from file for cluster creation MASTER_PASSWORD=$(cat "$TEMP_PASS_FILE") CLUSTER_OUTPUT=$(aws docdb create-db-cluster \ --db-cluster-identifier "$CLUSTER_ID" \ --engine docdb \ --engine-version "$ENGINE_VERSION" \ --master-username "$MASTER_USER" \ --master-user-password "$MASTER_PASSWORD" \ --db-subnet-group-name "$SUBNET_GROUP_NAME" \ --storage-encrypted \ --kms-key-id "alias/aws/docdb" \ --no-deletion-protection \ --enable-cloudwatch-logs-exports '["audit","error","general","slowquery"]' \ --tags Key=project,Value=doc-smith Key=tutorial,Value=documentdb-gs \ --query "DBCluster.DBClusterIdentifier" --output text 2>&1) # Clear password immediately after use MASTER_PASSWORD="" if echo "$CLUSTER_OUTPUT" | grep -iq "error"; then echo "ERROR creating cluster: $CLUSTER_OUTPUT" exit 1 fi CREATED_RESOURCES+=("cluster:${CLUSTER_ID}") echo "Cluster created: $CLUSTER_ID" echo "" wait_for_status "cluster" "$CLUSTER_ID" "available" echo "" ############################################################################### # Step 5: Create DocumentDB instance ############################################################################### echo "===========================================" echo "Step 5: Create DocumentDB instance" echo "===========================================" echo "" INSTANCE_OUTPUT=$(aws docdb create-db-instance \ --db-instance-identifier "$INSTANCE_ID" \ --db-instance-class "$INSTANCE_CLASS" \ --db-cluster-identifier "$CLUSTER_ID" \ --engine docdb \ --tags Key=project,Value=doc-smith Key=tutorial,Value=documentdb-gs \ --query "DBInstance.DBInstanceIdentifier" --output text 2>&1) if echo "$INSTANCE_OUTPUT" | grep -iq "error"; then echo "ERROR creating instance: $INSTANCE_OUTPUT" exit 1 fi CREATED_RESOURCES+=("instance:${INSTANCE_ID}") echo "Instance created: $INSTANCE_ID" echo "" wait_for_status "instance" "$INSTANCE_ID" "available" echo "" ############################################################################### # Step 6: Get cluster endpoint and security group ############################################################################### echo "===========================================" echo "Step 6: Get cluster endpoint and security group" echo "===========================================" echo "" CLUSTER_DETAILS=$(aws docdb describe-db-clusters \ --db-cluster-identifier "$CLUSTER_ID" \ --query "DBClusters[0].[Endpoint,VpcSecurityGroups[0].VpcSecurityGroupId]" \ --output text 2>&1) if echo "$CLUSTER_DETAILS" | grep -iq "error"; then echo "ERROR getting cluster details: $CLUSTER_DETAILS" exit 1 fi CLUSTER_ENDPOINT=$(echo "$CLUSTER_DETAILS" | awk '{print $1}') SG_ID=$(echo "$CLUSTER_DETAILS" | awk '{print $2}') echo "Cluster endpoint: $CLUSTER_ENDPOINT" echo "Security group: $SG_ID" echo "" ############################################################################### # Step 7: Add security group ingress for port 27017 from user's IP ############################################################################### echo "===========================================" echo "Step 7: Add security group ingress rule" echo "===========================================" echo "" # Get the user's public IP with timeout and error handling MY_IP=$(timeout 5 curl -s --max-time 5 https://checkip.amazonaws.com 2>/dev/null || true) if [ -z "$MY_IP" ] || echo "$MY_IP" | grep -iq "error\|could not\|failed"; then echo "WARNING: Could not determine public IP address. Skipping security group rule." echo "You must manually add an ingress rule for your IP to security group $SG_ID" MY_IP="" else # Trim whitespace MY_IP=$(echo "$MY_IP" | tr -d '[:space:]') # Validate IP format (basic check) if ! echo "$MY_IP" | grep -qE '^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$'; then echo "WARNING: Invalid IP address format: $MY_IP. Skipping security group rule." MY_IP="" else echo "Your public IP: $MY_IP" SG_RULE_OUTPUT=$(aws ec2 authorize-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port "$DOCDB_PORT" \ --cidr "${MY_IP}/32" 2>&1) if echo "$SG_RULE_OUTPUT" | grep -iq "error"; then # Ignore if rule already exists if echo "$SG_RULE_OUTPUT" | grep -iq "Duplicate"; then echo "Ingress rule already exists." else echo "ERROR adding ingress rule: $SG_RULE_OUTPUT" exit 1 fi else echo "Ingress rule added: TCP ${DOCDB_PORT} from ${MY_IP}/32" CREATED_RESOURCES+=("sg-rule:${SG_ID}:${MY_IP}") fi fi fi echo "" ############################################################################### # Step 8: Download CA certificate ############################################################################### echo "===========================================" echo "Step 8: Download Amazon DocumentDB CA certificate" echo "===========================================" echo "" CA_CERT_PATH="${TEMP_DIR}/global-bundle.pem" CA_CERT_URL="https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem" if timeout 10 curl -s --max-time 10 -o "$CA_CERT_PATH" "$CA_CERT_URL" 2>&1; then if [ -s "$CA_CERT_PATH" ]; then # Verify it's a valid PEM file and check file permissions if grep -q "BEGIN CERTIFICATE" "$CA_CERT_PATH"; then chmod 644 "$CA_CERT_PATH" echo "CA certificate downloaded to: $CA_CERT_PATH" else echo "WARNING: Downloaded file is not a valid PEM certificate." CA_CERT_PATH="" fi else echo "WARNING: Failed to download CA certificate (empty file)." CA_CERT_PATH="" fi else echo "WARNING: Failed to download CA certificate (timeout or network error)." CA_CERT_PATH="" fi echo "" ############################################################################### # Step 9: Display connection information ############################################################################### echo "===========================================" echo "CONNECTION INFORMATION" echo "===========================================" echo "" echo "Cluster endpoint : $CLUSTER_ENDPOINT" echo "Port : $DOCDB_PORT" echo "Master username : $MASTER_USER" echo "Secret name : $SECRET_NAME (contains password)" echo "Security group : $SG_ID" if [ -n "$CA_CERT_PATH" ]; then echo "CA certificate : $CA_CERT_PATH" fi echo "" echo "To connect with mongosh:" if [ -n "$CA_CERT_PATH" ]; then echo " mongosh --tls --host ${CLUSTER_ENDPOINT} --tlsCAFile ${CA_CERT_PATH} \\" else echo " mongosh --tls --host ${CLUSTER_ENDPOINT} \\" fi echo " --retryWrites false --username ${MASTER_USER} --password \$(aws secretsmanager get-secret-value --secret-id ${SECRET_NAME} --query SecretString --output text)" echo "" ############################################################################### # Step 10: Cleanup ############################################################################### echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "" echo "Resources created:" for r in "${CREATED_RESOURCES[@]}"; do echo " - $r" done echo "" echo "Automatically cleaning up all created resources..." echo "" cleanup_resources echo "Done."

The following code example shows how to:

  • Create a key pair

  • Create a security group

  • Stop and start your instance

  • Test Elastic IP persistence

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # EC2 Basics Tutorial Script - Revised # This script demonstrates the basics of working with EC2 instances using AWS CLI # Updated to use Amazon Linux 2023 and enhanced security settings # Set up logging LOG_FILE="ec2_tutorial_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 # Function to log messages log() { echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" } # Function to handle errors handle_error() { log "ERROR: $1" log "Cleaning up resources..." cleanup exit 1 } # Function to clean up resources cleanup() { log "Resources created:" if [ -n "$ASSOCIATION_ID" ]; then log "- Elastic IP Association: $ASSOCIATION_ID" fi if [ -n "$ALLOCATION_ID" ]; then log "- Elastic IP Allocation: $ALLOCATION_ID (IP: $ELASTIC_IP)" fi if [ -n "$INSTANCE_ID" ]; then log "- EC2 Instance: $INSTANCE_ID" fi if [ -n "$SECURITY_GROUP_ID" ]; then log "- Security Group: $SECURITY_GROUP_ID" fi if [ -n "$KEY_NAME" ]; then log "- Key Pair: $KEY_NAME (File: $KEY_FILE)" fi REPLY=y if [[ $REPLY =~ ^[Yy]$ ]]; then log "Starting cleanup..." # Track cleanup failures CLEANUP_FAILURES=0 # Disassociate Elastic IP if it exists if [ -n "$ASSOCIATION_ID" ]; then log "Disassociating Elastic IP..." if ! aws ec2 disassociate-address --association-id "$ASSOCIATION_ID"; then log "Failed to disassociate Elastic IP" ((CLEANUP_FAILURES++)) fi fi # Release Elastic IP if it exists if [ -n "$ALLOCATION_ID" ]; then log "Releasing Elastic IP..." if ! aws ec2 release-address --allocation-id "$ALLOCATION_ID"; then log "Failed to release Elastic IP" ((CLEANUP_FAILURES++)) fi fi # Terminate instance if it exists if [ -n "$INSTANCE_ID" ]; then log "Terminating instance $INSTANCE_ID..." if ! aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" > /dev/null; then log "Failed to terminate instance" ((CLEANUP_FAILURES++)) else log "Waiting for instance to terminate..." if ! aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID"; then log "Failed while waiting for instance to terminate" ((CLEANUP_FAILURES++)) fi fi fi # Delete security group if it exists if [ -n "$SECURITY_GROUP_ID" ]; then log "Deleting security group..." if ! aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID"; then log "Failed to delete security group" ((CLEANUP_FAILURES++)) fi fi # Delete key pair if it exists if [ -n "$KEY_NAME" ]; then log "Deleting key pair..." if ! aws ec2 delete-key-pair --key-name "$KEY_NAME"; then log "Failed to delete key pair" ((CLEANUP_FAILURES++)) fi # Remove key file if [ -f "$KEY_FILE" ]; then log "Removing key file..." if ! rm -f "$KEY_FILE"; then log "Failed to remove key file" ((CLEANUP_FAILURES++)) fi fi fi # Report cleanup status if [ $CLEANUP_FAILURES -eq 0 ]; then log "Cleanup completed successfully." else log "WARNING: Cleanup completed with $CLEANUP_FAILURES failures. Some resources may not have been deleted properly." fi else log "Resources were not deleted." fi } # Generate random identifier for resource names RANDOM_ID=$(openssl rand -hex 4) KEY_NAME="ec2-tutorial-key-$RANDOM_ID" SG_NAME="ec2-tutorial-sg-$RANDOM_ID" # Create a directory for the key file KEY_DIR=$(mktemp -d) KEY_FILE="$KEY_DIR/$KEY_NAME.pem" log "Starting EC2 basics tutorial script" log "Random identifier: $RANDOM_ID" log "Key name: $KEY_NAME" log "Security group name: $SG_NAME" # Step 1: Create a key pair log "Creating key pair..." KEY_RESULT=$(aws ec2 create-key-pair --key-name "$KEY_NAME" --query 'KeyMaterial' --output text) if [ $? -ne 0 ] || [ -z "$KEY_RESULT" ]; then handle_error "Failed to create key pair" fi echo "$KEY_RESULT" > "$KEY_FILE" chmod 400 "$KEY_FILE" log "Created key pair and saved to $KEY_FILE" # Step 2: Create a security group log "Creating security group..." SECURITY_GROUP_ID=$(aws ec2 create-security-group \ --group-name "$SG_NAME" \ --description "Security group for EC2 tutorial" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=ec2-basics}]' \ --query "GroupId" \ --output text) if [ $? -ne 0 ] || [ -z "$SECURITY_GROUP_ID" ]; then handle_error "Failed to create security group" fi log "Created security group: $SECURITY_GROUP_ID" # Get current public IP address for SSH access MY_IP=$(curl -s http://checkip.amazonaws.com) if [ $? -ne 0 ] || [ -z "$MY_IP" ]; then handle_error "Failed to get current IP address" fi log "Adding SSH ingress rule for IP $MY_IP..." aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 22 \ --cidr "$MY_IP/32" > /dev/null if [ $? -ne 0 ]; then handle_error "Failed to add security group ingress rule" fi log "Added SSH ingress rule for IP $MY_IP" # Step 3: Find an Amazon Linux 2023 AMI (updated from AL2) log "Finding latest Amazon Linux 2023 AMI..." AMI_ID=$(aws ssm get-parameters-by-path \ --path "/aws/service/ami-amazon-linux-latest" \ --query "Parameters[?contains(Name, 'al2023-ami-kernel-default-x86_64')].Value" \ --output text | head -1) if [ $? -ne 0 ] || [ -z "$AMI_ID" ]; then handle_error "Failed to find Amazon Linux 2023 AMI" fi log "Selected AMI: $AMI_ID" # Get the architecture of the AMI log "Getting AMI architecture..." AMI_ARCH=$(aws ec2 describe-images \ --image-ids "$AMI_ID" \ --query "Images[0].Architecture" \ --output text) if [ $? -ne 0 ] || [ -z "$AMI_ARCH" ]; then handle_error "Failed to get AMI architecture" fi log "AMI architecture: $AMI_ARCH" # Find a compatible instance type log "Finding compatible instance type..." # Directly use t2.micro for simplicity INSTANCE_TYPE="t2.micro" log "Using instance type: $INSTANCE_TYPE" # Step 4: Launch an EC2 instance with enhanced security log "Launching EC2 instance with IMDSv2 and encryption enabled..." INSTANCE_ID=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --instance-type "$INSTANCE_TYPE" \ --key-name "$KEY_NAME" \ --security-group-ids "$SECURITY_GROUP_ID" \ --metadata-options "HttpTokens=required,HttpEndpoint=enabled" \ --block-device-mappings "DeviceName=/dev/xvda,Ebs={Encrypted=true}" \ --tag-specifications 'ResourceType=instance,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=ec2-basics}]' 'ResourceType=volume,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=ec2-basics}]' \ --count 1 \ --query 'Instances[0].InstanceId' \ --output text) if [ $? -ne 0 ] || [ -z "$INSTANCE_ID" ]; then handle_error "Failed to launch EC2 instance" fi log "Launched instance $INSTANCE_ID. Waiting for it to start..." # Wait for the instance to be running aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" if [ $? -ne 0 ]; then handle_error "Failed while waiting for instance to start" fi # Get instance details INSTANCE_DETAILS=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query 'Reservations[0].Instances[0].{ID:InstanceId,Type:InstanceType,State:State.Name,PublicIP:PublicIpAddress}' \ --output json) if [ $? -ne 0 ]; then handle_error "Failed to get instance details" fi log "Instance details: $INSTANCE_DETAILS" # Get the public IP address PUBLIC_IP=$(echo "$INSTANCE_DETAILS" | grep -oP '"PublicIP": "\K[^"]+') if [ -z "$PUBLIC_IP" ]; then handle_error "Failed to get instance public IP" fi log "Instance public IP: $PUBLIC_IP" log "To connect to your instance, run: ssh -i $KEY_FILE ec2-user@$PUBLIC_IP" # Pause to allow user to connect if desired sleep 2 # Step 6: Stop and Start the Instance log "Stopping instance $INSTANCE_ID..." aws ec2 stop-instances --instance-ids "$INSTANCE_ID" > /dev/null if [ $? -ne 0 ]; then handle_error "Failed to stop instance" fi log "Waiting for instance to stop..." aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID" if [ $? -ne 0 ]; then handle_error "Failed while waiting for instance to stop" fi log "Instance stopped. Starting instance again..." aws ec2 start-instances --instance-ids "$INSTANCE_ID" > /dev/null if [ $? -ne 0 ]; then handle_error "Failed to start instance" fi log "Waiting for instance to start..." aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" if [ $? -ne 0 ]; then handle_error "Failed while waiting for instance to start" fi # Get the new public IP address NEW_PUBLIC_IP=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query 'Reservations[0].Instances[0].PublicIpAddress' \ --output text) if [ $? -ne 0 ] || [ -z "$NEW_PUBLIC_IP" ]; then handle_error "Failed to get new public IP" fi log "Instance restarted with new public IP: $NEW_PUBLIC_IP" log "To connect to your instance, run: ssh -i $KEY_FILE ec2-user@$NEW_PUBLIC_IP" # Step 7: Allocate and Associate an Elastic IP Address log "Allocating Elastic IP address..." ALLOCATION_RESULT=$(aws ec2 allocate-address \ --domain vpc \ --tag-specifications 'ResourceType=elastic-ip,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=ec2-basics}]' \ --query '[PublicIp,AllocationId]' \ --output text) if [ $? -ne 0 ] || [ -z "$ALLOCATION_RESULT" ]; then handle_error "Failed to allocate Elastic IP" fi ELASTIC_IP=$(echo "$ALLOCATION_RESULT" | awk '{print $1}') ALLOCATION_ID=$(echo "$ALLOCATION_RESULT" | awk '{print $2}') log "Allocated Elastic IP: $ELASTIC_IP with ID: $ALLOCATION_ID" log "Associating Elastic IP with instance..." ASSOCIATION_ID=$(aws ec2 associate-address \ --instance-id "$INSTANCE_ID" \ --allocation-id "$ALLOCATION_ID" \ --query "AssociationId" \ --output text) if [ $? -ne 0 ] || [ -z "$ASSOCIATION_ID" ]; then handle_error "Failed to associate Elastic IP" fi log "Associated Elastic IP with instance. Association ID: $ASSOCIATION_ID" log "To connect to your instance using the Elastic IP, run: ssh -i $KEY_FILE ec2-user@$ELASTIC_IP" # Pause to allow user to connect if desired sleep 2 # Step 8: Test the Elastic IP by Stopping and Starting the Instance log "Stopping instance $INSTANCE_ID to test Elastic IP persistence..." aws ec2 stop-instances --instance-ids "$INSTANCE_ID" > /dev/null if [ $? -ne 0 ]; then handle_error "Failed to stop instance" fi log "Waiting for instance to stop..." aws ec2 wait instance-stopped --instance-ids "$INSTANCE_ID" if [ $? -ne 0 ]; then handle_error "Failed while waiting for instance to stop" fi log "Instance stopped. Starting instance again..." aws ec2 start-instances --instance-ids "$INSTANCE_ID" > /dev/null if [ $? -ne 0 ]; then handle_error "Failed to start instance" fi log "Waiting for instance to start..." aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" if [ $? -ne 0 ]; then handle_error "Failed while waiting for instance to start" fi # Verify the Elastic IP is still associated CURRENT_IP=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query 'Reservations[0].Instances[0].PublicIpAddress' \ --output text) if [ $? -ne 0 ] || [ -z "$CURRENT_IP" ]; then handle_error "Failed to get current public IP" fi log "Current public IP address: $CURRENT_IP" log "Elastic IP address: $ELASTIC_IP" if [ "$CURRENT_IP" = "$ELASTIC_IP" ]; then log "Success! The Elastic IP is still associated with your instance." else log "Something went wrong. The Elastic IP is not associated with your instance." fi log "To connect to your instance, run: ssh -i $KEY_FILE ec2-user@$ELASTIC_IP" # Step 9: Clean up resources log "Tutorial completed successfully!" cleanup exit 0

The following code example shows how to:

  • Create an EC2 key pair

  • Set up storage and prepare your application

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # EMR Getting Started Tutorial Script # This script automates the steps in the Amazon EMR Getting Started tutorial set -euo pipefail # Security: Set strict mode and trap errors trap 'handle_error "Script interrupted or command failed"' ERR # Set up logging with secure permissions LOG_FILE="emr-tutorial.log" touch "$LOG_FILE" chmod 600 "$LOG_FILE" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting Amazon EMR Getting Started Tutorial Script" echo "Logging to $LOG_FILE" # Function to handle errors handle_error() { echo "ERROR: $1" echo "Resources created so far:" if [ -n "${BUCKET_NAME:-}" ]; then echo "- S3 Bucket: $BUCKET_NAME"; fi if [ -n "${CLUSTER_ID:-}" ]; then echo "- EMR Cluster: $CLUSTER_ID"; fi echo "Attempting to clean up resources..." cleanup exit 1 } # Function to clean up resources cleanup() { echo "" echo "===========================================" echo "CLEANUP IN PROGRESS" echo "===========================================" echo "Starting cleanup process..." # Terminate EMR cluster if it exists if [ -n "${CLUSTER_ID:-}" ]; then echo "Terminating EMR cluster: $CLUSTER_ID" aws emr terminate-clusters --cluster-ids "$CLUSTER_ID" 2>/dev/null || true echo "Waiting for cluster to terminate..." aws emr wait cluster-terminated --cluster-id "$CLUSTER_ID" 2>/dev/null || true echo "Cluster terminated successfully." fi # Delete S3 bucket and contents if it exists and is not shared if [ -n "${BUCKET_NAME:-}" ] && [ "${BUCKET_IS_SHARED:-false}" != "true" ]; then echo "Deleting S3 bucket contents: $BUCKET_NAME" aws s3 rm "s3://$BUCKET_NAME" --recursive 2>/dev/null || true echo "Deleting S3 bucket: $BUCKET_NAME" aws s3 rb "s3://$BUCKET_NAME" 2>/dev/null || true fi # Remove temporary key pair file if created by this script if [ -f "${KEY_NAME_FILE:-}" ]; then rm -f "$KEY_NAME_FILE" echo "Removed temporary key pair file." fi echo "Cleanup completed." } # Validate AWS CLI is installed and configured if ! command -v aws &> /dev/null; then handle_error "AWS CLI is not installed" fi # Test AWS credentials if ! aws sts get-caller-identity > /dev/null 2>&1; then handle_error "AWS credentials are not configured or invalid" fi # Generate a random identifier for S3 bucket RANDOM_ID=$(openssl rand -hex 6) # Check for shared prereq bucket PREREQ_BUCKET=$(aws cloudformation describe-stacks --stack-name tutorial-prereqs-bucket \ --query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text 2>/dev/null || true) if [ -n "$PREREQ_BUCKET" ] && [ "$PREREQ_BUCKET" != "None" ]; then BUCKET_NAME="$PREREQ_BUCKET" BUCKET_IS_SHARED=true echo "Using shared bucket: $BUCKET_NAME" else BUCKET_IS_SHARED=false BUCKET_NAME="emr-${RANDOM_ID}" fi echo "Using bucket name: $BUCKET_NAME" # Create S3 bucket with security best practices echo "Creating S3 bucket: $BUCKET_NAME" aws s3 mb "s3://$BUCKET_NAME" --region "${AWS_REGION:-us-east-1}" || handle_error "Failed to create S3 bucket" # Tag the bucket aws s3api put-bucket-tagging --bucket "$BUCKET_NAME" \ --tagging 'TagSet=[{Key=project,Value=doc-smith},{Key=tutorial,Value=emr-gs}]' # Enable bucket versioning for safety aws s3api put-bucket-versioning --bucket "$BUCKET_NAME" --versioning-configuration Status=Enabled || true # Block public access to bucket aws s3api put-public-access-block --bucket "$BUCKET_NAME" \ --public-access-block-configuration \ "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" || true # Enable encryption on bucket aws s3api put-bucket-encryption --bucket "$BUCKET_NAME" \ --server-side-encryption-configuration '{ "Rules": [{ "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } }] }' || true echo "S3 bucket created successfully with security best practices." # Create PySpark script echo "Creating PySpark script: health_violations.py" cat > health_violations.py << 'EOL' import argparse from pyspark.sql import SparkSession def calculate_red_violations(data_source, output_uri): """ Processes sample food establishment inspection data and queries the data to find the top 10 establishments with the most Red violations from 2006 to 2020. :param data_source: The URI of your food establishment data CSV, such as 's3://emr-tutorial-bucket/food-establishment-data.csv'. :param output_uri: The URI where output is written, such as 's3://emr-tutorial-bucket/restaurant_violation_results'. """ with SparkSession.builder.appName("Calculate Red Health Violations").getOrCreate() as spark: # Load the restaurant violation CSV data if data_source is not None: restaurants_df = spark.read.option("header", "true").csv(data_source) # Create an in-memory DataFrame to query restaurants_df.createOrReplaceTempView("restaurant_violations") # Create a DataFrame of the top 10 restaurants with the most Red violations top_red_violation_restaurants = spark.sql("""SELECT name, count(*) AS total_red_violations FROM restaurant_violations WHERE violation_type = 'RED' GROUP BY name ORDER BY total_red_violations DESC LIMIT 10""") # Write the results to the specified output URI top_red_violation_restaurants.write.option("header", "true").mode("overwrite").csv(output_uri) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument( '--data_source', help="The URI for you CSV restaurant data, like an S3 bucket location.") parser.add_argument( '--output_uri', help="The URI where output is saved, like an S3 bucket location.") args = parser.parse_args() calculate_red_violations(args.data_source, args.output_uri) EOL # Secure the script file chmod 600 health_violations.py # Upload PySpark script to S3 echo "Uploading PySpark script to S3" aws s3 cp health_violations.py "s3://$BUCKET_NAME/" --sse AES256 || handle_error "Failed to upload PySpark script" echo "PySpark script uploaded successfully." # Download and prepare sample data echo "Downloading sample data" curl -sS -o food_establishment_data.zip "https://docs.aws.amazon.com/emr/latest/ManagementGuide/samples/food_establishment_data.zip" || handle_error "Failed to download sample data" # Verify downloaded file if [ ! -f food_establishment_data.zip ] || [ ! -s food_establishment_data.zip ]; then handle_error "Downloaded file is empty or missing" fi unzip -o food_establishment_data.zip || handle_error "Failed to unzip sample data" echo "Sample data downloaded and extracted successfully." # Secure the sample data file chmod 600 food_establishment_data.csv # Upload sample data to S3 echo "Uploading sample data to S3" aws s3 cp food_establishment_data.csv "s3://$BUCKET_NAME/" --sse AES256 || handle_error "Failed to upload sample data" echo "Sample data uploaded successfully." # Clean up sensitive local files rm -f food_establishment_data.zip health_violations.py # Create IAM default roles for EMR echo "Creating IAM default roles for EMR" aws emr create-default-roles 2>/dev/null || true echo "IAM default roles created successfully." # Check if EC2 key pair exists echo "Checking for EC2 key pair" KEY_PAIRS=$(aws ec2 describe-key-pairs --query "KeyPairs[*].KeyName" --output text 2>/dev/null || true) if [ -z "$KEY_PAIRS" ]; then echo "No EC2 key pairs found. Creating a new key pair..." KEY_NAME="emr-tutorial-key-${RANDOM_ID}" KEY_NAME_FILE="${KEY_NAME}.pem" aws ec2 create-key-pair --key-name "$KEY_NAME" \ --tag-specifications 'ResourceType=key-pair,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=emr-gs}]' \ --query "KeyMaterial" --output text > "$KEY_NAME_FILE" chmod 400 "$KEY_NAME_FILE" echo "Created new key pair: $KEY_NAME" else # Use the first available key pair KEY_NAME=$(echo "$KEY_PAIRS" | awk '{print $1}') echo "Using existing key pair: $KEY_NAME" fi # Launch EMR cluster with security best practices echo "Launching EMR cluster with Spark" CLUSTER_RESPONSE=$(aws emr create-cluster \ --name "EMR Tutorial Cluster" \ --release-label emr-6.10.0 \ --applications Name=Spark \ --ec2-attributes KeyName="$KEY_NAME" \ --instance-type m5.xlarge \ --instance-count 3 \ --use-default-roles \ --log-uri "s3://$BUCKET_NAME/logs/" \ --ebs-root-volume-size 100 \ --tags Key=project,Value=doc-smith Key=tutorial,Value=emr-gs \ --security-configuration "EMR-Tutorial-SecurityConfig" 2>/dev/null || true) # Check for errors in the response if echo "$CLUSTER_RESPONSE" | grep -i "error" > /dev/null; then handle_error "Failed to create EMR cluster: $CLUSTER_RESPONSE" fi # Extract cluster ID using jq if available, otherwise use alternative parsing if command -v jq &> /dev/null; then CLUSTER_ID=$(echo "$CLUSTER_RESPONSE" | jq -r '.ClusterId // empty') else CLUSTER_ID=$(echo "$CLUSTER_RESPONSE" | grep -o '"ClusterId"[[:space:]]*:[[:space:]]*"[^"]*' | grep -o 'j-[A-Z0-9]*' || true) fi if [ -z "$CLUSTER_ID" ] || [ "$CLUSTER_ID" == "null" ]; then handle_error "Failed to extract cluster ID from response: $CLUSTER_RESPONSE" fi echo "EMR cluster created with ID: $CLUSTER_ID" # Wait for cluster to be ready echo "Waiting for cluster to be ready (this may take several minutes)..." aws emr wait cluster-running --cluster-id "$CLUSTER_ID" || handle_error "Cluster failed to reach running state" # Check if cluster is in WAITING state CLUSTER_STATE=$(aws emr describe-cluster --cluster-id "$CLUSTER_ID" --query "Cluster.Status.State" --output text) if [ "$CLUSTER_STATE" != "WAITING" ]; then echo "Waiting for cluster to reach WAITING state..." WAIT_COUNT=0 MAX_WAIT=120 while [ "$CLUSTER_STATE" != "WAITING" ]; do if [ $WAIT_COUNT -ge $MAX_WAIT ]; then handle_error "Cluster did not reach WAITING state within timeout period" fi sleep 30 CLUSTER_STATE=$(aws emr describe-cluster --cluster-id "$CLUSTER_ID" --query "Cluster.Status.State" --output text) echo "Current cluster state: $CLUSTER_STATE" # Check for error states if [[ "$CLUSTER_STATE" == "TERMINATED_WITH_ERRORS" || "$CLUSTER_STATE" == "TERMINATED" ]]; then handle_error "Cluster entered error state: $CLUSTER_STATE" fi WAIT_COUNT=$((WAIT_COUNT + 1)) done fi echo "Cluster is now in WAITING state and ready to accept work." # Submit Spark application as a step echo "Submitting Spark application as a step" STEP_RESPONSE=$(aws emr add-steps \ --cluster-id "$CLUSTER_ID" \ --steps Type=Spark,Name="Health Violations Analysis",ActionOnFailure=CONTINUE,Args=["s3://$BUCKET_NAME/health_violations.py","--data_source","s3://$BUCKET_NAME/food_establishment_data.csv","--output_uri","s3://$BUCKET_NAME/results/"]) # Check for errors in the response if echo "$STEP_RESPONSE" | grep -i "error" > /dev/null; then handle_error "Failed to submit step: $STEP_RESPONSE" fi # Extract step ID using appropriate method if command -v jq &> /dev/null; then STEP_ID=$(echo "$STEP_RESPONSE" | jq -r '.StepIds[0] // empty') else STEP_ID=$(echo "$STEP_RESPONSE" | grep -o 's-[A-Z0-9]*' | head -1 || true) fi if [ -z "$STEP_ID" ] || [ "$STEP_ID" == "null" ]; then echo "Full step response: $STEP_RESPONSE" handle_error "Failed to extract valid step ID from response" fi echo "Step submitted with ID: $STEP_ID" # Wait for step to complete with timeout echo "Waiting for step to complete (this may take several minutes)..." aws emr wait step-complete --cluster-id "$CLUSTER_ID" --step-id "$STEP_ID" || handle_error "Step failed to complete" # Check step status STEP_STATE=$(aws emr describe-step --cluster-id "$CLUSTER_ID" --step-id "$STEP_ID" --query "Step.Status.State" --output text) if [ "$STEP_STATE" != "COMPLETED" ]; then handle_error "Step did not complete successfully. Final state: $STEP_STATE" fi echo "Step completed successfully." # View results echo "Listing output files in S3" aws s3 ls "s3://$BUCKET_NAME/results/" || handle_error "Failed to list output files" # Download results echo "Downloading results file" RESULT_FILE=$(aws s3 ls "s3://$BUCKET_NAME/results/" | grep -o "part-[0-9]*\.csv" | head -1 || true) if [ -z "$RESULT_FILE" ]; then echo "No result file found with pattern 'part-[0-9]*.csv'. Trying to find any CSV file..." RESULT_FILE=$(aws s3 ls "s3://$BUCKET_NAME/results/" | grep -o "part-.*\.csv" | head -1 || true) if [ -z "$RESULT_FILE" ]; then echo "Listing all files in results directory:" aws s3 ls "s3://$BUCKET_NAME/results/" handle_error "No result file found in the output directory" fi fi aws s3 cp "s3://$BUCKET_NAME/results/$RESULT_FILE" ./results.csv --sse AES256 || handle_error "Failed to download results file" chmod 600 ./results.csv echo "Results downloaded to results.csv" echo "Top 10 establishments with the most red violations:" cat results.csv # Display SSH connection information echo "" echo "To connect to the cluster via SSH, use the following command:" echo "aws emr ssh --cluster-id $CLUSTER_ID --key-pair-file ${KEY_NAME_FILE:-./${KEY_NAME}.pem}" # Display summary of created resources echo "" echo "===========================================" echo "RESOURCES CREATED" echo "===========================================" echo "- S3 Bucket: $BUCKET_NAME" echo "- EMR Cluster: $CLUSTER_ID" echo "- Results file: results.csv" if [ -f "${KEY_NAME_FILE:-}" ]; then echo "- EC2 Key Pair: $KEY_NAME (saved to ${KEY_NAME_FILE})" fi # Perform cleanup cleanup echo "Script completed successfully."

The following code example shows how to:

  • Set up security group for ElastiCache access

  • Create a Valkey serverless cache

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Amazon ElastiCache Getting Started Script # This script creates a Valkey serverless cache, configures security groups, # and demonstrates how to connect to and use the cache. set -uo pipefail # Set up logging LOG_FILE="elasticache_tutorial_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting ElastiCache tutorial script. Logging to $LOG_FILE" echo "============================================================" # Function to handle errors handle_error() { echo "ERROR: $1" echo "Resources created:" if [ -n "${CACHE_NAME:-}" ]; then echo "- ElastiCache serverless cache: $CACHE_NAME" fi if [ -n "${SG_RULE_6379:-}" ] || [ -n "${SG_RULE_6380:-}" ]; then echo "- Security group rules for ports 6379 and 6380" fi echo "Please clean up these resources manually." exit 1 } # Validate AWS CLI is installed and configured if ! command -v aws &> /dev/null; then handle_error "AWS CLI is not installed or not in PATH" fi # Check AWS credentials are configured if ! aws sts get-caller-identity &> /dev/null; then handle_error "AWS credentials are not configured or invalid" fi # Generate a random identifier for resource names RANDOM_ID=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | fold -w 8 | head -n 1) CACHE_NAME="valkey-cache-${RANDOM_ID}" echo "Using cache name: $CACHE_NAME" # Step 1: Set up security group for ElastiCache access echo "Step 1: Setting up security group for ElastiCache access..." # Get default security group ID echo "Getting default security group ID..." SG_ID=$(aws ec2 describe-security-groups \ --filters Name=group-name,Values=default \ --query "SecurityGroups[0].GroupId" \ --output text 2>/dev/null || echo "") if [[ -z "$SG_ID" || "$SG_ID" == "None" ]]; then handle_error "Failed to get default security group ID" fi echo "Default security group ID: $SG_ID" # Add inbound rule for port 6379 echo "Adding inbound rule for port 6379..." SG_RULE_6379="" if SG_RULE_6379=$(aws ec2 authorize-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port 6379 \ --cidr 0.0.0.0/0 \ --query "SecurityGroupRules[0].SecurityGroupRuleId" \ --output text 2>&1); then if [[ "$SG_RULE_6379" == *"InvalidGroup.Duplicate"* ]] || [[ "$SG_RULE_6379" == *"already exists"* ]]; then echo "Rule for port 6379 already exists, continuing..." SG_RULE_6379="existing" fi else if [[ "$SG_RULE_6379" == *"InvalidGroup.Duplicate"* ]] || [[ "$SG_RULE_6379" == *"already exists"* ]]; then echo "Rule for port 6379 already exists, continuing..." SG_RULE_6379="existing" else handle_error "Failed to add security group rule for port 6379: $SG_RULE_6379" fi fi # Add inbound rule for port 6380 echo "Adding inbound rule for port 6380..." SG_RULE_6380="" if SG_RULE_6380=$(aws ec2 authorize-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port 6380 \ --cidr 0.0.0.0/0 \ --query "SecurityGroupRules[0].SecurityGroupRuleId" \ --output text 2>&1); then if [[ "$SG_RULE_6380" == *"InvalidGroup.Duplicate"* ]] || [[ "$SG_RULE_6380" == *"already exists"* ]]; then echo "Rule for port 6380 already exists, continuing..." SG_RULE_6380="existing" fi else if [[ "$SG_RULE_6380" == *"InvalidGroup.Duplicate"* ]] || [[ "$SG_RULE_6380" == *"already exists"* ]]; then echo "Rule for port 6380 already exists, continuing..." SG_RULE_6380="existing" else handle_error "Failed to add security group rule for port 6380: $SG_RULE_6380" fi fi echo "Security group rules added successfully." echo "" echo "⚠️ SECURITY WARNING: The security group rules created allow access from any IP address (0.0.0.0/0)." echo "This is NOT RECOMMENDED for production environments. For production," echo "you should restrict access to specific IP ranges or security groups." echo "Update the CIDR blocks in this script before using in production." echo "" # Step 2: Create a Valkey serverless cache echo "Step 2: Creating Valkey serverless cache..." if ! CREATE_RESULT=$(aws elasticache create-serverless-cache \ --serverless-cache-name "$CACHE_NAME" \ --engine valkey \ --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-elasticache-gs 2>&1); then handle_error "Failed to create serverless cache: $CREATE_RESULT" fi echo "Cache creation initiated. Waiting for cache to become available..." # Step 3: Check the status of the cache creation echo "Step 3: Checking cache status..." # Wait for the cache to become active MAX_ATTEMPTS=30 ATTEMPT=1 CACHE_STATUS="" while [[ $ATTEMPT -le $MAX_ATTEMPTS ]]; do echo "Checking cache status (attempt $ATTEMPT of $MAX_ATTEMPTS)..." if ! DESCRIBE_RESULT=$(aws elasticache describe-serverless-caches \ --serverless-cache-name "$CACHE_NAME" 2>&1); then handle_error "Failed to describe serverless cache: $DESCRIBE_RESULT" fi # Extract status using jq for reliable JSON parsing if command -v jq &> /dev/null; then CACHE_STATUS=$(echo "$DESCRIBE_RESULT" | jq -r '.ServerlessCaches[0].Status // "UNKNOWN"' 2>/dev/null || echo "") else CACHE_STATUS=$(echo "$DESCRIBE_RESULT" | grep -o '"Status": "[^"]*"' | awk -F'"' '{print $4}' | head -n 1) fi echo "Current status: $CACHE_STATUS" if [[ "${CACHE_STATUS,,}" == "available" ]]; then echo "Cache is now available!" break elif [[ "${CACHE_STATUS,,}" == "create-failed" ]]; then handle_error "Cache creation failed. Please check the AWS console for details." fi if [[ $ATTEMPT -lt $MAX_ATTEMPTS ]]; then echo "Waiting 30 seconds..." sleep 30 fi ((ATTEMPT++)) done if [[ "${CACHE_STATUS,,}" != "available" ]]; then handle_error "Cache did not become available within the expected time. Last status: $CACHE_STATUS" fi # Step 4: Find your cache endpoint echo "Step 4: Getting cache endpoint..." if ! ENDPOINT=$(aws elasticache describe-serverless-caches \ --serverless-cache-name "$CACHE_NAME" \ --query "ServerlessCaches[0].Endpoint.Address" \ --output text 2>&1); then handle_error "Failed to get cache endpoint: $ENDPOINT" fi if [[ -z "$ENDPOINT" || "$ENDPOINT" == "None" ]]; then handle_error "Failed to get cache endpoint" fi echo "Cache endpoint: $ENDPOINT" # Step 5: Instructions for connecting to the cache echo "" echo "============================================================" echo "Your Valkey serverless cache has been successfully created!" echo "Cache Name: $CACHE_NAME" echo "Endpoint: $ENDPOINT" echo "============================================================" echo "" echo "To connect to your cache from an EC2 instance, follow these steps:" echo "" echo "1. Install valkey-cli on your EC2 instance:" echo " sudo amazon-linux-extras install epel -y" echo " sudo yum install gcc jemalloc-devel openssl-devel tcl tcl-devel -y" echo " wget https://github.com/valkey-io/valkey/archive/refs/tags/8.0.0.tar.gz" echo " tar xvzf 8.0.0.tar.gz" echo " cd valkey-8.0.0" echo " make BUILD_TLS=yes" echo "" echo "2. Connect to your cache using valkey-cli:" echo " src/valkey-cli -h $ENDPOINT --tls -p 6379" echo "" echo "3. Once connected, you can run commands like:" echo " set mykey \"Hello ElastiCache\"" echo " get mykey" echo "" # Auto-confirm cleanup echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Resources created:" echo "- ElastiCache serverless cache: $CACHE_NAME" if [[ "${SG_RULE_6379:-}" != "existing" ]] || [[ "${SG_RULE_6380:-}" != "existing" ]]; then echo "- Security group rules for ports 6379 and 6380" fi echo "" echo "Proceeding with cleanup..." CLEANUP_CHOICE="y" if [[ "${CLEANUP_CHOICE,,}" == "y" ]]; then echo "Starting cleanup process..." # Step 7: Delete the cache echo "Deleting serverless cache $CACHE_NAME..." if ! DELETE_RESULT=$(aws elasticache delete-serverless-cache \ --serverless-cache-name "$CACHE_NAME" 2>&1); then echo "WARNING: Failed to delete serverless cache: $DELETE_RESULT" echo "Please delete the cache manually from the AWS console." else echo "Cache deletion initiated. This may take several minutes to complete." fi # Only attempt to remove security group rules if we created them if [[ "${SG_RULE_6379:-}" != "existing" ]]; then echo "Removing security group rule for port 6379..." if ! aws ec2 revoke-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port 6379 \ --cidr 0.0.0.0/0 2>&1; then echo "WARNING: Failed to remove security group rule for port 6379" fi fi if [[ "${SG_RULE_6380:-}" != "existing" ]]; then echo "Removing security group rule for port 6380..." if ! aws ec2 revoke-security-group-ingress \ --group-id "$SG_ID" \ --protocol tcp \ --port 6380 \ --cidr 0.0.0.0/0 2>&1; then echo "WARNING: Failed to remove security group rule for port 6380" fi fi echo "Cleanup completed." fi echo "" echo "Script completed. See $LOG_FILE for the full log." echo "============================================================"

The following code example shows how to:

  • Create an MSK cluster

  • Create IAM permissions for MSK access

  • Create a client machine

  • Get bootstrap brokers

  • Set up the client machine

  • Create a topic and produce/consume data

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Amazon MSK Getting Started Tutorial Script - Version 8 # This script automates the steps in the Amazon MSK Getting Started tutorial # It creates an MSK cluster, sets up IAM permissions, creates a client machine, # and configures the client to interact with the cluster # Set up logging LOG_FILE="msk_tutorial_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting Amazon MSK Getting Started Tutorial Script - Version 8" echo "Logging to $LOG_FILE" echo "==============================================" # Function to handle errors handle_error() { echo "ERROR: $1" echo "Resources created so far:" if [ -n "$CLUSTER_ARN" ]; then echo "- MSK Cluster: $CLUSTER_ARN"; fi if [ -n "$POLICY_ARN" ]; then echo "- IAM Policy: $POLICY_ARN"; fi if [ -n "$ROLE_NAME" ]; then echo "- IAM Role: $ROLE_NAME"; fi if [ -n "$INSTANCE_PROFILE_NAME" ]; then echo "- IAM Instance Profile: $INSTANCE_PROFILE_NAME"; fi if [ -n "$CLIENT_SG_ID" ]; then echo "- Client Security Group: $CLIENT_SG_ID"; fi if [ -n "$INSTANCE_ID" ]; then echo "- EC2 Instance: $INSTANCE_ID"; fi if [ -n "$KEY_NAME" ]; then echo "- Key Pair: $KEY_NAME"; fi echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to check if a resource exists resource_exists() { local resource_type="$1" local resource_id="$2" case "$resource_type" in "cluster") aws kafka describe-cluster --cluster-arn "$resource_id" &>/dev/null ;; "policy") aws iam get-policy --policy-arn "$resource_id" &>/dev/null ;; "role") aws iam get-role --role-name "$resource_id" &>/dev/null ;; "instance-profile") aws iam get-instance-profile --instance-profile-name "$resource_id" &>/dev/null ;; "security-group") aws ec2 describe-security-groups --group-ids "$resource_id" &>/dev/null ;; "instance") aws ec2 describe-instances --instance-ids "$resource_id" --query 'Reservations[0].Instances[0].State.Name' --output text | grep -v "terminated" &>/dev/null ;; "key-pair") aws ec2 describe-key-pairs --key-names "$resource_id" &>/dev/null ;; esac } # Function to remove security group references remove_security_group_references() { local sg_id="$1" if [ -z "$sg_id" ]; then echo "No security group ID provided for reference removal" return fi echo "Removing security group references for $sg_id" # Get all security groups in the VPC that might reference our client security group local vpc_security_groups=$(aws ec2 describe-security-groups \ --filters "Name=vpc-id,Values=$DEFAULT_VPC_ID" \ --query 'SecurityGroups[].GroupId' \ --output text 2>/dev/null) if [ -n "$vpc_security_groups" ]; then for other_sg in $vpc_security_groups; do if [ "$other_sg" != "$sg_id" ]; then echo "Checking security group $other_sg for references to $sg_id" # Get the security group details in JSON format local sg_details=$(aws ec2 describe-security-groups \ --group-ids "$other_sg" \ --output json 2>/dev/null) if [ -n "$sg_details" ]; then # Check if our security group is referenced in inbound rules local has_inbound_ref=$(echo "$sg_details" | grep -o "\"GroupId\": \"$sg_id\"" | head -1) if [ -n "$has_inbound_ref" ]; then echo "Found inbound rules in $other_sg referencing $sg_id, removing them..." # Try to remove common rule types echo "Attempting to remove all-traffic rule" aws ec2 revoke-security-group-ingress \ --group-id "$other_sg" \ --protocol all \ --source-group "$sg_id" 2>/dev/null || echo "No all-traffic rule to remove" # Try to remove TCP rules on common ports for port in 22 80 443 9092 9094 9096; do aws ec2 revoke-security-group-ingress \ --group-id "$other_sg" \ --protocol tcp \ --port "$port" \ --source-group "$sg_id" 2>/dev/null || true done # Try to remove UDP rules aws ec2 revoke-security-group-ingress \ --group-id "$other_sg" \ --protocol udp \ --source-group "$sg_id" 2>/dev/null || true fi # Check for outbound rules (less common but possible) local has_outbound_ref=$(echo "$sg_details" | grep -A 20 "IpPermissionsEgress" | grep -o "\"GroupId\": \"$sg_id\"" | head -1) if [ -n "$has_outbound_ref" ]; then echo "Found outbound rules in $other_sg referencing $sg_id, removing them..." aws ec2 revoke-security-group-egress \ --group-id "$other_sg" \ --protocol all \ --source-group "$sg_id" 2>/dev/null || echo "No outbound all-traffic rule to remove" fi fi fi done fi echo "Completed security group reference removal for $sg_id" } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources..." # Delete EC2 instance if it exists if [ -n "$INSTANCE_ID" ] && resource_exists "instance" "$INSTANCE_ID"; then echo "Terminating EC2 instance: $INSTANCE_ID" aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" || echo "Failed to terminate instance" echo "Waiting for instance to terminate..." aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" || echo "Failed to wait for instance termination" fi # Delete MSK cluster first (to remove dependencies on security group) if [ -n "$CLUSTER_ARN" ] && resource_exists "cluster" "$CLUSTER_ARN"; then echo "Deleting MSK cluster: $CLUSTER_ARN" aws kafka delete-cluster --cluster-arn "$CLUSTER_ARN" || echo "Failed to delete cluster" # Wait a bit for the cluster deletion to start echo "Waiting 30 seconds for cluster deletion to begin..." sleep 30 fi # Remove security group references before attempting deletion if [ -n "$CLIENT_SG_ID" ] && resource_exists "security-group" "$CLIENT_SG_ID"; then remove_security_group_references "$CLIENT_SG_ID" echo "Deleting security group: $CLIENT_SG_ID" # Try multiple times with longer delays to ensure dependencies are removed for i in {1..10}; do if aws ec2 delete-security-group --group-id "$CLIENT_SG_ID"; then echo "Security group deleted successfully" break fi echo "Failed to delete security group (attempt $i/10), retrying in 30 seconds..." sleep 30 done fi # Delete key pair if it exists if [ -n "$KEY_NAME" ] && resource_exists "key-pair" "$KEY_NAME"; then echo "Deleting key pair: $KEY_NAME" aws ec2 delete-key-pair --key-name "$KEY_NAME" || echo "Failed to delete key pair" fi # Remove role from instance profile if [ -n "$ROLE_NAME" ] && [ -n "$INSTANCE_PROFILE_NAME" ] && resource_exists "instance-profile" "$INSTANCE_PROFILE_NAME"; then echo "Removing role from instance profile" aws iam remove-role-from-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME" \ --role-name "$ROLE_NAME" || echo "Failed to remove role from instance profile" fi # Delete instance profile if [ -n "$INSTANCE_PROFILE_NAME" ] && resource_exists "instance-profile" "$INSTANCE_PROFILE_NAME"; then echo "Deleting instance profile: $INSTANCE_PROFILE_NAME" aws iam delete-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME" || echo "Failed to delete instance profile" fi # Detach policy from role if [ -n "$ROLE_NAME" ] && [ -n "$POLICY_ARN" ] && resource_exists "role" "$ROLE_NAME"; then echo "Detaching policy from role" aws iam detach-role-policy \ --role-name "$ROLE_NAME" \ --policy-arn "$POLICY_ARN" || echo "Failed to detach policy" fi # Delete role if [ -n "$ROLE_NAME" ] && resource_exists "role" "$ROLE_NAME"; then echo "Deleting role: $ROLE_NAME" aws iam delete-role --role-name "$ROLE_NAME" || echo "Failed to delete role" fi # Delete policy if [ -n "$POLICY_ARN" ] && resource_exists "policy" "$POLICY_ARN"; then echo "Deleting policy: $POLICY_ARN" aws iam delete-policy --policy-arn "$POLICY_ARN" || echo "Failed to delete policy" fi echo "Cleanup completed" } # Function to find a suitable subnet and instance type combination find_suitable_subnet_and_instance_type() { local vpc_id="$1" local -a subnet_array=("${!2}") # List of instance types to try, in order of preference local instance_types=("t3.micro" "t2.micro" "t3.small" "t2.small") echo "Finding suitable subnet and instance type combination..." for instance_type in "${instance_types[@]}"; do echo "Trying instance type: $instance_type" for subnet_id in "${subnet_array[@]}"; do # Get the availability zone for this subnet local az=$(aws ec2 describe-subnets \ --subnet-ids "$subnet_id" \ --query 'Subnets[0].AvailabilityZone' \ --output text) echo " Checking subnet $subnet_id in AZ $az" # Check if this instance type is available in this AZ local available=$(aws ec2 describe-instance-type-offerings \ --location-type availability-zone \ --filters "Name=location,Values=$az" "Name=instance-type,Values=$instance_type" \ --query 'InstanceTypeOfferings[0].InstanceType' \ --output text 2>/dev/null) if [ "$available" = "$instance_type" ]; then echo " ✓ Found suitable combination: $instance_type in $az (subnet: $subnet_id)" SELECTED_SUBNET_ID="$subnet_id" SELECTED_INSTANCE_TYPE="$instance_type" return 0 else echo " ✗ $instance_type not available in $az" fi done done echo "ERROR: Could not find any suitable subnet and instance type combination" return 1 } # Generate unique identifiers RANDOM_SUFFIX=$(LC_ALL=C tr -dc 'a-z0-9' < /dev/urandom | fold -w 8 | head -n 1) CLUSTER_NAME="MSKTutorialCluster-${RANDOM_SUFFIX}" POLICY_NAME="msk-tutorial-policy-${RANDOM_SUFFIX}" ROLE_NAME="msk-tutorial-role-${RANDOM_SUFFIX}" INSTANCE_PROFILE_NAME="msk-tutorial-profile-${RANDOM_SUFFIX}" SG_NAME="MSKClientSecurityGroup-${RANDOM_SUFFIX}" echo "Using the following resource names:" echo "- Cluster Name: $CLUSTER_NAME" echo "- Policy Name: $POLICY_NAME" echo "- Role Name: $ROLE_NAME" echo "- Instance Profile Name: $INSTANCE_PROFILE_NAME" echo "- Security Group Name: $SG_NAME" echo "==============================================" # Step 1: Create an MSK Provisioned cluster echo "Step 1: Creating MSK Provisioned cluster" # Get the default VPC ID first echo "Getting default VPC..." DEFAULT_VPC_ID=$(aws ec2 describe-vpcs \ --filters "Name=is-default,Values=true" \ --query "Vpcs[0].VpcId" \ --output text) if [ -z "$DEFAULT_VPC_ID" ] || [ "$DEFAULT_VPC_ID" = "None" ]; then handle_error "Could not find default VPC. Please ensure you have a default VPC in your region." fi echo "Default VPC ID: $DEFAULT_VPC_ID" # Get available subnets in the default VPC echo "Getting available subnets in the default VPC..." SUBNETS=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=$DEFAULT_VPC_ID" "Name=default-for-az,Values=true" \ --query "Subnets[0:3].SubnetId" \ --output text) # Convert space-separated subnet IDs to an array read -r -a SUBNET_ARRAY <<< "$SUBNETS" if [ ${#SUBNET_ARRAY[@]} -lt 3 ]; then handle_error "Not enough subnets available in the default VPC. Need at least 3 subnets, found ${#SUBNET_ARRAY[@]}." fi # Get default security group for the default VPC echo "Getting default security group for the default VPC..." DEFAULT_SG=$(aws ec2 describe-security-groups \ --filters "Name=group-name,Values=default" "Name=vpc-id,Values=$DEFAULT_VPC_ID" \ --query "SecurityGroups[0].GroupId" \ --output text) if [ -z "$DEFAULT_SG" ] || [ "$DEFAULT_SG" = "None" ]; then handle_error "Could not find default security group for VPC $DEFAULT_VPC_ID" fi echo "Creating MSK cluster: $CLUSTER_NAME" echo "Using VPC: $DEFAULT_VPC_ID" echo "Using subnets: ${SUBNET_ARRAY[0]} ${SUBNET_ARRAY[1]} ${SUBNET_ARRAY[2]}" echo "Using security group: $DEFAULT_SG" # Create the MSK cluster with proper error handling CLUSTER_RESPONSE=$(aws kafka create-cluster \ --cluster-name "$CLUSTER_NAME" \ --broker-node-group-info "{\"InstanceType\": \"kafka.t3.small\", \"ClientSubnets\": [\"${SUBNET_ARRAY[0]}\", \"${SUBNET_ARRAY[1]}\", \"${SUBNET_ARRAY[2]}\"], \"SecurityGroups\": [\"$DEFAULT_SG\"]}" \ --kafka-version "3.6.0" \ --number-of-broker-nodes 3 \ --encryption-info "{\"EncryptionInTransit\": {\"InCluster\": true, \"ClientBroker\": \"TLS\"}}" \ --tags project=doc-smith,tutorial=amazon-managed-streaming-for-apache-kafka-gs 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to create MSK cluster: $CLUSTER_RESPONSE" fi # Extract the cluster ARN using grep CLUSTER_ARN=$(echo "$CLUSTER_RESPONSE" | grep -o '"ClusterArn": "[^"]*' | cut -d'"' -f4) if [ -z "$CLUSTER_ARN" ]; then handle_error "Failed to extract cluster ARN from response: $CLUSTER_RESPONSE" fi echo "MSK cluster creation initiated. ARN: $CLUSTER_ARN" echo "Waiting for cluster to become active (this may take 15-20 minutes)..." # Wait for the cluster to become active while true; do CLUSTER_STATUS=$(aws kafka describe-cluster --cluster-arn "$CLUSTER_ARN" --query "ClusterInfo.State" --output text 2>/dev/null) if [ $? -ne 0 ]; then echo "Failed to get cluster status. Retrying in 30 seconds..." sleep 30 continue fi echo "Current cluster status: $CLUSTER_STATUS" if [ "$CLUSTER_STATUS" = "ACTIVE" ]; then echo "Cluster is now active!" break elif [ "$CLUSTER_STATUS" = "FAILED" ]; then handle_error "Cluster creation failed" fi echo "Still waiting for cluster to become active... (checking again in 60 seconds)" sleep 60 done echo "==============================================" # Step 2: Create an IAM role granting access to create topics on the Amazon MSK cluster echo "Step 2: Creating IAM policy and role" # Get account ID and region ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) REGION=$(aws configure get region) if [ -z "$REGION" ]; then REGION=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[0].RegionName' --output text) fi if [ -z "$ACCOUNT_ID" ] || [ -z "$REGION" ]; then handle_error "Could not determine AWS account ID or region" fi echo "Account ID: $ACCOUNT_ID" echo "Region: $REGION" # Create IAM policy echo "Creating IAM policy: $POLICY_NAME" POLICY_DOCUMENT="{ \"Version\": \"2012-10-17\", \"Statement\": [ { \"Effect\": \"Allow\", \"Action\": [ \"kafka-cluster:Connect\", \"kafka-cluster:AlterCluster\", \"kafka-cluster:DescribeCluster\" ], \"Resource\": [ \"$CLUSTER_ARN\" ] }, { \"Effect\": \"Allow\", \"Action\": [ \"kafka-cluster:*Topic*\", \"kafka-cluster:WriteData\", \"kafka-cluster:ReadData\" ], \"Resource\": [ \"arn:aws:kafka:$REGION:$ACCOUNT_ID:topic/$CLUSTER_NAME/*\" ] }, { \"Effect\": \"Allow\", \"Action\": [ \"kafka-cluster:AlterGroup\", \"kafka-cluster:DescribeGroup\" ], \"Resource\": [ \"arn:aws:kafka:$REGION:$ACCOUNT_ID:group/$CLUSTER_NAME/*\" ] } ] }" POLICY_RESPONSE=$(aws iam create-policy \ --policy-name "$POLICY_NAME" \ --policy-document "$POLICY_DOCUMENT" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to create IAM policy: $POLICY_RESPONSE" fi # Extract the policy ARN using grep POLICY_ARN=$(echo "$POLICY_RESPONSE" | grep -o '"Arn": "[^"]*' | cut -d'"' -f4) if [ -z "$POLICY_ARN" ]; then handle_error "Failed to extract policy ARN from response: $POLICY_RESPONSE" fi aws iam tag-policy --policy-arn "$POLICY_ARN" \ --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-managed-streaming-for-apache-kafka-gs echo "IAM policy created. ARN: $POLICY_ARN" # Create IAM role for EC2 echo "Creating IAM role: $ROLE_NAME" TRUST_POLICY="{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}" ROLE_RESPONSE=$(aws iam create-role \ --role-name "$ROLE_NAME" \ --assume-role-policy-document "$TRUST_POLICY" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to create IAM role: $ROLE_RESPONSE" fi echo "IAM role created: $ROLE_NAME" aws iam tag-role --role-name "$ROLE_NAME" \ --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-managed-streaming-for-apache-kafka-gs # Attach policy to role echo "Attaching policy to role" ATTACH_RESPONSE=$(aws iam attach-role-policy \ --role-name "$ROLE_NAME" \ --policy-arn "$POLICY_ARN" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to attach policy to role: $ATTACH_RESPONSE" fi echo "Policy attached to role" # Create instance profile and add role to it echo "Creating instance profile: $INSTANCE_PROFILE_NAME" PROFILE_RESPONSE=$(aws iam create-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to create instance profile: $PROFILE_RESPONSE" fi echo "Instance profile created" echo "Adding role to instance profile" ADD_ROLE_RESPONSE=$(aws iam add-role-to-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME" \ --role-name "$ROLE_NAME" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to add role to instance profile: $ADD_ROLE_RESPONSE" fi echo "Role added to instance profile" # Wait a moment for IAM propagation echo "Waiting 10 seconds for IAM propagation..." sleep 10 echo "==============================================" # Step 3: Create a client machine echo "Step 3: Creating client machine" # Find a suitable subnet and instance type combination if ! find_suitable_subnet_and_instance_type "$DEFAULT_VPC_ID" SUBNET_ARRAY[@]; then handle_error "Could not find a suitable subnet and instance type combination" fi echo "Selected subnet: $SELECTED_SUBNET_ID" echo "Selected instance type: $SELECTED_INSTANCE_TYPE" # Verify the subnet is in the same VPC we're using SUBNET_VPC_ID=$(aws ec2 describe-subnets \ --subnet-ids "$SELECTED_SUBNET_ID" \ --query 'Subnets[0].VpcId' \ --output text) if [ "$SUBNET_VPC_ID" != "$DEFAULT_VPC_ID" ]; then handle_error "Subnet VPC ($SUBNET_VPC_ID) does not match default VPC ($DEFAULT_VPC_ID)" fi echo "VPC ID: $SUBNET_VPC_ID" # Get security group ID from the MSK cluster echo "Getting security group ID from the MSK cluster" MSK_SG_ID=$(aws kafka describe-cluster \ --cluster-arn "$CLUSTER_ARN" \ --query 'ClusterInfo.BrokerNodeGroupInfo.SecurityGroups[0]' \ --output text) if [ -z "$MSK_SG_ID" ] || [ "$MSK_SG_ID" = "None" ]; then handle_error "Failed to get security group ID from cluster" fi echo "MSK security group ID: $MSK_SG_ID" # Create security group for client echo "Creating security group for client: $SG_NAME" CLIENT_SG_RESPONSE=$(aws ec2 create-security-group \ --group-name "$SG_NAME" \ --description "Security group for MSK client" \ --vpc-id "$DEFAULT_VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-managed-streaming-for-apache-kafka-gs}]' 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to create security group: $CLIENT_SG_RESPONSE" fi # Extract the security group ID using grep CLIENT_SG_ID=$(echo "$CLIENT_SG_RESPONSE" | grep -o '"GroupId": "[^"]*' | cut -d'"' -f4) if [ -z "$CLIENT_SG_ID" ]; then handle_error "Failed to extract security group ID from response: $CLIENT_SG_RESPONSE" fi echo "Client security group created. ID: $CLIENT_SG_ID" # Allow SSH access to client from your IP only echo "Getting your public IP address" MY_IP=$(curl -s https://checkip.amazonaws.com 2>/dev/null) if [ -z "$MY_IP" ]; then echo "Warning: Could not determine your IP address. Using 0.0.0.0/0 (not recommended for production)" MY_IP="0.0.0.0/0" else MY_IP="$MY_IP/32" echo "Your IP address: $MY_IP" fi echo "Adding SSH ingress rule to client security group" SSH_RULE_RESPONSE=$(aws ec2 authorize-security-group-ingress \ --group-id "$CLIENT_SG_ID" \ --protocol tcp \ --port 22 \ --cidr "$MY_IP" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then echo "Warning: Failed to add SSH ingress rule: $SSH_RULE_RESPONSE" echo "You may need to manually add SSH access to security group $CLIENT_SG_ID" fi echo "SSH ingress rule added" # Update MSK security group to allow traffic from client security group echo "Adding ingress rule to MSK security group to allow traffic from client" MSK_RULE_RESPONSE=$(aws ec2 authorize-security-group-ingress \ --group-id "$MSK_SG_ID" \ --protocol all \ --source-group "$CLIENT_SG_ID" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then echo "Warning: Failed to add ingress rule to MSK security group: $MSK_RULE_RESPONSE" echo "You may need to manually add ingress rule to security group $MSK_SG_ID" fi echo "Ingress rule added to MSK security group" # Create key pair KEY_NAME="MSKKeyPair-${RANDOM_SUFFIX}" echo "Creating key pair: $KEY_NAME" KEY_RESPONSE=$(aws ec2 create-key-pair --key-name "$KEY_NAME" --tag-specifications 'ResourceType=key-pair,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-managed-streaming-for-apache-kafka-gs}]' --query 'KeyMaterial' --output text 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to create key pair: $KEY_RESPONSE" fi # Save the private key to a file KEY_FILE="${KEY_NAME}.pem" echo "$KEY_RESPONSE" > "$KEY_FILE" chmod 400 "$KEY_FILE" echo "Key pair created and saved to $KEY_FILE" # Get the latest Amazon Linux 2 AMI echo "Getting latest Amazon Linux 2 AMI ID" AMI_ID=$(aws ec2 describe-images \ --owners amazon \ --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" "Name=state,Values=available" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" \ --output text 2>/dev/null) if [ -z "$AMI_ID" ] || [ "$AMI_ID" = "None" ]; then handle_error "Failed to get Amazon Linux 2 AMI ID" fi echo "Using AMI ID: $AMI_ID" # Launch EC2 instance with the selected subnet and instance type echo "Launching EC2 instance" echo "Instance type: $SELECTED_INSTANCE_TYPE" echo "Subnet: $SELECTED_SUBNET_ID" INSTANCE_RESPONSE=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --instance-type "$SELECTED_INSTANCE_TYPE" \ --key-name "$KEY_NAME" \ --security-group-ids "$CLIENT_SG_ID" \ --subnet-id "$SELECTED_SUBNET_ID" \ --iam-instance-profile "Name=$INSTANCE_PROFILE_NAME" \ --tag-specifications "ResourceType=instance,Tags=[{Key=Name,Value=MSKTutorialClient-${RANDOM_SUFFIX}},{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-managed-streaming-for-apache-kafka-gs}]" 2>&1) # Check if the command was successful if [ $? -ne 0 ]; then handle_error "Failed to launch EC2 instance: $INSTANCE_RESPONSE" fi # Extract the instance ID using grep INSTANCE_ID=$(echo "$INSTANCE_RESPONSE" | grep -o '"InstanceId": "[^"]*' | head -1 | cut -d'"' -f4) if [ -z "$INSTANCE_ID" ]; then handle_error "Failed to extract instance ID from response: $INSTANCE_RESPONSE" fi echo "EC2 instance launched successfully. ID: $INSTANCE_ID" echo "Waiting for instance to be running..." # Wait for the instance to be running aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" if [ $? -ne 0 ]; then handle_error "Instance failed to reach running state" fi # Wait a bit more for the instance to initialize echo "Instance is running. Waiting 30 seconds for initialization..." sleep 30 # Get public DNS name of instance CLIENT_DNS=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query 'Reservations[0].Instances[0].PublicDnsName' \ --output text) if [ -z "$CLIENT_DNS" ] || [ "$CLIENT_DNS" = "None" ]; then echo "Warning: Could not get public DNS name for instance. Trying public IP..." CLIENT_DNS=$(aws ec2 describe-instances \ --instance-ids "$INSTANCE_ID" \ --query 'Reservations[0].Instances[0].PublicIpAddress' \ --output text) if [ -z "$CLIENT_DNS" ] || [ "$CLIENT_DNS" = "None" ]; then handle_error "Failed to get public DNS name or IP address for instance" fi fi echo "Client instance DNS/IP: $CLIENT_DNS" echo "==============================================" # Get bootstrap brokers with improved logic echo "Getting bootstrap brokers" MAX_RETRIES=10 RETRY_COUNT=0 BOOTSTRAP_BROKERS="" AUTH_METHOD="" while [ -z "$BOOTSTRAP_BROKERS" ] || [ "$BOOTSTRAP_BROKERS" = "None" ]; do # Get the full bootstrap brokers response BOOTSTRAP_RESPONSE=$(aws kafka get-bootstrap-brokers \ --cluster-arn "$CLUSTER_ARN" 2>/dev/null) if [ $? -eq 0 ] && [ -n "$BOOTSTRAP_RESPONSE" ]; then # Try to get IAM authentication brokers first using grep BOOTSTRAP_BROKERS=$(echo "$BOOTSTRAP_RESPONSE" | grep -o '"BootstrapBrokerStringSaslIam": "[^"]*' | cut -d'"' -f4) if [ -n "$BOOTSTRAP_BROKERS" ]; then AUTH_METHOD="IAM" else # Fall back to TLS authentication BOOTSTRAP_BROKERS=$(echo "$BOOTSTRAP_RESPONSE" | grep -o '"BootstrapBrokerStringTls": "[^"]*' | cut -d'"' -f4) if [ -n "$BOOTSTRAP_BROKERS" ]; then AUTH_METHOD="TLS" fi fi fi RETRY_COUNT=$((RETRY_COUNT + 1)) if [ "$RETRY_COUNT" -ge "$MAX_RETRIES" ]; then echo "Warning: Could not get bootstrap brokers after $MAX_RETRIES attempts." echo "You may need to manually retrieve them later using:" echo "aws kafka get-bootstrap-brokers --cluster-arn $CLUSTER_ARN" BOOTSTRAP_BROKERS="BOOTSTRAP_BROKERS_NOT_AVAILABLE" AUTH_METHOD="UNKNOWN" break fi if [ -z "$BOOTSTRAP_BROKERS" ] || [ "$BOOTSTRAP_BROKERS" = "None" ]; then echo "Bootstrap brokers not available yet. Retrying in 30 seconds... (Attempt $RETRY_COUNT/$MAX_RETRIES)" sleep 30 fi done echo "Bootstrap brokers: $BOOTSTRAP_BROKERS" echo "Authentication method: $AUTH_METHOD" echo "==============================================" # Create setup script for the client machine echo "Creating setup script for the client machine" cat > setup_client.sh << 'EOF' #!/bin/bash # Set up logging LOG_FILE="client_setup_$(date +%Y%m%d_%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting client setup" echo "==============================================" # Install Java echo "Installing Java" sudo yum -y install java-11 # Set environment variables echo "Setting up environment variables" export KAFKA_VERSION="3.6.0" echo "KAFKA_VERSION=$KAFKA_VERSION" # Download and extract Apache Kafka echo "Downloading Apache Kafka" wget https://archive.apache.org/dist/kafka/$KAFKA_VERSION/kafka_2.13-$KAFKA_VERSION.tgz if [ $? -ne 0 ]; then echo "Failed to download Kafka. Trying alternative mirror..." wget https://www.apache.org/dyn/closer.cgi?path=/kafka/$KAFKA_VERSION/kafka_2.13-$KAFKA_VERSION.tgz fi echo "Extracting Kafka" tar -xzf kafka_2.13-$KAFKA_VERSION.tgz export KAFKA_ROOT=$(pwd)/kafka_2.13-$KAFKA_VERSION echo "KAFKA_ROOT=$KAFKA_ROOT" # Download the MSK IAM authentication package (needed for both IAM and TLS) echo "Downloading MSK IAM authentication package" cd $KAFKA_ROOT/libs wget https://github.com/aws/aws-msk-iam-auth/releases/latest/download/aws-msk-iam-auth-1.1.6-all.jar if [ $? -ne 0 ]; then echo "Failed to download specific version. Trying to get latest version..." LATEST_VERSION=$(curl -s https://api.github.com/repos/aws/aws-msk-iam-auth/releases/latest | grep -o '"tag_name": "[^"]*' | cut -d'"' -f4) wget https://github.com/aws/aws-msk-iam-auth/releases/download/$LATEST_VERSION/aws-msk-iam-auth-$LATEST_VERSION-all.jar if [ $? -ne 0 ]; then echo "Failed to download IAM auth package. Please check the URL and try again." exit 1 fi export CLASSPATH=$KAFKA_ROOT/libs/aws-msk-iam-auth-$LATEST_VERSION-all.jar else export CLASSPATH=$KAFKA_ROOT/libs/aws-msk-iam-auth-1.1.6-all.jar fi echo "CLASSPATH=$CLASSPATH" # Create client properties file based on authentication method echo "Creating client properties file" cd $KAFKA_ROOT/config # The AUTH_METHOD_PLACEHOLDER will be replaced by the script AUTH_METHOD="AUTH_METHOD_PLACEHOLDER" if [ "$AUTH_METHOD" = "IAM" ]; then echo "Configuring for IAM authentication" cat > client.properties << 'EOT' security.protocol=SASL_SSL sasl.mechanism=AWS_MSK_IAM sasl.jaas.config=software.amazon.msk.auth.iam.IAMLoginModule required; sasl.client.callback.handler.class=software.amazon.msk.auth.iam.IAMClientCallbackHandler EOT elif [ "$AUTH_METHOD" = "TLS" ]; then echo "Configuring for TLS authentication" cat > client.properties << 'EOT' security.protocol=SSL EOT else echo "Unknown authentication method. Creating basic TLS configuration." cat > client.properties << 'EOT' security.protocol=SSL EOT fi echo "Client setup completed" echo "==============================================" # Create a script to set up environment variables cat > ~/setup_env.sh << 'EOT' #!/bin/bash export KAFKA_VERSION="3.6.0" export KAFKA_ROOT=~/kafka_2.13-$KAFKA_VERSION export CLASSPATH=$KAFKA_ROOT/libs/aws-msk-iam-auth-1.1.6-all.jar export BOOTSTRAP_SERVER="BOOTSTRAP_SERVER_PLACEHOLDER" export AUTH_METHOD="AUTH_METHOD_PLACEHOLDER" echo "Environment variables set:" echo "KAFKA_VERSION=$KAFKA_VERSION" echo "KAFKA_ROOT=$KAFKA_ROOT" echo "CLASSPATH=$CLASSPATH" echo "BOOTSTRAP_SERVER=$BOOTSTRAP_SERVER" echo "AUTH_METHOD=$AUTH_METHOD" EOT chmod +x ~/setup_env.sh echo "Created environment setup script: ~/setup_env.sh" echo "Run 'source ~/setup_env.sh' to set up your environment" EOF # Replace placeholders in the setup script if [ -n "$BOOTSTRAP_BROKERS" ] && [ "$BOOTSTRAP_BROKERS" != "None" ] && [ "$BOOTSTRAP_BROKERS" != "BOOTSTRAP_BROKERS_NOT_AVAILABLE" ]; then sed -i "s|BOOTSTRAP_SERVER_PLACEHOLDER|$BOOTSTRAP_BROKERS|g" setup_client.sh else # If bootstrap brokers are not available, provide instructions to get them sed -i "s|BOOTSTRAP_SERVER_PLACEHOLDER|\$(aws kafka get-bootstrap-brokers --cluster-arn $CLUSTER_ARN --query 'BootstrapBrokerStringTls' --output text)|g" setup_client.sh fi # Replace auth method placeholder sed -i "s|AUTH_METHOD_PLACEHOLDER|$AUTH_METHOD|g" setup_client.sh echo "Setup script created" echo "==============================================" # Display summary of created resources echo "" echo "==============================================" echo "RESOURCE SUMMARY" echo "==============================================" echo "MSK Cluster ARN: $CLUSTER_ARN" echo "MSK Cluster Name: $CLUSTER_NAME" echo "Authentication Method: $AUTH_METHOD" echo "IAM Policy ARN: $POLICY_ARN" echo "IAM Role Name: $ROLE_NAME" echo "IAM Instance Profile: $INSTANCE_PROFILE_NAME" echo "Client Security Group: $CLIENT_SG_ID" echo "EC2 Instance ID: $INSTANCE_ID" echo "EC2 Instance Type: $SELECTED_INSTANCE_TYPE" echo "EC2 Instance DNS: $CLIENT_DNS" echo "Key Pair: $KEY_NAME (saved to $KEY_FILE)" echo "Bootstrap Brokers: $BOOTSTRAP_BROKERS" echo "==============================================" echo "" # Instructions for connecting to the instance and setting up the client echo "NEXT STEPS:" echo "1. Connect to your EC2 instance:" echo " ssh -i $KEY_FILE ec2-user@$CLIENT_DNS" echo "" echo "2. Upload the setup script to your instance:" echo " scp -i $KEY_FILE setup_client.sh ec2-user@$CLIENT_DNS:~/" echo "" echo "3. Run the setup script on your instance:" echo " ssh -i $KEY_FILE ec2-user@$CLIENT_DNS 'chmod +x ~/setup_client.sh && ~/setup_client.sh'" echo "" echo "4. Source the environment setup script:" echo " source ~/setup_env.sh" echo "" # Provide different instructions based on authentication method if [ "$AUTH_METHOD" = "IAM" ]; then echo "5. Create a Kafka topic (using IAM authentication):" echo " \$KAFKA_ROOT/bin/kafka-topics.sh --create \\" echo " --bootstrap-server \$BOOTSTRAP_SERVER \\" echo " --command-config \$KAFKA_ROOT/config/client.properties \\" echo " --replication-factor 3 \\" echo " --partitions 1 \\" echo " --topic MSKTutorialTopic" echo "" echo "6. Start a producer:" echo " \$KAFKA_ROOT/bin/kafka-console-producer.sh \\" echo " --broker-list \$BOOTSTRAP_SERVER \\" echo " --producer.config \$KAFKA_ROOT/config/client.properties \\" echo " --topic MSKTutorialTopic" echo "" echo "7. Start a consumer:" echo " \$KAFKA_ROOT/bin/kafka-console-consumer.sh \\" echo " --bootstrap-server \$BOOTSTRAP_SERVER \\" echo " --consumer.config \$KAFKA_ROOT/config/client.properties \\" echo " --topic MSKTutorialTopic \\" echo " --from-beginning" elif [ "$AUTH_METHOD" = "TLS" ]; then echo "5. Create a Kafka topic (using TLS authentication):" echo " \$KAFKA_ROOT/bin/kafka-topics.sh --create \\" echo " --bootstrap-server \$BOOTSTRAP_SERVER \\" echo " --command-config \$KAFKA_ROOT/config/client.properties \\" echo " --replication-factor 3 \\" echo " --partitions 1 \\" echo " --topic MSKTutorialTopic" echo "" echo "6. Start a producer:" echo " \$KAFKA_ROOT/bin/kafka-console-producer.sh \\" echo " --broker-list \$BOOTSTRAP_SERVER \\" echo " --producer.config \$KAFKA_ROOT/config/client.properties \\" echo " --topic MSKTutorialTopic" echo "" echo "7. Start a consumer:" echo " \$KAFKA_ROOT/bin/kafka-console-consumer.sh \\" echo " --bootstrap-server \$BOOTSTRAP_SERVER \\" echo " --consumer.config \$KAFKA_ROOT/config/client.properties \\" echo " --topic MSKTutorialTopic \\" echo " --from-beginning" else echo "5. Manually retrieve bootstrap brokers and configure authentication:" echo " aws kafka get-bootstrap-brokers --cluster-arn $CLUSTER_ARN" fi echo "" echo "8. Verify the topic was created:" echo " \$KAFKA_ROOT/bin/kafka-topics.sh --list \\" echo " --bootstrap-server \$BOOTSTRAP_SERVER \\" echo " --command-config \$KAFKA_ROOT/config/client.properties" echo "==============================================" echo "" # Ask if user wants to clean up resources echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ $CLEANUP_CHOICE =~ ^[Yy]$ ]]; then cleanup_resources echo "All resources have been cleaned up." else echo "Resources will not be cleaned up. You can manually clean them up later." echo "To clean up resources, run this script again and choose 'y' when prompted." fi echo "Script completed successfully!"

The following code example shows how to:

  • Create a VPC for your Neptune database

  • Create subnets in multiple availability zones

  • Configure security for your Neptune database

  • Create a Neptune DB subnet group

  • Create a Neptune DB cluster and instance

  • Add data to your graph database

  • Query your graph database

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Amazon Neptune Getting Started Script # This script creates an Amazon Neptune database cluster and demonstrates basic operations # Set up logging LOG_FILE="neptune-setup.log" echo "Starting Neptune setup at $(date)" > "$LOG_FILE" # Function to log commands and their output log_cmd() { echo "Running: $1" | tee -a "$LOG_FILE" eval "$1" 2>&1 | tee -a "$LOG_FILE" return ${PIPESTATUS[0]} } # Function to check for errors in command output check_error() { local cmd_output="$1" local cmd_status="$2" local error_msg="$3" if [[ $cmd_status -ne 0 || "$cmd_output" =~ [Ee][Rr][Rr][Oo][Rr] ]]; then echo "ERROR: $error_msg" | tee -a "$LOG_FILE" cleanup_on_error exit 1 fi } # Function to clean up resources on error cleanup_on_error() { echo "Error encountered. Cleaning up resources..." | tee -a "$LOG_FILE" # Only attempt to delete resources that were successfully created if [[ -n "$DB_INSTANCE_ID" ]]; then echo "Deleting DB instance $DB_INSTANCE_ID..." | tee -a "$LOG_FILE" log_cmd "aws neptune delete-db-instance --db-instance-identifier $DB_INSTANCE_ID --skip-final-snapshot" log_cmd "aws neptune wait db-instance-deleted --db-instance-identifier $DB_INSTANCE_ID" fi if [[ -n "$DB_CLUSTER_ID" ]]; then echo "Deleting DB cluster $DB_CLUSTER_ID..." | tee -a "$LOG_FILE" log_cmd "aws neptune delete-db-cluster --db-cluster-identifier $DB_CLUSTER_ID --skip-final-snapshot" fi if [[ -n "$DB_SUBNET_GROUP" ]]; then echo "Deleting DB subnet group $DB_SUBNET_GROUP..." | tee -a "$LOG_FILE" log_cmd "aws neptune delete-db-subnet-group --db-subnet-group-name $DB_SUBNET_GROUP" fi if [[ -n "$SECURITY_GROUP_ID" ]]; then echo "Deleting security group $SECURITY_GROUP_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-security-group --group-id $SECURITY_GROUP_ID" fi if [[ -n "$SUBNET_IDS" ]]; then for SUBNET_ID in $SUBNET_IDS; do echo "Deleting subnet $SUBNET_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-subnet --subnet-id $SUBNET_ID" done fi if [[ -n "$VPC_ID" ]]; then echo "Deleting VPC $VPC_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-vpc --vpc-id $VPC_ID" fi } # Generate random identifier for resource names RANDOM_ID=$(openssl rand -hex 4) VPC_NAME="neptune-vpc-$RANDOM_ID" DB_SUBNET_GROUP="neptune-subnet-group-$RANDOM_ID" DB_CLUSTER_ID="neptune-cluster-$RANDOM_ID" DB_INSTANCE_ID="neptune-instance-$RANDOM_ID" SG_NAME="neptune-sg-$RANDOM_ID" echo "Using random identifier: $RANDOM_ID" | tee -a "$LOG_FILE" echo "VPC Name: $VPC_NAME" | tee -a "$LOG_FILE" echo "DB Subnet Group: $DB_SUBNET_GROUP" | tee -a "$LOG_FILE" echo "DB Cluster ID: $DB_CLUSTER_ID" | tee -a "$LOG_FILE" echo "DB Instance ID: $DB_INSTANCE_ID" | tee -a "$LOG_FILE" echo "Security Group Name: $SG_NAME" | tee -a "$LOG_FILE" # Step 1: Create VPC echo "Creating VPC..." | tee -a "$LOG_FILE" VPC_OUTPUT=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=$VPC_NAME},{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]" --output json) check_error "$VPC_OUTPUT" $? "Failed to create VPC" VPC_ID=$(echo "$VPC_OUTPUT" | grep -o '"VpcId": "[^"]*' | cut -d'"' -f4) echo "VPC created with ID: $VPC_ID" | tee -a "$LOG_FILE" # Enable DNS support for the VPC log_cmd "aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-support" log_cmd "aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames" # Step 2: Create Internet Gateway and attach to VPC echo "Creating Internet Gateway..." | tee -a "$LOG_FILE" IGW_OUTPUT=$(aws ec2 create-internet-gateway --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]' --output json) check_error "$IGW_OUTPUT" $? "Failed to create Internet Gateway" IGW_ID=$(echo "$IGW_OUTPUT" | grep -o '"InternetGatewayId": "[^"]*' | cut -d'"' -f4) echo "Internet Gateway created with ID: $IGW_ID" | tee -a "$LOG_FILE" log_cmd "aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID" # Step 3: Create subnets in different AZs echo "Creating subnets..." | tee -a "$LOG_FILE" # Get available AZs AZ_OUTPUT=$(aws ec2 describe-availability-zones --output json) check_error "$AZ_OUTPUT" $? "Failed to get availability zones" # Extract first 3 AZ names AZ1=$(echo "$AZ_OUTPUT" | grep -o '"ZoneName": "[^"]*' | cut -d'"' -f4 | head -1) AZ2=$(echo "$AZ_OUTPUT" | grep -o '"ZoneName": "[^"]*' | cut -d'"' -f4 | head -2 | tail -1) AZ3=$(echo "$AZ_OUTPUT" | grep -o '"ZoneName": "[^"]*' | cut -d'"' -f4 | head -3 | tail -1) # Create 3 subnets in different AZs SUBNET1_OUTPUT=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24 --availability-zone $AZ1 --tag-specifications 'ResourceType=subnet,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]' --output json) check_error "$SUBNET1_OUTPUT" $? "Failed to create subnet 1" SUBNET1_ID=$(echo "$SUBNET1_OUTPUT" | grep -o '"SubnetId": "[^"]*' | cut -d'"' -f4) SUBNET2_OUTPUT=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24 --availability-zone $AZ2 --tag-specifications 'ResourceType=subnet,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]' --output json) check_error "$SUBNET2_OUTPUT" $? "Failed to create subnet 2" SUBNET2_ID=$(echo "$SUBNET2_OUTPUT" | grep -o '"SubnetId": "[^"]*' | cut -d'"' -f4) SUBNET3_OUTPUT=$(aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.3.0/24 --availability-zone $AZ3 --tag-specifications 'ResourceType=subnet,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]' --output json) check_error "$SUBNET3_OUTPUT" $? "Failed to create subnet 3" SUBNET3_ID=$(echo "$SUBNET3_OUTPUT" | grep -o '"SubnetId": "[^"]*' | cut -d'"' -f4) SUBNET_IDS="$SUBNET1_ID $SUBNET2_ID $SUBNET3_ID" echo "Created subnets: $SUBNET1_ID, $SUBNET2_ID, $SUBNET3_ID" | tee -a "$LOG_FILE" # Step 4: Create route table and add route to Internet Gateway echo "Creating route table..." | tee -a "$LOG_FILE" ROUTE_TABLE_OUTPUT=$(aws ec2 create-route-table --vpc-id $VPC_ID --tag-specifications 'ResourceType=route-table,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]' --output json) check_error "$ROUTE_TABLE_OUTPUT" $? "Failed to create route table" ROUTE_TABLE_ID=$(echo "$ROUTE_TABLE_OUTPUT" | grep -o '"RouteTableId": "[^"]*' | cut -d'"' -f4) echo "Route table created with ID: $ROUTE_TABLE_ID" | tee -a "$LOG_FILE" # Add route to Internet Gateway log_cmd "aws ec2 create-route --route-table-id $ROUTE_TABLE_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID" # Associate route table with subnets log_cmd "aws ec2 associate-route-table --route-table-id $ROUTE_TABLE_ID --subnet-id $SUBNET1_ID" log_cmd "aws ec2 associate-route-table --route-table-id $ROUTE_TABLE_ID --subnet-id $SUBNET2_ID" log_cmd "aws ec2 associate-route-table --route-table-id $ROUTE_TABLE_ID --subnet-id $SUBNET3_ID" # Step 5: Create security group echo "Creating security group..." | tee -a "$LOG_FILE" SG_OUTPUT=$(aws ec2 create-security-group --group-name $SG_NAME --description "Security group for Neptune" --vpc-id $VPC_ID --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=amazon-neptune-gs}]' --output json) check_error "$SG_OUTPUT" $? "Failed to create security group" SECURITY_GROUP_ID=$(echo "$SG_OUTPUT" | grep -o '"GroupId": "[^"]*' | cut -d'"' -f4) echo "Security group created with ID: $SECURITY_GROUP_ID" | tee -a "$LOG_FILE" # Add inbound rule for Neptune port (8182) # Note: In production, you should restrict this to specific IP ranges echo "Adding security group rule for Neptune port 8182..." | tee -a "$LOG_FILE" log_cmd "aws ec2 authorize-security-group-ingress --group-id $SECURITY_GROUP_ID --protocol tcp --port 8182 --cidr 10.0.0.0/16" # Step 6: Create DB subnet group echo "Creating DB subnet group..." | tee -a "$LOG_FILE" DB_SUBNET_GROUP_OUTPUT=$(aws neptune create-db-subnet-group --db-subnet-group-name $DB_SUBNET_GROUP --db-subnet-group-description "Subnet group for Neptune" --subnet-ids $SUBNET1_ID $SUBNET2_ID $SUBNET3_ID --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-neptune-gs --output json) check_error "$DB_SUBNET_GROUP_OUTPUT" $? "Failed to create DB subnet group" echo "DB subnet group created: $DB_SUBNET_GROUP" | tee -a "$LOG_FILE" # Step 7: Create Neptune DB cluster echo "Creating Neptune DB cluster..." | tee -a "$LOG_FILE" DB_CLUSTER_OUTPUT=$(aws neptune create-db-cluster --db-cluster-identifier $DB_CLUSTER_ID --engine neptune --vpc-security-group-ids $SECURITY_GROUP_ID --db-subnet-group-name $DB_SUBNET_GROUP --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-neptune-gs --output json) check_error "$DB_CLUSTER_OUTPUT" $? "Failed to create Neptune DB cluster" echo "Neptune DB cluster created: $DB_CLUSTER_ID" | tee -a "$LOG_FILE" # Step 8: Create Neptune DB instance echo "Creating Neptune DB instance..." | tee -a "$LOG_FILE" DB_INSTANCE_OUTPUT=$(aws neptune create-db-instance --db-instance-identifier $DB_INSTANCE_ID --db-instance-class db.r5.large --engine neptune --db-cluster-identifier $DB_CLUSTER_ID --tags Key=project,Value=doc-smith Key=tutorial,Value=amazon-neptune-gs --output json) check_error "$DB_INSTANCE_OUTPUT" $? "Failed to create Neptune DB instance" echo "Neptune DB instance created: $DB_INSTANCE_ID" | tee -a "$LOG_FILE" # Step 9: Wait for the DB instance to become available echo "Waiting for Neptune DB instance to become available..." | tee -a "$LOG_FILE" log_cmd "aws neptune wait db-instance-available --db-instance-identifier $DB_INSTANCE_ID" # Step 10: Get the Neptune endpoint echo "Getting Neptune endpoint..." | tee -a "$LOG_FILE" ENDPOINT_OUTPUT=$(aws neptune describe-db-clusters --db-cluster-identifier $DB_CLUSTER_ID --output json) check_error "$ENDPOINT_OUTPUT" $? "Failed to get Neptune endpoint" NEPTUNE_ENDPOINT=$(echo "$ENDPOINT_OUTPUT" | grep -o '"Endpoint": "[^"]*' | cut -d'"' -f4) echo "Neptune endpoint: $NEPTUNE_ENDPOINT" | tee -a "$LOG_FILE" # Step 11: Display information about how to connect to Neptune echo "" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" echo "NEPTUNE SETUP COMPLETE" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" echo "Neptune Endpoint: $NEPTUNE_ENDPOINT" | tee -a "$LOG_FILE" echo "Port: 8182" | tee -a "$LOG_FILE" echo "" | tee -a "$LOG_FILE" echo "To query your Neptune database using Gremlin, you can use curl:" | tee -a "$LOG_FILE" echo "curl -X POST -d '{\"gremlin\":\"g.V().limit(1)\"}' https://$NEPTUNE_ENDPOINT:8182/gremlin" | tee -a "$LOG_FILE" echo "" | tee -a "$LOG_FILE" echo "To add data to your graph:" | tee -a "$LOG_FILE" echo "curl -X POST -d '{\"gremlin\":\"g.addV(\\\"person\\\").property(\\\"name\\\", \\\"Howard\\\")\"}' https://$NEPTUNE_ENDPOINT:8182/gremlin" | tee -a "$LOG_FILE" echo "" | tee -a "$LOG_FILE" echo "Note: You may need to configure your client machine to access the Neptune instance within the VPC." | tee -a "$LOG_FILE" echo "For production use, consider using an EC2 instance in the same VPC or setting up a bastion host." | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" # Step 12: List all created resources echo "" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" echo "RESOURCES CREATED" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" echo "VPC: $VPC_ID" | tee -a "$LOG_FILE" echo "Internet Gateway: $IGW_ID" | tee -a "$LOG_FILE" echo "Subnets: $SUBNET1_ID, $SUBNET2_ID, $SUBNET3_ID" | tee -a "$LOG_FILE" echo "Route Table: $ROUTE_TABLE_ID" | tee -a "$LOG_FILE" echo "Security Group: $SECURITY_GROUP_ID" | tee -a "$LOG_FILE" echo "DB Subnet Group: $DB_SUBNET_GROUP" | tee -a "$LOG_FILE" echo "Neptune DB Cluster: $DB_CLUSTER_ID" | tee -a "$LOG_FILE" echo "Neptune DB Instance: $DB_INSTANCE_ID" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" # Step 13: Ask if user wants to clean up resources echo "" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" echo "CLEANUP CONFIRMATION" | tee -a "$LOG_FILE" echo "=============================================" | tee -a "$LOG_FILE" echo "Do you want to clean up all created resources? (y/n): " | tee -a "$LOG_FILE" read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Starting cleanup process..." | tee -a "$LOG_FILE" # Delete DB instance echo "Deleting DB instance $DB_INSTANCE_ID..." | tee -a "$LOG_FILE" log_cmd "aws neptune delete-db-instance --db-instance-identifier $DB_INSTANCE_ID --skip-final-snapshot" # Wait for DB instance to be deleted echo "Waiting for DB instance to be deleted..." | tee -a "$LOG_FILE" log_cmd "aws neptune wait db-instance-deleted --db-instance-identifier $DB_INSTANCE_ID" # Delete DB cluster echo "Deleting DB cluster $DB_CLUSTER_ID..." | tee -a "$LOG_FILE" log_cmd "aws neptune delete-db-cluster --db-cluster-identifier $DB_CLUSTER_ID --skip-final-snapshot" # Wait for DB cluster to be deleted (no specific wait command for this, so we'll sleep) echo "Waiting for DB cluster to be deleted..." | tee -a "$LOG_FILE" sleep 60 # Delete DB subnet group echo "Deleting DB subnet group $DB_SUBNET_GROUP..." | tee -a "$LOG_FILE" log_cmd "aws neptune delete-db-subnet-group --db-subnet-group-name $DB_SUBNET_GROUP" # Delete security group echo "Deleting security group $SECURITY_GROUP_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-security-group --group-id $SECURITY_GROUP_ID" # Detach and delete internet gateway echo "Detaching and deleting internet gateway $IGW_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 detach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID" log_cmd "aws ec2 delete-internet-gateway --internet-gateway-id $IGW_ID" # Delete subnets echo "Deleting subnets..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-subnet --subnet-id $SUBNET1_ID" log_cmd "aws ec2 delete-subnet --subnet-id $SUBNET2_ID" log_cmd "aws ec2 delete-subnet --subnet-id $SUBNET3_ID" # Delete route table echo "Deleting route table $ROUTE_TABLE_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-route-table --route-table-id $ROUTE_TABLE_ID" # Delete VPC echo "Deleting VPC $VPC_ID..." | tee -a "$LOG_FILE" log_cmd "aws ec2 delete-vpc --vpc-id $VPC_ID" echo "Cleanup complete!" | tee -a "$LOG_FILE" else echo "Resources will not be cleaned up. You can delete them manually later." | tee -a "$LOG_FILE" echo "See the list of resources above for reference." | tee -a "$LOG_FILE" fi echo "Script completed. See $LOG_FILE for details." | tee -a "$LOG_FILE"

The following code example shows how to:

  • Create a transit gateway

  • Attach your VPCs to your transit gateway

  • Add routes between the transit gateway and your VPCs

  • Test the transit gateway

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Amazon VPC Transit Gateway CLI Script # This script demonstrates how to create a transit gateway and connect two VPCs # Modified to work with older AWS CLI versions that don't support transit gateway wait commands # Security improved: Added input validation, error handling, and credential checks # Error handling set -euo pipefail LOG_FILE="transit-gateway-tutorial.log" exec > >(tee -a "$LOG_FILE") 2>&1 # Security: Check for required AWS credentials if ! aws sts get-caller-identity &>/dev/null; then echo "ERROR: AWS credentials not configured or invalid. Please configure AWS credentials." exit 1 fi # Function to validate AWS CLI output validate_aws_output() { local output=$1 local context=$2 if [ -z "$output" ] || [ "$output" = "None" ]; then echo "ERROR: Failed to retrieve $context from AWS API" return 1 fi } # Function to validate CIDR blocks validate_cidr() { local cidr=$1 if ! [[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]]; then echo "ERROR: Invalid CIDR block format: $cidr" return 1 fi } # Function to wait for transit gateway to be available wait_for_tgw() { local tgw_id=$1 local max_attempts=60 local attempt=0 echo "Waiting for Transit Gateway $tgw_id to become available..." while [ $attempt -lt $max_attempts ]; do status=$(aws ec2 describe-transit-gateways \ --transit-gateway-ids "$tgw_id" \ --query "TransitGateways[0].State" \ --output text 2>/dev/null || echo "failed") echo "Current status: $status" if [ "$status" = "available" ]; then echo "Transit Gateway is now available" return 0 fi if [ "$status" = "failed" ]; then echo "ERROR: Transit Gateway creation failed" return 1 fi echo "Waiting for transit gateway to become available. Current state: $status" sleep 10 ((attempt++)) done echo "ERROR: Timeout waiting for transit gateway to become available" return 1 } # Function to wait for transit gateway attachment to be available wait_for_tgw_attachment() { local attachment_id=$1 local max_attempts=60 local attempt=0 echo "Waiting for Transit Gateway Attachment $attachment_id to become available..." while [ $attempt -lt $max_attempts ]; do status=$(aws ec2 describe-transit-gateway-vpc-attachments \ --transit-gateway-attachment-ids "$attachment_id" \ --query "TransitGatewayVpcAttachments[0].State" \ --output text 2>/dev/null || echo "failed") echo "Current status: $status" if [ "$status" = "available" ]; then echo "Transit Gateway Attachment is now available" return 0 fi if [ "$status" = "failed" ]; then echo "ERROR: Transit Gateway Attachment creation failed" return 1 fi echo "Waiting for transit gateway attachment to become available. Current state: $status" sleep 10 ((attempt++)) done echo "ERROR: Timeout waiting for transit gateway attachment to become available" return 1 } # Function to wait for transit gateway attachment to be deleted wait_for_tgw_attachment_deleted() { local attachment_id=$1 local max_attempts=60 local attempt=0 echo "Waiting for Transit Gateway Attachment $attachment_id to be deleted..." while [ $attempt -lt $max_attempts ]; do count=$(aws ec2 describe-transit-gateway-vpc-attachments \ --filters "Name=transit-gateway-attachment-id,Values=$attachment_id" \ --query "length(TransitGatewayVpcAttachments)" \ --output text 2>/dev/null || echo "0") if [ "$count" = "0" ]; then echo "Transit Gateway Attachment has been deleted" return 0 fi status=$(aws ec2 describe-transit-gateway-vpc-attachments \ --transit-gateway-attachment-ids "$attachment_id" \ --query "TransitGatewayVpcAttachments[0].State" \ --output text 2>/dev/null || echo "deleted") if [ "$status" = "deleted" ] || [ "$status" = "deleting" ]; then echo "Transit Gateway Attachment is being deleted. Current state: $status" fi echo "Waiting for transit gateway attachment to be deleted. Current state: $status" sleep 10 ((attempt++)) done echo "WARNING: Timeout waiting for transit gateway attachment to be deleted" return 0 } # Function to clean up resources cleanup() { local exit_code=$? echo "Error occurred (exit code: $exit_code). Cleaning up resources..." # Delete resources in reverse order if [ -n "${TGW_ATTACHMENT_1_ID:-}" ]; then echo "Deleting Transit Gateway VPC Attachment 1: $TGW_ATTACHMENT_1_ID" aws ec2 delete-transit-gateway-vpc-attachment \ --transit-gateway-attachment-id "$TGW_ATTACHMENT_1_ID" &>/dev/null || true wait_for_tgw_attachment_deleted "$TGW_ATTACHMENT_1_ID" || true fi if [ -n "${TGW_ATTACHMENT_2_ID:-}" ]; then echo "Deleting Transit Gateway VPC Attachment 2: $TGW_ATTACHMENT_2_ID" aws ec2 delete-transit-gateway-vpc-attachment \ --transit-gateway-attachment-id "$TGW_ATTACHMENT_2_ID" &>/dev/null || true wait_for_tgw_attachment_deleted "$TGW_ATTACHMENT_2_ID" || true fi if [ -n "${TGW_ID:-}" ]; then echo "Deleting Transit Gateway: $TGW_ID" aws ec2 delete-transit-gateway --transit-gateway-id "$TGW_ID" &>/dev/null || true fi exit "$exit_code" } # Set up trap for error handling trap cleanup EXIT echo "=== Amazon VPC Transit Gateway Tutorial ===" echo "This script will create a transit gateway and connect two VPCs" echo "" # Get a valid availability zone dynamically echo "Getting available AZ in current region..." AZ=$(aws ec2 describe-availability-zones \ --query "AvailabilityZones[0].ZoneName" \ --output text) validate_aws_output "$AZ" "availability zone" || exit 1 echo "Using availability zone: $AZ" # Check if VPCs exist echo "Checking for existing VPCs..." VPC1_ID=$(aws ec2 describe-vpcs \ --filters "Name=tag:Name,Values=VPC1" \ --query "Vpcs[0].VpcId" \ --output text) VPC2_ID=$(aws ec2 describe-vpcs \ --filters "Name=tag:Name,Values=VPC2" \ --query "Vpcs[0].VpcId" \ --output text) if [ "$VPC1_ID" = "None" ] || [ -z "$VPC1_ID" ]; then echo "Creating VPC1..." VPC1_ID=$(aws ec2 create-vpc \ --cidr-block 10.1.0.0/16 \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=VPC1},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query Vpc.VpcId \ --output text) validate_aws_output "$VPC1_ID" "VPC1" || exit 1 echo "Created VPC1: $VPC1_ID" # Create a subnet in VPC1 echo "Creating subnet in VPC1..." SUBNET1_ID=$(aws ec2 create-subnet \ --vpc-id "$VPC1_ID" \ --cidr-block 10.1.0.0/24 \ --availability-zone "$AZ" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=VPC1-Subnet},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query Subnet.SubnetId \ --output text) validate_aws_output "$SUBNET1_ID" "VPC1 subnet" || exit 1 echo "Created subnet in VPC1: $SUBNET1_ID" else echo "Using existing VPC1: $VPC1_ID" SUBNET1_ID=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=$VPC1_ID" \ --query "Subnets[0].SubnetId" \ --output text) if [ "$SUBNET1_ID" = "None" ] || [ -z "$SUBNET1_ID" ]; then echo "Creating subnet in VPC1..." SUBNET1_ID=$(aws ec2 create-subnet \ --vpc-id "$VPC1_ID" \ --cidr-block 10.1.0.0/24 \ --availability-zone "$AZ" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=VPC1-Subnet},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query Subnet.SubnetId \ --output text) validate_aws_output "$SUBNET1_ID" "VPC1 subnet" || exit 1 echo "Created subnet in VPC1: $SUBNET1_ID" else echo "Using existing subnet in VPC1: $SUBNET1_ID" fi fi if [ "$VPC2_ID" = "None" ] || [ -z "$VPC2_ID" ]; then echo "Creating VPC2..." VPC2_ID=$(aws ec2 create-vpc \ --cidr-block 10.2.0.0/16 \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=VPC2},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query Vpc.VpcId \ --output text) validate_aws_output "$VPC2_ID" "VPC2" || exit 1 echo "Created VPC2: $VPC2_ID" # Create a subnet in VPC2 echo "Creating subnet in VPC2..." SUBNET2_ID=$(aws ec2 create-subnet \ --vpc-id "$VPC2_ID" \ --cidr-block 10.2.0.0/24 \ --availability-zone "$AZ" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=VPC2-Subnet},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query Subnet.SubnetId \ --output text) validate_aws_output "$SUBNET2_ID" "VPC2 subnet" || exit 1 echo "Created subnet in VPC2: $SUBNET2_ID" else echo "Using existing VPC2: $VPC2_ID" SUBNET2_ID=$(aws ec2 describe-subnets \ --filters "Name=vpc-id,Values=$VPC2_ID" \ --query "Subnets[0].SubnetId" \ --output text) if [ "$SUBNET2_ID" = "None" ] || [ -z "$SUBNET2_ID" ]; then echo "Creating subnet in VPC2..." SUBNET2_ID=$(aws ec2 create-subnet \ --vpc-id "$VPC2_ID" \ --cidr-block 10.2.0.0/24 \ --availability-zone "$AZ" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=VPC2-Subnet},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query Subnet.SubnetId \ --output text) validate_aws_output "$SUBNET2_ID" "VPC2 subnet" || exit 1 echo "Created subnet in VPC2: $SUBNET2_ID" else echo "Using existing subnet in VPC2: $SUBNET2_ID" fi fi # Get route tables for each VPC RTB1_ID=$(aws ec2 describe-route-tables \ --filters "Name=vpc-id,Values=$VPC1_ID" \ --query "RouteTables[0].RouteTableId" \ --output text) validate_aws_output "$RTB1_ID" "VPC1 route table" || exit 1 RTB2_ID=$(aws ec2 describe-route-tables \ --filters "Name=vpc-id,Values=$VPC2_ID" \ --query "RouteTables[0].RouteTableId" \ --output text) validate_aws_output "$RTB2_ID" "VPC2 route table" || exit 1 echo "Route table for VPC1: $RTB1_ID" echo "Route table for VPC2: $RTB2_ID" # Step 1: Create the transit gateway echo "Creating Transit Gateway..." TGW_ID=$(aws ec2 create-transit-gateway \ --description "My Transit Gateway" \ --options "AmazonSideAsn=64512,AutoAcceptSharedAttachments=disable,DefaultRouteTableAssociation=enable,DefaultRouteTablePropagation=enable,VpnEcmpSupport=enable,DnsSupport=enable,MulticastSupport=disable" \ --tag-specifications 'ResourceType=transit-gateway,Tags=[{Key=Name,Value=MyTransitGateway},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query TransitGateway.TransitGatewayId \ --output text) validate_aws_output "$TGW_ID" "Transit Gateway" || exit 1 echo "Created Transit Gateway: $TGW_ID" # Wait for the transit gateway to become available wait_for_tgw "$TGW_ID" || exit 1 # Step 2: Attach VPCs to the transit gateway echo "Attaching VPC1 to Transit Gateway..." TGW_ATTACHMENT_1_ID=$(aws ec2 create-transit-gateway-vpc-attachment \ --transit-gateway-id "$TGW_ID" \ --vpc-id "$VPC1_ID" \ --subnet-ids "$SUBNET1_ID" \ --tag-specifications 'ResourceType=transit-gateway-attachment,Tags=[{Key=Name,Value=VPC1-Attachment},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query TransitGatewayVpcAttachment.TransitGatewayAttachmentId \ --output text) validate_aws_output "$TGW_ATTACHMENT_1_ID" "Transit Gateway VPC1 Attachment" || exit 1 echo "Created Transit Gateway VPC Attachment for VPC1: $TGW_ATTACHMENT_1_ID" echo "Attaching VPC2 to Transit Gateway..." TGW_ATTACHMENT_2_ID=$(aws ec2 create-transit-gateway-vpc-attachment \ --transit-gateway-id "$TGW_ID" \ --vpc-id "$VPC2_ID" \ --subnet-ids "$SUBNET2_ID" \ --tag-specifications 'ResourceType=transit-gateway-attachment,Tags=[{Key=Name,Value=VPC2-Attachment},{Key=project,Value=doc-smith},{Key=tutorial,Value=transitgateway-gettingstarted}]' \ --query TransitGatewayVpcAttachment.TransitGatewayAttachmentId \ --output text) validate_aws_output "$TGW_ATTACHMENT_2_ID" "Transit Gateway VPC2 Attachment" || exit 1 echo "Created Transit Gateway VPC Attachment for VPC2: $TGW_ATTACHMENT_2_ID" # Wait for the attachments to become available wait_for_tgw_attachment "$TGW_ATTACHMENT_1_ID" || exit 1 wait_for_tgw_attachment "$TGW_ATTACHMENT_2_ID" || exit 1 # Step 3: Add routes between the transit gateway and VPCs echo "Adding route from VPC1 to VPC2 via Transit Gateway..." validate_cidr "10.2.0.0/16" || exit 1 aws ec2 create-route \ --route-table-id "$RTB1_ID" \ --destination-cidr-block 10.2.0.0/16 \ --transit-gateway-id "$TGW_ID" || exit 1 echo "Adding route from VPC2 to VPC1 via Transit Gateway..." validate_cidr "10.1.0.0/16" || exit 1 aws ec2 create-route \ --route-table-id "$RTB2_ID" \ --destination-cidr-block 10.1.0.0/16 \ --transit-gateway-id "$TGW_ID" || exit 1 echo "Routes added successfully" # Step 4: Display information for testing echo "" echo "=== Transit Gateway Setup Complete ===" echo "Transit Gateway ID: $TGW_ID" echo "VPC1 ID: $VPC1_ID" echo "VPC2 ID: $VPC2_ID" echo "" echo "To test connectivity:" echo "1. Launch an EC2 instance in each VPC" echo "2. Configure security groups to allow ICMP traffic" echo "3. Connect to one instance and ping the other instance's private IP" echo "" echo "Viewing created resources..." echo "" echo "=== Resources Created ===" echo "Transit Gateway: $TGW_ID" echo "VPC1: $VPC1_ID" echo "VPC2: $VPC2_ID" echo "Subnet in VPC1: $SUBNET1_ID" echo "Subnet in VPC2: $SUBNET2_ID" echo "Transit Gateway Attachment for VPC1: $TGW_ATTACHMENT_1_ID" echo "Transit Gateway Attachment for VPC2: $TGW_ATTACHMENT_2_ID" echo "" echo "Starting cleanup..." # Delete routes echo "Deleting routes..." aws ec2 delete-route --route-table-id "$RTB1_ID" --destination-cidr-block 10.2.0.0/16 || true aws ec2 delete-route --route-table-id "$RTB2_ID" --destination-cidr-block 10.1.0.0/16 || true # Delete transit gateway attachments echo "Deleting Transit Gateway VPC Attachment for VPC1: $TGW_ATTACHMENT_1_ID" aws ec2 delete-transit-gateway-vpc-attachment \ --transit-gateway-attachment-id "$TGW_ATTACHMENT_1_ID" || true echo "Deleting Transit Gateway VPC Attachment for VPC2: $TGW_ATTACHMENT_2_ID" aws ec2 delete-transit-gateway-vpc-attachment \ --transit-gateway-attachment-id "$TGW_ATTACHMENT_2_ID" || true # Wait for attachments to be deleted wait_for_tgw_attachment_deleted "$TGW_ATTACHMENT_1_ID" || true wait_for_tgw_attachment_deleted "$TGW_ATTACHMENT_2_ID" || true # Delete transit gateway echo "Deleting Transit Gateway: $TGW_ID" aws ec2 delete-transit-gateway --transit-gateway-id "$TGW_ID" || true echo "Cleanup completed successfully" echo "Tutorial completed. See $LOG_FILE for detailed logs."

The following code example shows how to:

  • Create an Application Load Balancer

  • Create a target group

  • Create a listener

  • Verify your configuration

  • Add an HTTPS listener (optional)

  • Add path-based routing (optional)

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Elastic Load Balancing Getting Started Script - v2 # This script creates an Application Load Balancer with HTTP listener and target group set -euo pipefail # Set up logging LOG_FILE="elb-script-v2.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting Elastic Load Balancing setup script at $(date)" echo "All commands and outputs will be logged to $LOG_FILE" # Function to handle errors handle_error() { echo "ERROR: $1" >&2 echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to check AWS CLI command success check_command() { local output="$1" if [[ -z "$output" ]] || [[ "$output" == "None" ]]; then handle_error "AWS CLI command returned empty or invalid output" fi } # Function to validate ARN format validate_arn() { local arn="$1" if [[ ! "$arn" =~ ^arn:aws:[a-z0-9-]+:[a-z0-9-]*:[0-9]{12}:.+$ ]]; then handle_error "Invalid ARN format: $arn" fi } # Function to validate security group ID validate_security_group_id() { local sg_id="$1" if [[ ! "$sg_id" =~ ^sg-[a-f0-9]{8,17}$ ]]; then handle_error "Invalid security group ID format: $sg_id" fi } # Function to validate VPC ID validate_vpc_id() { local vpc_id="$1" if [[ ! "$vpc_id" =~ ^vpc-[a-f0-9]{8,17}$ ]]; then handle_error "Invalid VPC ID format: $vpc_id" fi } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources in reverse order..." if [ -n "${LISTENER_ARN:-}" ]; then echo "Deleting listener: $LISTENER_ARN" aws elbv2 delete-listener --listener-arn "$LISTENER_ARN" 2>/dev/null || true fi if [ -n "${LOAD_BALANCER_ARN:-}" ]; then echo "Deleting load balancer: $LOAD_BALANCER_ARN" aws elbv2 delete-load-balancer --load-balancer-arn "$LOAD_BALANCER_ARN" 2>/dev/null || true # Wait for load balancer to be deleted before deleting target group echo "Waiting for load balancer to be deleted..." aws elbv2 wait load-balancers-deleted --load-balancer-arns "$LOAD_BALANCER_ARN" 2>/dev/null || true fi if [ -n "${TARGET_GROUP_ARN:-}" ]; then echo "Deleting target group: $TARGET_GROUP_ARN" aws elbv2 delete-target-group --target-group-arn "$TARGET_GROUP_ARN" 2>/dev/null || true fi if [ -n "${SECURITY_GROUP_ID:-}" ]; then echo "Waiting 30 seconds before deleting security group to ensure all dependencies are removed..." sleep 30 echo "Deleting security group: $SECURITY_GROUP_ID" local sg_delete_output sg_delete_output=$(aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID" 2>&1 || true) local retry_count=0 local max_retries=5 while echo "$sg_delete_output" | grep -i "DependencyViolation" > /dev/null && [ $retry_count -lt $max_retries ]; do retry_count=$((retry_count+1)) echo "Security group still has dependencies. Retrying in 30 seconds... (Attempt $retry_count of $max_retries)" sleep 30 sg_delete_output=$(aws ec2 delete-security-group --group-id "$SECURITY_GROUP_ID" 2>&1 || true) done if echo "$sg_delete_output" | grep -i "error" > /dev/null; then echo "WARNING: Could not delete security group: $SECURITY_GROUP_ID" >&2 echo "You may need to delete it manually using: aws ec2 delete-security-group --group-id $SECURITY_GROUP_ID" else echo "Security group deleted successfully." fi fi } # Generate a random identifier for resource names RANDOM_ID=$(openssl rand -hex 4) RESOURCE_PREFIX="elb-demo-${RANDOM_ID}" # Verify AWS CLI is available if ! command -v aws &> /dev/null; then handle_error "AWS CLI is not installed or not in PATH" fi # Step 1: Verify AWS CLI support for Elastic Load Balancing echo "Verifying AWS CLI support for Elastic Load Balancing..." if ! aws elbv2 help > /dev/null 2>&1; then handle_error "AWS CLI does not support elbv2 commands. Please update your AWS CLI." fi # Step 2: Get VPC ID and subnet information echo "Retrieving VPC information..." VPC_INFO=$(aws ec2 describe-vpcs --filters "Name=isDefault,Values=true" --query "Vpcs[0].VpcId" --output text 2>/dev/null || echo "") check_command "$VPC_INFO" VPC_ID="$VPC_INFO" validate_vpc_id "$VPC_ID" echo "Using VPC: $VPC_ID" # Get two subnets from different Availability Zones echo "Retrieving subnet information..." SUBNET_INFO=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" --query "Subnets[0:2].SubnetId" --output text 2>/dev/null || echo "") check_command "$SUBNET_INFO" # Convert space-separated list to array read -r -a SUBNETS <<< "$SUBNET_INFO" if [ ${#SUBNETS[@]} -lt 2 ]; then handle_error "Need at least 2 subnets in different Availability Zones. Found: ${#SUBNETS[@]}" fi echo "Using subnets: ${SUBNETS[0]} and ${SUBNETS[1]}" # Step 3: Create a security group for the load balancer echo "Creating security group for the load balancer..." SG_INFO=$(aws ec2 create-security-group \ --group-name "${RESOURCE_PREFIX}-sg" \ --description "Security group for ELB demo" \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=security-group,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=elastic-load-balancing-gs}]' \ --query "GroupId" --output text 2>/dev/null || echo "") check_command "$SG_INFO" SECURITY_GROUP_ID="$SG_INFO" validate_security_group_id "$SECURITY_GROUP_ID" echo "Created security group: $SECURITY_GROUP_ID" # Add inbound rule to allow HTTP traffic echo "Adding inbound rule to allow HTTP traffic..." aws ec2 authorize-security-group-ingress \ --group-id "$SECURITY_GROUP_ID" \ --protocol tcp \ --port 80 \ --cidr "0.0.0.0/0" > /dev/null 2>&1 || handle_error "Failed to authorize security group ingress" echo "WARNING: Security group allows HTTP from 0.0.0.0/0. In production, restrict to specific IP addresses." >&2 # Step 4: Create the load balancer echo "Creating Application Load Balancer..." LB_INFO=$(aws elbv2 create-load-balancer \ --name "${RESOURCE_PREFIX}-lb" \ --subnets "${SUBNETS[0]}" "${SUBNETS[1]}" \ --security-groups "$SECURITY_GROUP_ID" \ --tags Key=project,Value=doc-smith Key=tutorial,Value=elastic-load-balancing-gs \ --query "LoadBalancers[0].LoadBalancerArn" --output text 2>/dev/null || echo "") check_command "$LB_INFO" LOAD_BALANCER_ARN="$LB_INFO" validate_arn "$LOAD_BALANCER_ARN" echo "Created load balancer: $LOAD_BALANCER_ARN" # Wait for the load balancer to be active echo "Waiting for load balancer to become active..." if ! aws elbv2 wait load-balancer-available --load-balancer-arns "$LOAD_BALANCER_ARN" 2>/dev/null; then handle_error "Load balancer did not reach active state within timeout period" fi # Step 5: Create a target group echo "Creating target group..." TG_INFO=$(aws elbv2 create-target-group \ --name "${RESOURCE_PREFIX}-targets" \ --protocol HTTP \ --port 80 \ --vpc-id "$VPC_ID" \ --target-type instance \ --tags Key=project,Value=doc-smith Key=tutorial,Value=elastic-load-balancing-gs \ --query "TargetGroups[0].TargetGroupArn" --output text 2>/dev/null || echo "") check_command "$TG_INFO" TARGET_GROUP_ARN="$TG_INFO" validate_arn "$TARGET_GROUP_ARN" echo "Created target group: $TARGET_GROUP_ARN" # Step 6: Find EC2 instances to register as targets echo "Looking for available EC2 instances to register as targets..." INSTANCES=$(aws ec2 describe-instances \ --filters "Name=vpc-id,Values=$VPC_ID" "Name=instance-state-name,Values=running" \ --query "Reservations[*].Instances[*].InstanceId" --output text 2>/dev/null || echo "") # Convert space-separated list to array read -r -a INSTANCE_IDS <<< "$INSTANCES" if [ ${#INSTANCE_IDS[@]} -eq 0 ]; then echo "No running instances found in VPC $VPC_ID." echo "You will need to register targets manually after launching instances." else # Step 7: Register targets with the target group (up to 2 instances) echo "Registering targets with the target group..." target_args=() for i in "${!INSTANCE_IDS[@]}"; do if [ "$i" -lt 2 ]; then target_args+=("Id=${INSTANCE_IDS[$i]}") fi done if [ ${#target_args[@]} -gt 0 ]; then if aws elbv2 register-targets \ --target-group-arn "$TARGET_GROUP_ARN" \ --targets "${target_args[@]}" 2>/dev/null; then echo "Registered instances: ${target_args[*]}" else handle_error "Failed to register targets" fi fi fi # Step 8: Create a listener echo "Creating HTTP listener..." LISTENER_INFO=$(aws elbv2 create-listener \ --load-balancer-arn "$LOAD_BALANCER_ARN" \ --protocol HTTP \ --port 80 \ --default-actions Type=forward,TargetGroupArn="$TARGET_GROUP_ARN" \ --tags Key=project,Value=doc-smith Key=tutorial,Value=elastic-load-balancing-gs \ --query "Listeners[0].ListenerArn" --output text 2>/dev/null || echo "") check_command "$LISTENER_INFO" LISTENER_ARN="$LISTENER_INFO" validate_arn "$LISTENER_ARN" echo "Created listener: $LISTENER_ARN" # Step 9: Verify target health echo "Verifying target health..." aws elbv2 describe-target-health --target-group-arn "$TARGET_GROUP_ARN" 2>/dev/null || true # Display load balancer DNS name LB_DNS=$(aws elbv2 describe-load-balancers \ --load-balancer-arns "$LOAD_BALANCER_ARN" \ --query "LoadBalancers[0].DNSName" --output text 2>/dev/null || echo "") check_command "$LB_DNS" echo "" echo "==============================================" echo "SETUP COMPLETE" echo "==============================================" echo "Load Balancer DNS Name: $LB_DNS" echo "" echo "Resources created:" echo "- Load Balancer: $LOAD_BALANCER_ARN" echo "- Target Group: $TARGET_GROUP_ARN" echo "- Listener: $LISTENER_ARN" echo "- Security Group: $SECURITY_GROUP_ID" echo "" # Prompt for cleanup confirmation echo "==============================================" echo "CLEANUP CONFIRMATION" echo "==============================================" read -p "Do you want to clean up all created resources? (y/n): " -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Starting cleanup process..." cleanup_resources echo "Cleanup completed." else echo "Resources have been preserved." echo "To clean up later, run the following commands:" echo "aws elbv2 delete-listener --listener-arn $LISTENER_ARN" echo "aws elbv2 delete-load-balancer --load-balancer-arn $LOAD_BALANCER_ARN" echo "aws elbv2 wait load-balancers-deleted --load-balancer-arns $LOAD_BALANCER_ARN" echo "aws elbv2 delete-target-group --target-group-arn $TARGET_GROUP_ARN" echo "aws ec2 delete-security-group --group-id $SECURITY_GROUP_ID" fi echo "Script completed at $(date)"

The following code example shows how to:

  • Verify access to Elemental MediaConnect

  • Create a flow

  • Add an output

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # AWS Elemental MediaConnect Getting Started Tutorial Script # This script creates a MediaConnect flow, adds an output, grants an entitlement, # and then cleans up the resources. set -euo pipefail # Security: Restrict umask to prevent world-readable files umask 0077 # Set up logging with restricted permissions LOG_FILE="mediaconnect-tutorial.log" touch "$LOG_FILE" chmod 600 "$LOG_FILE" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting AWS Elemental MediaConnect tutorial script at $(date)" echo "All commands and outputs will be logged to $LOG_FILE" # Tags for all resources TAGS_ARRAY=("Key=project,Value=doc-smith" "Key=tutorial,Value=aws-elemental-mediaconnect-gs") # Function to handle errors handle_error() { echo "ERROR: $1" >&2 echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to validate AWS CLI is available validate_aws_cli() { if ! command -v aws &> /dev/null; then handle_error "AWS CLI is not installed or not in PATH" fi # Security: Verify AWS CLI version is recent local aws_version aws_version=$(aws --version 2>&1 | head -1) echo "AWS CLI version: $aws_version" if ! aws sts get-caller-identity &> /dev/null; then handle_error "AWS credentials are not configured or invalid" fi # Security: Validate caller identity local account_id account_id=$(aws sts get-caller-identity --query Account --output text 2>/dev/null) if [ -z "$account_id" ]; then handle_error "Failed to retrieve AWS account ID" fi echo "AWS Account ID: $account_id" } # Function to safely extract JSON values using jq (preferred) or fallback extract_json_value() { local json_output="$1" local key="$2" if [ -z "$json_output" ]; then return 1 fi # Security: Use jq if available for safer JSON parsing if command -v jq &> /dev/null; then echo "$json_output" | jq -r ".${key} // empty" 2>/dev/null || return 1 else # Fallback with additional validation if ! echo "$json_output" | grep -q "\"$key\""; then return 1 fi echo "$json_output" | grep -o "\"$key\": \"[^\"]*" | head -1 | cut -d'"' -f4 fi } # Function to tag MediaConnect resource tag_mediaconnect_resource() { local resource_arn="$1" echo "Tagging resource: $resource_arn" for tag in "${TAGS_ARRAY[@]}"; do if ! aws mediaconnect tag-resource --resource-arn "$resource_arn" --tags "$tag" 2>&1; then echo "WARNING: Failed to apply tag $tag to resource" fi done } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources..." if [ -n "${FLOW_ARN:-}" ]; then # Security: Validate ARN format before using it if [[ ! "$FLOW_ARN" =~ ^arn:aws:mediaconnect:[a-z0-9-]+:[0-9]+:flow:[a-zA-Z0-9:-]+$ ]]; then echo "WARNING: Invalid Flow ARN format, skipping cleanup: $FLOW_ARN" return 1 fi # Check flow status before attempting to stop echo "Checking flow status..." local flow_status_output if flow_status_output=$(aws mediaconnect describe-flow --flow-arn "$FLOW_ARN" --query "Flow.Status" --output text 2>&1); then echo "Current flow status: $flow_status_output" if [ "$flow_status_output" == "ACTIVE" ] || [ "$flow_status_output" == "UPDATING" ]; then echo "Stopping flow: $FLOW_ARN" if aws mediaconnect stop-flow --flow-arn "$FLOW_ARN" 2>&1; then # Wait for flow to stop before deleting echo "Waiting for flow to stop..." sleep 10 else echo "WARNING: Failed to stop flow. Attempting to delete anyway..." fi else echo "Flow is not in ACTIVE or UPDATING state, skipping stop operation." fi # Delete the flow echo "Deleting flow: $FLOW_ARN" if aws mediaconnect delete-flow --flow-arn "$FLOW_ARN" 2>&1; then echo "Flow deleted successfully" else echo "WARNING: Failed to delete flow. You may need to manually delete it from the AWS console." fi else echo "WARNING: Could not check flow status" fi fi } # Set trap to cleanup on script exit trap cleanup_resources EXIT # Validate AWS CLI setup validate_aws_cli # Get the current AWS region aws_region="" if aws_region=$(aws configure get region 2>/dev/null); then if [ -z "$aws_region" ]; then handle_error "Failed to get AWS region. Please make sure AWS CLI is configured." fi else handle_error "Failed to retrieve AWS region configuration" fi # Security: Validate region format if [[ ! "$aws_region" =~ ^[a-z]{2}-[a-z]+-[0-9]$ ]]; then handle_error "Invalid AWS region format: $aws_region" fi AWS_REGION="$aws_region" echo "Using AWS Region: $AWS_REGION" # Get available availability zones in the current region echo "Getting available availability zones in region $AWS_REGION..." az_output="" if az_output=$(aws ec2 describe-availability-zones --region "$AWS_REGION" --query "AvailabilityZones[0].ZoneName" --output text 2>&1); then AVAILABILITY_ZONE="$az_output" if [ -z "$AVAILABILITY_ZONE" ]; then handle_error "Failed to retrieve availability zones" fi # Security: Validate AZ format if [[ ! "$AVAILABILITY_ZONE" =~ ^[a-z]{2}-[a-z]+-[0-9][a-z]$ ]]; then handle_error "Invalid availability zone format: $AVAILABILITY_ZONE" fi echo "Using availability zone: $AVAILABILITY_ZONE" else handle_error "Failed to get availability zones" fi # Generate a unique suffix for resource names SUFFIX=$(date +%s | cut -c 6-10) FLOW_NAME="AwardsNYCShow-${SUFFIX}" SOURCE_NAME="AwardsNYCSource-${SUFFIX}" OUTPUT_NAME="AwardsNYCOutput-${SUFFIX}" ENTITLEMENT_NAME="PhillyTeam-${SUFFIX}" echo "Using the following resource names:" echo "Flow name: $FLOW_NAME" echo "Source name: $SOURCE_NAME" echo "Output name: $OUTPUT_NAME" echo "Entitlement name: $ENTITLEMENT_NAME" # Step 1: Verify access to MediaConnect echo "Step 1: Verifying access to AWS Elemental MediaConnect..." list_flows_output="" if list_flows_output=$(aws mediaconnect list-flows 2>&1); then echo "$list_flows_output" else handle_error "Failed to list flows. Please check your AWS credentials and permissions." fi # Step 2: Create a flow echo "Step 2: Creating a flow..." create_flow_output="" if create_flow_output=$(aws mediaconnect create-flow \ --availability-zone "$AVAILABILITY_ZONE" \ --name "$FLOW_NAME" \ --source "Name=$SOURCE_NAME,Protocol=zixi-push,WhitelistCidr=10.24.34.0/23,StreamId=ZixiAwardsNYCFeed" 2>&1); then echo "$create_flow_output" else handle_error "Failed to create flow" fi # Extract the flow ARN from the output FLOW_ARN=$(echo "$create_flow_output" | jq -r '.Flow.FlowArn // empty' 2>/dev/null) if [ -z "$FLOW_ARN" ]; then FLOW_ARN=$(echo "$create_flow_output" | grep -o '"FlowArn": "[^"]*' | head -1 | cut -d'"' -f4) fi if [ -z "$FLOW_ARN" ]; then handle_error "Failed to extract flow ARN from output" fi echo "Flow ARN: $FLOW_ARN" # Validate flow ARN format if [[ ! "$FLOW_ARN" =~ ^arn:aws:mediaconnect:[a-z0-9-]+:[0-9]+:flow:[a-zA-Z0-9:-]+$ ]]; then handle_error "Invalid Flow ARN format: $FLOW_ARN" fi # Tag the flow tag_mediaconnect_resource "$FLOW_ARN" # Step 3: Add an output echo "Step 3: Adding an output to the flow..." add_output_output="" if add_output_output=$(aws mediaconnect add-flow-outputs \ --flow-arn "$FLOW_ARN" \ --outputs "Name=$OUTPUT_NAME,Protocol=zixi-push,Destination=198.51.100.11,Port=1024,StreamId=ZixiAwardsOutput" 2>&1); then echo "$add_output_output" else handle_error "Failed to add output to flow" fi # Extract the output ARN output_arn="" output_arn=$(echo "$add_output_output" | jq -r ".Output.OutputArn // empty" 2>/dev/null) if [ -z "$output_arn" ]; then output_arn=$(echo "$add_output_output" | grep -o '"OutputArn": "[^"]*' | head -1 | cut -d'"' -f4); fi if [ -z "$output_arn" ]; then echo "WARNING: Failed to extract output ARN from output" else OUTPUT_ARN="$output_arn" echo "Output ARN: $OUTPUT_ARN" tag_mediaconnect_resource "$OUTPUT_ARN" fi # Step 4: Grant an entitlement echo "Step 4: Granting an entitlement..." grant_entitlement_output="" if grant_entitlement_output=$(aws mediaconnect grant-flow-entitlements \ --flow-arn "$FLOW_ARN" \ --entitlements "Name=$ENTITLEMENT_NAME,Subscribers=222233334444" 2>&1); then echo "$grant_entitlement_output" else handle_error "Failed to grant entitlement" fi # Extract the entitlement ARN entitlement_arn="" entitlement_arn=$(echo "$grant_entitlement_output" | jq -r '.Entitlement.EntitlementArn // empty' 2>/dev/null) if [ -z "$entitlement_arn" ]; then entitlement_arn=$(echo "$grant_entitlement_output" | grep -o '"EntitlementArn": "[^"]*' | head -1 | cut -d'"' -f4) fi if [ -z "$entitlement_arn" ]; then echo "WARNING: Failed to extract entitlement ARN from output" else ENTITLEMENT_ARN="$entitlement_arn" echo "Entitlement ARN: $ENTITLEMENT_ARN" tag_mediaconnect_resource "$ENTITLEMENT_ARN" fi # Step 5: List entitlements to share with affiliates echo "Step 5: Listing entitlements for the flow..." describe_flow_output="" if describe_flow_output=$(aws mediaconnect describe-flow --flow-arn "$FLOW_ARN" --query "Flow.Entitlements" 2>&1); then echo "Entitlements for the flow:" echo "$describe_flow_output" else handle_error "Failed to describe flow" fi # Display information to share with affiliates echo "" echo "Information to share with your Philadelphia affiliate:" echo "Entitlement ARN: ${ENTITLEMENT_ARN:-N/A}" echo "AWS Region: $AWS_REGION" # Display resource summary echo "" echo "===========================================" echo "RESOURCE SUMMARY" echo "===========================================" echo "The following resources were created:" echo "1. Flow: $FLOW_NAME (ARN: $FLOW_ARN)" echo "2. Output: $OUTPUT_NAME (ARN: ${OUTPUT_ARN:-N/A})" echo "3. Entitlement: $ENTITLEMENT_NAME (ARN: ${ENTITLEMENT_ARN:-N/A})" echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Automatically cleaning up all created resources..." echo "Script completed at $(date)"

The following code example shows how to:

  • Create IAM roles

  • Create a CloudWatch alarm

  • Create an experiment template

  • Run the experiment

  • Verify the results

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # AWS FIS CPU Stress Test Tutorial Script # This script automates the steps in the AWS FIS CPU stress test tutorial # approach using epoch time calculations that work across all Linux distributions # Set up logging LOG_FILE="fis-tutorial-$(date +%Y%m%d-%H%M%S).log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting AWS FIS CPU Stress Test Tutorial Script" echo "Logging to $LOG_FILE" echo "==============================================" # Function to check for errors in command output check_error() { local output=$1 local cmd=$2 if echo "$output" | grep -i "error" > /dev/null; then # Ignore specific expected errors if [[ "$cmd" == *"aws fis get-experiment"* ]] && [[ "$output" == *"ConfigurationFailure"* ]]; then echo "Note: Experiment failed due to configuration issue. This is expected in some cases." return 0 fi echo "ERROR: Command failed: $cmd" echo "Output: $output" cleanup_on_error exit 1 fi } # Function to clean up resources on error cleanup_on_error() { echo "Error encountered. Cleaning up resources..." if [ -n "$EXPERIMENT_ID" ]; then echo "Stopping experiment $EXPERIMENT_ID if running..." aws fis stop-experiment --id "$EXPERIMENT_ID" 2>/dev/null || true fi if [ -n "$TEMPLATE_ID" ]; then echo "Deleting experiment template $TEMPLATE_ID..." aws fis delete-experiment-template --id "$TEMPLATE_ID" || true fi if [ -n "$INSTANCE_ID" ]; then echo "Terminating EC2 instance $INSTANCE_ID..." aws ec2 terminate-instances --instance-ids "$INSTANCE_ID" || true fi if [ -n "$ALARM_NAME" ]; then echo "Deleting CloudWatch alarm $ALARM_NAME..." aws cloudwatch delete-alarms --alarm-names "$ALARM_NAME" || true fi if [ -n "$INSTANCE_PROFILE_NAME" ]; then echo "Removing role from instance profile..." aws iam remove-role-from-instance-profile --instance-profile-name "$INSTANCE_PROFILE_NAME" --role-name "$EC2_ROLE_NAME" || true echo "Deleting instance profile..." aws iam delete-instance-profile --instance-profile-name "$INSTANCE_PROFILE_NAME" || true fi if [ -n "$FIS_ROLE_NAME" ]; then echo "Deleting FIS role policy..." aws iam delete-role-policy --role-name "$FIS_ROLE_NAME" --policy-name "$FIS_POLICY_NAME" || true echo "Deleting FIS role..." aws iam delete-role --role-name "$FIS_ROLE_NAME" || true fi if [ -n "$EC2_ROLE_NAME" ]; then echo "Detaching policy from EC2 role..." aws iam detach-role-policy --role-name "$EC2_ROLE_NAME" --policy-arn "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" || true echo "Deleting EC2 role..." aws iam delete-role --role-name "$EC2_ROLE_NAME" || true fi echo "Cleanup completed." } # Generate unique identifiers for resources TIMESTAMP=$(date +%Y%m%d%H%M%S) FIS_ROLE_NAME="FISRole-${TIMESTAMP}" FIS_POLICY_NAME="FISPolicy-${TIMESTAMP}" EC2_ROLE_NAME="EC2SSMRole-${TIMESTAMP}" INSTANCE_PROFILE_NAME="EC2SSMProfile-${TIMESTAMP}" ALARM_NAME="FIS-CPU-Alarm-${TIMESTAMP}" # Track created resources CREATED_RESOURCES=() echo "Step 1: Creating IAM role for AWS FIS" # Create trust policy file for AWS FIS cat > fis-trust-policy.json << 'EOF' { "Version":"2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "fis.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF # Create IAM role for FIS echo "Creating IAM role $FIS_ROLE_NAME for AWS FIS..." FIS_ROLE_OUTPUT=$(aws iam create-role \ --role-name "$FIS_ROLE_NAME" \ --assume-role-policy-document file://fis-trust-policy.json) check_error "$FIS_ROLE_OUTPUT" "aws iam create-role" aws iam tag-role --role-name "$FIS_ROLE_NAME" --tags Key=project,Value=doc-smith Key=tutorial,Value=aws-fault-injection-service-gs CREATED_RESOURCES+=("IAM Role: $FIS_ROLE_NAME") # Create policy document for SSM actions cat > fis-ssm-policy.json << 'EOF' { "Version":"2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "ssm:SendCommand", "ssm:ListCommands", "ssm:ListCommandInvocations" ], "Resource": "*" } ] } EOF # Attach policy to the role echo "Attaching policy $FIS_POLICY_NAME to role $FIS_ROLE_NAME..." FIS_POLICY_OUTPUT=$(aws iam put-role-policy \ --role-name "$FIS_ROLE_NAME" \ --policy-name "$FIS_POLICY_NAME" \ --policy-document file://fis-ssm-policy.json) check_error "$FIS_POLICY_OUTPUT" "aws iam put-role-policy" CREATED_RESOURCES+=("IAM Policy: $FIS_POLICY_NAME attached to $FIS_ROLE_NAME") echo "Step 2: Creating IAM role for EC2 instance with SSM permissions" # Create trust policy file for EC2 cat > ec2-trust-policy.json << 'EOF' { "Version":"2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "ec2.amazonaws.com" }, "Action": "sts:AssumeRole" } ] } EOF # Create IAM role for EC2 echo "Creating IAM role $EC2_ROLE_NAME for EC2 instance..." EC2_ROLE_OUTPUT=$(aws iam create-role \ --role-name "$EC2_ROLE_NAME" \ --assume-role-policy-document file://ec2-trust-policy.json) check_error "$EC2_ROLE_OUTPUT" "aws iam create-role" aws iam tag-role --role-name "$EC2_ROLE_NAME" --tags Key=project,Value=doc-smith Key=tutorial,Value=aws-fault-injection-service-gs CREATED_RESOURCES+=("IAM Role: $EC2_ROLE_NAME") # Attach SSM policy to the EC2 role echo "Attaching AmazonSSMManagedInstanceCore policy to role $EC2_ROLE_NAME..." EC2_POLICY_OUTPUT=$(aws iam attach-role-policy \ --role-name "$EC2_ROLE_NAME" \ --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore) check_error "$EC2_POLICY_OUTPUT" "aws iam attach-role-policy" CREATED_RESOURCES+=("IAM Policy: AmazonSSMManagedInstanceCore attached to $EC2_ROLE_NAME") # Create instance profile echo "Creating instance profile $INSTANCE_PROFILE_NAME..." PROFILE_OUTPUT=$(aws iam create-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME") check_error "$PROFILE_OUTPUT" "aws iam create-instance-profile" CREATED_RESOURCES+=("IAM Instance Profile: $INSTANCE_PROFILE_NAME") # Add role to instance profile echo "Adding role $EC2_ROLE_NAME to instance profile $INSTANCE_PROFILE_NAME..." ADD_ROLE_OUTPUT=$(aws iam add-role-to-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME" \ --role-name "$EC2_ROLE_NAME") check_error "$ADD_ROLE_OUTPUT" "aws iam add-role-to-instance-profile" # Wait for role to propagate echo "Waiting for IAM role to propagate..." sleep 10 echo "Step 3: Launching EC2 instance" # Get the latest Amazon Linux 2 AMI ID echo "Finding latest Amazon Linux 2 AMI..." AMI_ID=$(aws ec2 describe-images \ --owners amazon \ --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" "Name=state,Values=available" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" \ --output text) check_error "$AMI_ID" "aws ec2 describe-images" echo "Using AMI: $AMI_ID" # Launch EC2 instance echo "Launching EC2 instance with AMI $AMI_ID..." INSTANCE_OUTPUT=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --instance-type t2.micro \ --iam-instance-profile Name="$INSTANCE_PROFILE_NAME" \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=FIS-Test-Instance},{Key=project,Value=doc-smith},{Key=tutorial,Value=aws-fault-injection-service-gs}]') check_error "$INSTANCE_OUTPUT" "aws ec2 run-instances" # Get instance ID INSTANCE_ID=$(echo "$INSTANCE_OUTPUT" | grep -i "InstanceId" | head -1 | awk -F'"' '{print $4}') if [ -z "$INSTANCE_ID" ]; then echo "Failed to get instance ID" cleanup_on_error exit 1 fi echo "Launched instance: $INSTANCE_ID" CREATED_RESOURCES+=("EC2 Instance: $INSTANCE_ID") # Enable detailed monitoring echo "Enabling detailed monitoring for instance $INSTANCE_ID..." MONITOR_OUTPUT=$(aws ec2 monitor-instances --instance-ids "$INSTANCE_ID") check_error "$MONITOR_OUTPUT" "aws ec2 monitor-instances" # Wait for instance to be running and status checks to pass echo "Waiting for instance to be ready..." aws ec2 wait instance-running --instance-ids "$INSTANCE_ID" aws ec2 wait instance-status-ok --instance-ids "$INSTANCE_ID" echo "Instance is ready" echo "Step 4: Creating CloudWatch alarm for CPU utilization" # Create CloudWatch alarm echo "Creating CloudWatch alarm $ALARM_NAME..." ALARM_OUTPUT=$(aws cloudwatch put-metric-alarm \ --alarm-name "$ALARM_NAME" \ --alarm-description "Alarm when CPU exceeds 50%" \ --metric-name CPUUtilization \ --namespace AWS/EC2 \ --statistic Maximum \ --period 60 \ --threshold 50 \ --comparison-operator GreaterThanOrEqualToThreshold \ --dimensions "Name=InstanceId,Value=$INSTANCE_ID" \ --evaluation-periods 1) check_error "$ALARM_OUTPUT" "aws cloudwatch put-metric-alarm" CREATED_RESOURCES+=("CloudWatch Alarm: $ALARM_NAME") # Get the alarm ARN echo "Getting CloudWatch alarm ARN..." ALARM_ARN_OUTPUT=$(aws cloudwatch describe-alarms \ --alarm-names "$ALARM_NAME") check_error "$ALARM_ARN_OUTPUT" "aws cloudwatch describe-alarms" ALARM_ARN=$(echo "$ALARM_ARN_OUTPUT" | grep -i "AlarmArn" | head -1 | awk -F'"' '{print $4}') if [ -z "$ALARM_ARN" ]; then echo "Failed to get alarm ARN" cleanup_on_error exit 1 fi echo "Alarm ARN: $ALARM_ARN" # Wait for the alarm to initialize and reach OK state echo "Waiting for CloudWatch alarm to initialize (60 seconds)..." sleep 60 # Check alarm state echo "Checking alarm state..." ALARM_STATE_OUTPUT=$(aws cloudwatch describe-alarms \ --alarm-names "$ALARM_NAME") ALARM_STATE=$(echo "$ALARM_STATE_OUTPUT" | grep -i "StateValue" | head -1 | awk -F'"' '{print $4}') echo "Current alarm state: $ALARM_STATE" # If alarm is not in OK state, wait longer or generate some baseline metrics if [ "$ALARM_STATE" != "OK" ]; then echo "Alarm not in OK state. Waiting for alarm to stabilize (additional 60 seconds)..." sleep 60 # Check alarm state again ALARM_STATE_OUTPUT=$(aws cloudwatch describe-alarms \ --alarm-names "$ALARM_NAME") ALARM_STATE=$(echo "$ALARM_STATE_OUTPUT" | grep -i "StateValue" | head -1 | awk -F'"' '{print $4}') echo "Updated alarm state: $ALARM_STATE" if [ "$ALARM_STATE" != "OK" ]; then echo "Warning: Alarm still not in OK state. Experiment may fail to start." fi fi echo "Step 5: Creating AWS FIS experiment template" # Get the IAM role ARN echo "Getting IAM role ARN for $FIS_ROLE_NAME..." ROLE_ARN_OUTPUT=$(aws iam get-role \ --role-name "$FIS_ROLE_NAME") check_error "$ROLE_ARN_OUTPUT" "aws iam get-role" ROLE_ARN=$(echo "$ROLE_ARN_OUTPUT" | grep -i "Arn" | head -1 | awk -F'"' '{print $4}') if [ -z "$ROLE_ARN" ]; then echo "Failed to get role ARN" cleanup_on_error exit 1 fi echo "Role ARN: $ROLE_ARN" # Get account ID and region ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text) REGION=$(aws configure get region) if [ -z "$REGION" ]; then REGION="us-east-1" # Default to us-east-1 if region not set fi INSTANCE_ARN="arn:aws:ec2:${REGION}:${ACCOUNT_ID}:instance/${INSTANCE_ID}" echo "Instance ARN: $INSTANCE_ARN" # Create experiment template - Fixed JSON escaping issue cat > experiment-template.json << EOF { "description": "Test CPU stress predefined SSM document", "targets": { "testInstance": { "resourceType": "aws:ec2:instance", "resourceArns": ["$INSTANCE_ARN"], "selectionMode": "ALL" } }, "actions": { "runCpuStress": { "actionId": "aws:ssm:send-command", "parameters": { "documentArn": "arn:aws:ssm:$REGION::document/AWSFIS-Run-CPU-Stress", "documentParameters": "{\"DurationSeconds\":\"120\"}", "duration": "PT5M" }, "targets": { "Instances": "testInstance" } } }, "stopConditions": [ { "source": "aws:cloudwatch:alarm", "value": "$ALARM_ARN" } ], "roleArn": "$ROLE_ARN", "tags": { "Name": "FIS-CPU-Stress-Experiment", "project": "doc-smith", "tutorial": "aws-fault-injection-service-gs" } } EOF # Create experiment template echo "Creating AWS FIS experiment template..." TEMPLATE_OUTPUT=$(aws fis create-experiment-template --cli-input-json file://experiment-template.json) check_error "$TEMPLATE_OUTPUT" "aws fis create-experiment-template" TEMPLATE_ID=$(echo "$TEMPLATE_OUTPUT" | grep -i "id" | head -1 | awk -F'"' '{print $4}') if [ -z "$TEMPLATE_ID" ]; then echo "Failed to get template ID" cleanup_on_error exit 1 fi echo "Experiment template created with ID: $TEMPLATE_ID" CREATED_RESOURCES+=("FIS Experiment Template: $TEMPLATE_ID") echo "Step 6: Starting the experiment" # Start the experiment echo "Starting AWS FIS experiment using template $TEMPLATE_ID..." EXPERIMENT_OUTPUT=$(aws fis start-experiment \ --experiment-template-id "$TEMPLATE_ID" \ --tags '{"Name": "FIS-CPU-Stress-Run"}') check_error "$EXPERIMENT_OUTPUT" "aws fis start-experiment" EXPERIMENT_ID=$(echo "$EXPERIMENT_OUTPUT" | grep -i "id" | head -1 | awk -F'"' '{print $4}') if [ -z "$EXPERIMENT_ID" ]; then echo "Failed to get experiment ID" cleanup_on_error exit 1 fi echo "Experiment started with ID: $EXPERIMENT_ID" CREATED_RESOURCES+=("FIS Experiment: $EXPERIMENT_ID") echo "Step 7: Tracking experiment progress" # Track experiment progress echo "Tracking experiment progress..." MAX_CHECKS=30 CHECK_COUNT=0 EXPERIMENT_STATE="" while [ $CHECK_COUNT -lt $MAX_CHECKS ]; do EXPERIMENT_INFO=$(aws fis get-experiment --id "$EXPERIMENT_ID") # Don't check for errors here, as we expect some experiments to fail EXPERIMENT_STATE=$(echo "$EXPERIMENT_INFO" | grep -i "status" | head -1 | awk -F'"' '{print $4}') echo "Experiment state: $EXPERIMENT_STATE" if [ "$EXPERIMENT_STATE" == "completed" ] || [ "$EXPERIMENT_STATE" == "stopped" ] || [ "$EXPERIMENT_STATE" == "failed" ]; then # Show the reason for the state REASON=$(echo "$EXPERIMENT_INFO" | grep -i "reason" | head -1 | awk -F'"' '{print $4}') if [ -n "$REASON" ]; then echo "Reason: $REASON" fi break fi echo "Waiting 10 seconds before checking again..." sleep 10 CHECK_COUNT=$((CHECK_COUNT + 1)) done if [ $CHECK_COUNT -eq $MAX_CHECKS ]; then echo "Experiment is taking longer than expected. You can check its status later using:" echo "aws fis get-experiment --id $EXPERIMENT_ID" fi echo "Step 8: Verifying experiment results" # Check CloudWatch alarm state echo "Checking CloudWatch alarm state..." ALARM_STATE_OUTPUT=$(aws cloudwatch describe-alarms --alarm-names "$ALARM_NAME") check_error "$ALARM_STATE_OUTPUT" "aws cloudwatch describe-alarms" echo "$ALARM_STATE_OUTPUT" # Get CPU utilization metrics echo "Getting CPU utilization metrics..." END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ") # FIXED: Cross-platform compatible way to calculate time 10 minutes ago # This approach uses epoch seconds and basic arithmetic which works on all Linux distributions CURRENT_EPOCH=$(date +%s) TEN_MINUTES_AGO_EPOCH=$((CURRENT_EPOCH - 600)) START_TIME=$(date -u -d "@$TEN_MINUTES_AGO_EPOCH" +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u -r "$TEN_MINUTES_AGO_EPOCH" +"%Y-%m-%dT%H:%M:%SZ") # Create metric query file cat > metric-query.json << EOF [ { "Id": "cpu", "MetricStat": { "Metric": { "Namespace": "AWS/EC2", "MetricName": "CPUUtilization", "Dimensions": [ { "Name": "InstanceId", "Value": "$INSTANCE_ID" } ] }, "Period": 60, "Stat": "Maximum" } } ] EOF METRICS_OUTPUT=$(aws cloudwatch get-metric-data \ --start-time "$START_TIME" \ --end-time "$END_TIME" \ --metric-data-queries file://metric-query.json) check_error "$METRICS_OUTPUT" "aws cloudwatch get-metric-data" echo "CPU Utilization Metrics:" echo "$METRICS_OUTPUT" # Display summary of created resources echo "" echo "===========================================" echo "RESOURCES CREATED" echo "===========================================" for resource in "${CREATED_RESOURCES[@]}"; do echo "- $resource" done echo "===========================================" # Prompt for cleanup echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Starting cleanup process..." # Stop experiment if still running if [ "$EXPERIMENT_STATE" != "completed" ] && [ "$EXPERIMENT_STATE" != "stopped" ] && [ "$EXPERIMENT_STATE" != "failed" ]; then echo "Stopping experiment $EXPERIMENT_ID..." STOP_OUTPUT=$(aws fis stop-experiment --id "$EXPERIMENT_ID") check_error "$STOP_OUTPUT" "aws fis stop-experiment" echo "Waiting for experiment to stop..." sleep 10 fi # Delete experiment template echo "Deleting experiment template $TEMPLATE_ID..." DELETE_TEMPLATE_OUTPUT=$(aws fis delete-experiment-template --id "$TEMPLATE_ID") check_error "$DELETE_TEMPLATE_OUTPUT" "aws fis delete-experiment-template" # Delete CloudWatch alarm echo "Deleting CloudWatch alarm $ALARM_NAME..." DELETE_ALARM_OUTPUT=$(aws cloudwatch delete-alarms --alarm-names "$ALARM_NAME") check_error "$DELETE_ALARM_OUTPUT" "aws cloudwatch delete-alarms" # Terminate EC2 instance echo "Terminating EC2 instance $INSTANCE_ID..." TERMINATE_OUTPUT=$(aws ec2 terminate-instances --instance-ids "$INSTANCE_ID") check_error "$TERMINATE_OUTPUT" "aws ec2 terminate-instances" echo "Waiting for instance to terminate..." aws ec2 wait instance-terminated --instance-ids "$INSTANCE_ID" # Clean up IAM resources echo "Removing role from instance profile..." REMOVE_ROLE_OUTPUT=$(aws iam remove-role-from-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME" \ --role-name "$EC2_ROLE_NAME") check_error "$REMOVE_ROLE_OUTPUT" "aws iam remove-role-from-instance-profile" echo "Deleting instance profile..." DELETE_PROFILE_OUTPUT=$(aws iam delete-instance-profile \ --instance-profile-name "$INSTANCE_PROFILE_NAME") check_error "$DELETE_PROFILE_OUTPUT" "aws iam delete-instance-profile" echo "Deleting FIS role policy..." DELETE_POLICY_OUTPUT=$(aws iam delete-role-policy \ --role-name "$FIS_ROLE_NAME" \ --policy-name "$FIS_POLICY_NAME") check_error "$DELETE_POLICY_OUTPUT" "aws iam delete-role-policy" echo "Detaching policy from EC2 role..." DETACH_POLICY_OUTPUT=$(aws iam detach-role-policy \ --role-name "$EC2_ROLE_NAME" \ --policy-arn "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore") check_error "$DETACH_POLICY_OUTPUT" "aws iam detach-role-policy" echo "Deleting FIS role..." DELETE_FIS_ROLE_OUTPUT=$(aws iam delete-role \ --role-name "$FIS_ROLE_NAME") check_error "$DELETE_FIS_ROLE_OUTPUT" "aws iam delete-role" echo "Deleting EC2 role..." DELETE_EC2_ROLE_OUTPUT=$(aws iam delete-role \ --role-name "$EC2_ROLE_NAME") check_error "$DELETE_EC2_ROLE_OUTPUT" "aws iam delete-role" # Clean up temporary files echo "Cleaning up temporary files..." rm -f fis-trust-policy.json ec2-trust-policy.json fis-ssm-policy.json experiment-template.json metric-query.json echo "Cleanup completed successfully." else echo "Cleanup skipped. Resources will remain in your AWS account." echo "You can manually clean up the resources listed above." fi echo "" echo "Script execution completed." echo "Log file: $LOG_FILE"

The following code example shows how to:

  • Set up and configure Amazon VPC IP Address Manager (IPAM) using the CLI.

  • Create an IPAM with operating regions (e.g., us-east-1, us-west-2).

  • Retrieve the private scope ID for the IPAM.

  • Create a hierarchical structure of IPv4 pools (top-level, regional, and development pools).

  • Provision CIDR blocks to each pool (e.g., 10.0.0.0/8, 10.0.0.0/16, 10.0.0.0/24).

  • Create a VPC using a CIDR allocated from an IPAM pool.

  • Verify IPAM pool allocations and VPC creation.

  • Troubleshoot common issues like permission errors, CIDR allocation failures, and dependency violations.

  • Clean up IPAM resources (VPC, pools, CIDRs, and IPAM) to avoid unnecessary charges.

  • Explore next steps for advanced IPAM features.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # IPAM Getting Started CLI Script - Version 7 # This script creates an IPAM, creates a hierarchy of IP address pools, and allocates a CIDR to a VPC # Fixed to correctly identify the private scope ID, wait for resources to be available, add locale to development pool, # use the correct parameter names for VPC creation, and wait for CIDR provisioning to complete # Set up logging LOG_FILE="ipam_script.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting IPAM setup script at $(date)" echo "All commands and outputs will be logged to $LOG_FILE" # Function to handle errors handle_error() { echo "ERROR: $1" echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to clean up resources cleanup_resources() { echo "" echo "===========================================" echo "RESOURCES CREATED:" echo "===========================================" if [ -n "$VPC_ID" ]; then echo "VPC: $VPC_ID" fi if [ -n "$DEV_POOL_ID" ]; then echo "Development Pool: $DEV_POOL_ID" fi if [ -n "$REGIONAL_POOL_ID" ]; then echo "Regional Pool: $REGIONAL_POOL_ID" fi if [ -n "$TOP_POOL_ID" ]; then echo "Top-level Pool: $TOP_POOL_ID" fi if [ -n "$IPAM_ID" ]; then echo "IPAM: $IPAM_ID" fi echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Starting cleanup..." # Delete resources in reverse order of creation to handle dependencies if [ -n "$VPC_ID" ]; then echo "Deleting VPC: $VPC_ID" aws ec2 delete-vpc --vpc-id "$VPC_ID" || echo "Failed to delete VPC" echo "Waiting for VPC to be deleted..." sleep 10 fi if [ -n "$DEV_POOL_ID" ]; then echo "Deleting Development Pool: $DEV_POOL_ID" # First deprovision any CIDRs from the pool CIDRS=$(aws ec2 get-ipam-pool-cidrs --ipam-pool-id "$DEV_POOL_ID" --query 'IpamPoolCidrs[].Cidr' --output text) for CIDR in $CIDRS; do echo "Deprovisioning CIDR $CIDR from Development Pool" aws ec2 deprovision-ipam-pool-cidr --ipam-pool-id "$DEV_POOL_ID" --cidr "$CIDR" || echo "Failed to deprovision CIDR $CIDR" sleep 5 done aws ec2 delete-ipam-pool --ipam-pool-id "$DEV_POOL_ID" || echo "Failed to delete Development Pool" echo "Waiting for Development Pool to be deleted..." sleep 10 fi if [ -n "$REGIONAL_POOL_ID" ]; then echo "Deleting Regional Pool: $REGIONAL_POOL_ID" # First deprovision any CIDRs from the pool CIDRS=$(aws ec2 get-ipam-pool-cidrs --ipam-pool-id "$REGIONAL_POOL_ID" --query 'IpamPoolCidrs[].Cidr' --output text) for CIDR in $CIDRS; do echo "Deprovisioning CIDR $CIDR from Regional Pool" aws ec2 deprovision-ipam-pool-cidr --ipam-pool-id "$REGIONAL_POOL_ID" --cidr "$CIDR" || echo "Failed to deprovision CIDR $CIDR" sleep 5 done aws ec2 delete-ipam-pool --ipam-pool-id "$REGIONAL_POOL_ID" || echo "Failed to delete Regional Pool" echo "Waiting for Regional Pool to be deleted..." sleep 10 fi if [ -n "$TOP_POOL_ID" ]; then echo "Deleting Top-level Pool: $TOP_POOL_ID" # First deprovision any CIDRs from the pool CIDRS=$(aws ec2 get-ipam-pool-cidrs --ipam-pool-id "$TOP_POOL_ID" --query 'IpamPoolCidrs[].Cidr' --output text) for CIDR in $CIDRS; do echo "Deprovisioning CIDR $CIDR from Top-level Pool" aws ec2 deprovision-ipam-pool-cidr --ipam-pool-id "$TOP_POOL_ID" --cidr "$CIDR" || echo "Failed to deprovision CIDR $CIDR" sleep 5 done aws ec2 delete-ipam-pool --ipam-pool-id "$TOP_POOL_ID" || echo "Failed to delete Top-level Pool" echo "Waiting for Top-level Pool to be deleted..." sleep 10 fi if [ -n "$IPAM_ID" ]; then echo "Deleting IPAM: $IPAM_ID" aws ec2 delete-ipam --ipam-id "$IPAM_ID" || echo "Failed to delete IPAM" fi echo "Cleanup completed." else echo "Cleanup skipped. Resources will remain in your account." fi } # Function to wait for a pool to be in the 'create-complete' state wait_for_pool() { local pool_id=$1 local max_attempts=30 local attempt=1 local state="" echo "Waiting for pool $pool_id to be available..." while [ $attempt -le $max_attempts ]; do state=$(aws ec2 describe-ipam-pools --ipam-pool-ids "$pool_id" --query 'IpamPools[0].State' --output text) if [ "$state" = "create-complete" ]; then echo "Pool $pool_id is now available (state: $state)" return 0 fi echo "Attempt $attempt/$max_attempts: Pool $pool_id is in state: $state. Waiting..." sleep 10 ((attempt++)) done echo "Timed out waiting for pool $pool_id to be available" return 1 } # Function to wait for a CIDR to be fully provisioned wait_for_cidr_provisioning() { local pool_id=$1 local cidr=$2 local max_attempts=30 local attempt=1 local state="" echo "Waiting for CIDR $cidr to be fully provisioned in pool $pool_id..." while [ $attempt -le $max_attempts ]; do state=$(aws ec2 get-ipam-pool-cidrs --ipam-pool-id "$pool_id" --query "IpamPoolCidrs[?Cidr=='$cidr'].State" --output text) if [ "$state" = "provisioned" ]; then echo "CIDR $cidr is now fully provisioned (state: $state)" return 0 fi echo "Attempt $attempt/$max_attempts: CIDR $cidr is in state: $state. Waiting..." sleep 10 ((attempt++)) done echo "Timed out waiting for CIDR $cidr to be provisioned" return 1 } # Step 1: Create an IPAM echo "Creating IPAM..." IPAM_RESULT=$(aws ec2 create-ipam \ --description "My IPAM" \ --operating-regions RegionName=us-east-1 RegionName=us-west-2 \ --tag-specifications 'ResourceType=ipam,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-ipam-gs}]') if [ $? -ne 0 ]; then handle_error "Failed to create IPAM" fi IPAM_ID=$(echo "$IPAM_RESULT" | grep -o '"IpamId": "[^"]*' | cut -d'"' -f4) echo "IPAM created with ID: $IPAM_ID" # Wait for IPAM to be created and available echo "Waiting for IPAM to be available..." sleep 20 # Step 2: Get the IPAM Scope ID - FIXED to correctly identify the private scope echo "Getting IPAM Scope ID..." SCOPE_RESULT=$(aws ec2 describe-ipams --ipam-id "$IPAM_ID") if [ $? -ne 0 ]; then handle_error "Failed to get IPAM details" fi # Extract the private scope ID directly from the IPAM details PRIVATE_SCOPE_ID=$(echo "$SCOPE_RESULT" | grep -o '"PrivateDefaultScopeId": "[^"]*' | cut -d'"' -f4) echo "Private Scope ID: $PRIVATE_SCOPE_ID" if [ -z "$PRIVATE_SCOPE_ID" ]; then handle_error "Failed to get Private Scope ID" fi # Step 3: Create a Top-Level IPv4 Pool echo "Creating Top-level IPv4 Pool..." TOP_POOL_RESULT=$(aws ec2 create-ipam-pool \ --ipam-scope-id "$PRIVATE_SCOPE_ID" \ --address-family ipv4 \ --description "Top-level pool" \ --tag-specifications 'ResourceType=ipam-pool,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-ipam-gs}]') if [ $? -ne 0 ]; then handle_error "Failed to create Top-level Pool" fi TOP_POOL_ID=$(echo "$TOP_POOL_RESULT" | grep -o '"IpamPoolId": "[^"]*' | cut -d'"' -f4) echo "Top-level Pool created with ID: $TOP_POOL_ID" # Wait for the top-level pool to be available if ! wait_for_pool "$TOP_POOL_ID"; then handle_error "Top-level Pool did not become available in time" fi # Provision CIDR to the top-level pool echo "Provisioning CIDR to Top-level Pool..." TOP_POOL_CIDR="10.0.0.0/8" PROVISION_RESULT=$(aws ec2 provision-ipam-pool-cidr \ --ipam-pool-id "$TOP_POOL_ID" \ --cidr "$TOP_POOL_CIDR") if [ $? -ne 0 ]; then handle_error "Failed to provision CIDR to Top-level Pool" fi echo "$PROVISION_RESULT" # Wait for the CIDR to be fully provisioned if ! wait_for_cidr_provisioning "$TOP_POOL_ID" "$TOP_POOL_CIDR"; then handle_error "CIDR provisioning to Top-level Pool did not complete in time" fi # Step 4: Create a Regional IPv4 Pool echo "Creating Regional IPv4 Pool..." REGIONAL_POOL_RESULT=$(aws ec2 create-ipam-pool \ --ipam-scope-id "$PRIVATE_SCOPE_ID" \ --source-ipam-pool-id "$TOP_POOL_ID" \ --locale us-east-1 \ --address-family ipv4 \ --description "Regional pool in us-east-1" \ --tag-specifications 'ResourceType=ipam-pool,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-ipam-gs}]') if [ $? -ne 0 ]; then handle_error "Failed to create Regional Pool" fi REGIONAL_POOL_ID=$(echo "$REGIONAL_POOL_RESULT" | grep -o '"IpamPoolId": "[^"]*' | cut -d'"' -f4) echo "Regional Pool created with ID: $REGIONAL_POOL_ID" # Wait for the regional pool to be available if ! wait_for_pool "$REGIONAL_POOL_ID"; then handle_error "Regional Pool did not become available in time" fi # Provision CIDR to the regional pool echo "Provisioning CIDR to Regional Pool..." REGIONAL_POOL_CIDR="10.0.0.0/16" PROVISION_RESULT=$(aws ec2 provision-ipam-pool-cidr \ --ipam-pool-id "$REGIONAL_POOL_ID" \ --cidr "$REGIONAL_POOL_CIDR") if [ $? -ne 0 ]; then handle_error "Failed to provision CIDR to Regional Pool" fi echo "$PROVISION_RESULT" # Wait for the CIDR to be fully provisioned if ! wait_for_cidr_provisioning "$REGIONAL_POOL_ID" "$REGIONAL_POOL_CIDR"; then handle_error "CIDR provisioning to Regional Pool did not complete in time" fi # Step 5: Create a Development IPv4 Pool - FIXED to include locale echo "Creating Development IPv4 Pool..." DEV_POOL_RESULT=$(aws ec2 create-ipam-pool \ --ipam-scope-id "$PRIVATE_SCOPE_ID" \ --source-ipam-pool-id "$REGIONAL_POOL_ID" \ --locale us-east-1 \ --address-family ipv4 \ --description "Development pool" \ --tag-specifications 'ResourceType=ipam-pool,Tags=[{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-ipam-gs}]') if [ $? -ne 0 ]; then handle_error "Failed to create Development Pool" fi DEV_POOL_ID=$(echo "$DEV_POOL_RESULT" | grep -o '"IpamPoolId": "[^"]*' | cut -d'"' -f4) echo "Development Pool created with ID: $DEV_POOL_ID" # Wait for the development pool to be available if ! wait_for_pool "$DEV_POOL_ID"; then handle_error "Development Pool did not become available in time" fi # Provision CIDR to the development pool echo "Provisioning CIDR to Development Pool..." DEV_POOL_CIDR="10.0.0.0/24" PROVISION_RESULT=$(aws ec2 provision-ipam-pool-cidr \ --ipam-pool-id "$DEV_POOL_ID" \ --cidr "$DEV_POOL_CIDR") if [ $? -ne 0 ]; then handle_error "Failed to provision CIDR to Development Pool" fi echo "$PROVISION_RESULT" # Wait for the CIDR to be fully provisioned if ! wait_for_cidr_provisioning "$DEV_POOL_ID" "$DEV_POOL_CIDR"; then handle_error "CIDR provisioning to Development Pool did not complete in time" fi # Step 6: Create a VPC Using an IPAM Pool CIDR - FIXED to use the correct parameter names and a smaller netmask length echo "Creating VPC using IPAM Pool CIDR..." VPC_RESULT=$(aws ec2 create-vpc \ --ipv4-ipam-pool-id "$DEV_POOL_ID" \ --ipv4-netmask-length 26 \ --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=IPAM-VPC},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-ipam-gs}]') if [ $? -ne 0 ]; then handle_error "Failed to create VPC" fi VPC_ID=$(echo "$VPC_RESULT" | grep -o '"VpcId": "[^"]*' | cut -d'"' -f4) echo "VPC created with ID: $VPC_ID" # Step 7: Verify the IPAM Pool Allocation echo "Verifying IPAM Pool Allocation..." ALLOCATION_RESULT=$(aws ec2 get-ipam-pool-allocations \ --ipam-pool-id "$DEV_POOL_ID") if [ $? -ne 0 ]; then handle_error "Failed to verify IPAM Pool Allocation" fi echo "IPAM Pool Allocation verified:" echo "$ALLOCATION_RESULT" | grep -A 5 "Allocations" echo "" echo "IPAM setup completed successfully!" echo "" # Prompt for cleanup cleanup_resources echo "Script completed at $(date)" exit 0

The following code example shows how to:

  • Create a VPC with private subnets and NAT gateways using the CLI.

  • Set up the necessary components including VPC, subnets, route tables, and NAT gateways.

  • Configure security groups and IAM roles for proper access and security.

  • Use CLI commands to automate the creation and configuration of these resources.

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # VPC with Private Subnets and NAT Gateways (IMDSv2 Compliant Version) # This script creates a VPC with public and private subnets in two Availability Zones, # NAT gateways, an internet gateway, route tables, a VPC endpoint for S3, # security groups, a launch template, an Auto Scaling group, and an Application Load Balancer. # Set up logging LOG_FILE="vpc-private-subnets-nat.log" exec > >(tee -a "$LOG_FILE") 2>&1 # Cleanup function to delete all created resources cleanup_resources() { echo "Cleaning up resources..." # Delete Auto Scaling group if it exists if [ -n "${ASG_NAME:-}" ]; then echo "Deleting Auto Scaling group: $ASG_NAME" aws autoscaling delete-auto-scaling-group --auto-scaling-group-name "$ASG_NAME" --force-delete echo "Waiting for Auto Scaling group to be deleted..." aws autoscaling wait auto-scaling-groups-deleted --auto-scaling-group-names "$ASG_NAME" fi # Delete load balancer if it exists if [ -n "${LB_ARN:-}" ]; then echo "Deleting load balancer: $LB_ARN" aws elbv2 delete-load-balancer --load-balancer-arn "$LB_ARN" # Wait for load balancer to be deleted sleep 30 fi # Delete target group if it exists if [ -n "${TARGET_GROUP_ARN:-}" ]; then echo "Deleting target group: $TARGET_GROUP_ARN" aws elbv2 delete-target-group --target-group-arn "$TARGET_GROUP_ARN" fi # Delete launch template if it exists if [ -n "${LAUNCH_TEMPLATE_NAME:-}" ]; then echo "Deleting launch template: $LAUNCH_TEMPLATE_NAME" aws ec2 delete-launch-template --launch-template-name "$LAUNCH_TEMPLATE_NAME" fi # Delete NAT Gateways if they exist if [ -n "${NAT_GW1_ID:-}" ]; then echo "Deleting NAT Gateway 1: $NAT_GW1_ID" aws ec2 delete-nat-gateway --nat-gateway-id "$NAT_GW1_ID" fi if [ -n "${NAT_GW2_ID:-}" ]; then echo "Deleting NAT Gateway 2: $NAT_GW2_ID" aws ec2 delete-nat-gateway --nat-gateway-id "$NAT_GW2_ID" fi # Wait for NAT Gateways to be deleted if [ -n "${NAT_GW1_ID:-}" ] || [ -n "${NAT_GW2_ID:-}" ]; then echo "Waiting for NAT Gateways to be deleted..." sleep 60 fi # Release Elastic IPs if they exist if [ -n "${EIP1_ALLOC_ID:-}" ]; then echo "Releasing Elastic IP 1: $EIP1_ALLOC_ID" aws ec2 release-address --allocation-id "$EIP1_ALLOC_ID" fi if [ -n "${EIP2_ALLOC_ID:-}" ]; then echo "Releasing Elastic IP 2: $EIP2_ALLOC_ID" aws ec2 release-address --allocation-id "$EIP2_ALLOC_ID" fi # Delete VPC endpoint if it exists if [ -n "${VPC_ENDPOINT_ID:-}" ]; then echo "Deleting VPC endpoint: $VPC_ENDPOINT_ID" aws ec2 delete-vpc-endpoints --vpc-endpoint-ids "$VPC_ENDPOINT_ID" fi # Delete security groups if they exist if [ -n "${APP_SG_ID:-}" ]; then echo "Deleting application security group: $APP_SG_ID" aws ec2 delete-security-group --group-id "$APP_SG_ID" fi if [ -n "${LB_SG_ID:-}" ]; then echo "Deleting load balancer security group: $LB_SG_ID" aws ec2 delete-security-group --group-id "$LB_SG_ID" fi # Detach and delete Internet Gateway if it exists if [ -n "${IGW_ID:-}" ] && [ -n "${VPC_ID:-}" ]; then echo "Detaching Internet Gateway: $IGW_ID from VPC: $VPC_ID" aws ec2 detach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" echo "Deleting Internet Gateway: $IGW_ID" aws ec2 delete-internet-gateway --internet-gateway-id "$IGW_ID" fi # Delete route table associations and route tables if they exist if [ -n "${PUBLIC_RT_ASSOC1_ID:-}" ]; then echo "Disassociating public route table from subnet 1: $PUBLIC_RT_ASSOC1_ID" aws ec2 disassociate-route-table --association-id "$PUBLIC_RT_ASSOC1_ID" fi if [ -n "${PUBLIC_RT_ASSOC2_ID:-}" ]; then echo "Disassociating public route table from subnet 2: $PUBLIC_RT_ASSOC2_ID" aws ec2 disassociate-route-table --association-id "$PUBLIC_RT_ASSOC2_ID" fi if [ -n "${PRIVATE_RT1_ASSOC_ID:-}" ]; then echo "Disassociating private route table 1: $PRIVATE_RT1_ASSOC_ID" aws ec2 disassociate-route-table --association-id "$PRIVATE_RT1_ASSOC_ID" fi if [ -n "${PRIVATE_RT2_ASSOC_ID:-}" ]; then echo "Disassociating private route table 2: $PRIVATE_RT2_ASSOC_ID" aws ec2 disassociate-route-table --association-id "$PRIVATE_RT2_ASSOC_ID" fi if [ -n "${PUBLIC_RT_ID:-}" ]; then echo "Deleting public route table: $PUBLIC_RT_ID" aws ec2 delete-route-table --route-table-id "$PUBLIC_RT_ID" fi if [ -n "${PRIVATE_RT1_ID:-}" ]; then echo "Deleting private route table 1: $PRIVATE_RT1_ID" aws ec2 delete-route-table --route-table-id "$PRIVATE_RT1_ID" fi if [ -n "${PRIVATE_RT2_ID:-}" ]; then echo "Deleting private route table 2: $PRIVATE_RT2_ID" aws ec2 delete-route-table --route-table-id "$PRIVATE_RT2_ID" fi # Delete subnets if they exist if [ -n "${PUBLIC_SUBNET1_ID:-}" ]; then echo "Deleting public subnet 1: $PUBLIC_SUBNET1_ID" aws ec2 delete-subnet --subnet-id "$PUBLIC_SUBNET1_ID" fi if [ -n "${PUBLIC_SUBNET2_ID:-}" ]; then echo "Deleting public subnet 2: $PUBLIC_SUBNET2_ID" aws ec2 delete-subnet --subnet-id "$PUBLIC_SUBNET2_ID" fi if [ -n "${PRIVATE_SUBNET1_ID:-}" ]; then echo "Deleting private subnet 1: $PRIVATE_SUBNET1_ID" aws ec2 delete-subnet --subnet-id "$PRIVATE_SUBNET1_ID" fi if [ -n "${PRIVATE_SUBNET2_ID:-}" ]; then echo "Deleting private subnet 2: $PRIVATE_SUBNET2_ID" aws ec2 delete-subnet --subnet-id "$PRIVATE_SUBNET2_ID" fi # Delete VPC if it exists if [ -n "${VPC_ID:-}" ]; then echo "Deleting VPC: $VPC_ID" aws ec2 delete-vpc --vpc-id "$VPC_ID" fi echo "Cleanup completed." } # Error handling function handle_error() { echo "ERROR: $1" echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to check command success check_command() { if [ $? -ne 0 ]; then handle_error "$1" fi } # Generate a random identifier for resource names RANDOM_ID=$(openssl rand -hex 4) echo "Using random identifier: $RANDOM_ID" # Create VPC echo "Creating VPC..." VPC_RESULT=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications "ResourceType=vpc,Tags=[{Key=Name,Value=ProductionVPC-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create VPC" VPC_ID=$(echo "$VPC_RESULT" | jq -r '.Vpc.VpcId') echo "VPC created with ID: $VPC_ID" # Get Availability Zones echo "Getting Availability Zones..." AZ_RESULT=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[0:2].ZoneName' --output text) check_command "Failed to get Availability Zones" # Convert space-separated output to array read -r -a AZS <<< "$AZ_RESULT" AZ1=${AZS[0]} AZ2=${AZS[1]} echo "Using Availability Zones: $AZ1 and $AZ2" # Create subnets echo "Creating subnets..." PUBLIC_SUBNET1_RESULT=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.0.0.0/24 --availability-zone "$AZ1" --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=PublicSubnet1-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create public subnet 1" PUBLIC_SUBNET1_ID=$(echo "$PUBLIC_SUBNET1_RESULT" | jq -r '.Subnet.SubnetId') PRIVATE_SUBNET1_RESULT=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.0.1.0/24 --availability-zone "$AZ1" --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=PrivateSubnet1-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create private subnet 1" PRIVATE_SUBNET1_ID=$(echo "$PRIVATE_SUBNET1_RESULT" | jq -r '.Subnet.SubnetId') PUBLIC_SUBNET2_RESULT=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.0.2.0/24 --availability-zone "$AZ2" --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=PublicSubnet2-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create public subnet 2" PUBLIC_SUBNET2_ID=$(echo "$PUBLIC_SUBNET2_RESULT" | jq -r '.Subnet.SubnetId') PRIVATE_SUBNET2_RESULT=$(aws ec2 create-subnet --vpc-id "$VPC_ID" --cidr-block 10.0.3.0/24 --availability-zone "$AZ2" --tag-specifications "ResourceType=subnet,Tags=[{Key=Name,Value=PrivateSubnet2-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create private subnet 2" PRIVATE_SUBNET2_ID=$(echo "$PRIVATE_SUBNET2_RESULT" | jq -r '.Subnet.SubnetId') echo "Subnets created with IDs:" echo "Public Subnet 1: $PUBLIC_SUBNET1_ID" echo "Private Subnet 1: $PRIVATE_SUBNET1_ID" echo "Public Subnet 2: $PUBLIC_SUBNET2_ID" echo "Private Subnet 2: $PRIVATE_SUBNET2_ID" # Create Internet Gateway echo "Creating Internet Gateway..." IGW_RESULT=$(aws ec2 create-internet-gateway --tag-specifications "ResourceType=internet-gateway,Tags=[{Key=Name,Value=ProductionIGW-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create Internet Gateway" IGW_ID=$(echo "$IGW_RESULT" | jq -r '.InternetGateway.InternetGatewayId') echo "Internet Gateway created with ID: $IGW_ID" # Attach Internet Gateway to VPC echo "Attaching Internet Gateway to VPC..." aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" check_command "Failed to attach Internet Gateway to VPC" # Create route tables echo "Creating route tables..." PUBLIC_RT_RESULT=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --tag-specifications "ResourceType=route-table,Tags=[{Key=Name,Value=PublicRouteTable-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create public route table" PUBLIC_RT_ID=$(echo "$PUBLIC_RT_RESULT" | jq -r '.RouteTable.RouteTableId') PRIVATE_RT1_RESULT=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --tag-specifications "ResourceType=route-table,Tags=[{Key=Name,Value=PrivateRouteTable1-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create private route table 1" PRIVATE_RT1_ID=$(echo "$PRIVATE_RT1_RESULT" | jq -r '.RouteTable.RouteTableId') PRIVATE_RT2_RESULT=$(aws ec2 create-route-table --vpc-id "$VPC_ID" --tag-specifications "ResourceType=route-table,Tags=[{Key=Name,Value=PrivateRouteTable2-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create private route table 2" PRIVATE_RT2_ID=$(echo "$PRIVATE_RT2_RESULT" | jq -r '.RouteTable.RouteTableId') echo "Route tables created with IDs:" echo "Public Route Table: $PUBLIC_RT_ID" echo "Private Route Table 1: $PRIVATE_RT1_ID" echo "Private Route Table 2: $PRIVATE_RT2_ID" # Add route to Internet Gateway in public route table echo "Adding route to Internet Gateway in public route table..." aws ec2 create-route --route-table-id "$PUBLIC_RT_ID" --destination-cidr-block 0.0.0.0/0 --gateway-id "$IGW_ID" check_command "Failed to add route to Internet Gateway" # Associate subnets with route tables echo "Associating subnets with route tables..." PUBLIC_RT_ASSOC1_RESULT=$(aws ec2 associate-route-table --route-table-id "$PUBLIC_RT_ID" --subnet-id "$PUBLIC_SUBNET1_ID") check_command "Failed to associate public subnet 1 with route table" PUBLIC_RT_ASSOC1_ID=$(echo "$PUBLIC_RT_ASSOC1_RESULT" | jq -r '.AssociationId') PUBLIC_RT_ASSOC2_RESULT=$(aws ec2 associate-route-table --route-table-id "$PUBLIC_RT_ID" --subnet-id "$PUBLIC_SUBNET2_ID") check_command "Failed to associate public subnet 2 with route table" PUBLIC_RT_ASSOC2_ID=$(echo "$PUBLIC_RT_ASSOC2_RESULT" | jq -r '.AssociationId') PRIVATE_RT1_ASSOC_RESULT=$(aws ec2 associate-route-table --route-table-id "$PRIVATE_RT1_ID" --subnet-id "$PRIVATE_SUBNET1_ID") check_command "Failed to associate private subnet 1 with route table" PRIVATE_RT1_ASSOC_ID=$(echo "$PRIVATE_RT1_ASSOC_RESULT" | jq -r '.AssociationId') PRIVATE_RT2_ASSOC_RESULT=$(aws ec2 associate-route-table --route-table-id "$PRIVATE_RT2_ID" --subnet-id "$PRIVATE_SUBNET2_ID") check_command "Failed to associate private subnet 2 with route table" PRIVATE_RT2_ASSOC_ID=$(echo "$PRIVATE_RT2_ASSOC_RESULT" | jq -r '.AssociationId') echo "Route table associations created with IDs:" echo "Public Subnet 1 Association: $PUBLIC_RT_ASSOC1_ID" echo "Public Subnet 2 Association: $PUBLIC_RT_ASSOC2_ID" echo "Private Subnet 1 Association: $PRIVATE_RT1_ASSOC_ID" echo "Private Subnet 2 Association: $PRIVATE_RT2_ASSOC_ID" # Create NAT Gateways echo "Creating NAT Gateways..." # Allocate Elastic IPs for NAT Gateways echo "Allocating Elastic IPs for NAT Gateways..." EIP1_RESULT=$(aws ec2 allocate-address --domain vpc --tag-specifications "ResourceType=elastic-ip,Tags=[{Key=Name,Value=NAT1-EIP-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to allocate Elastic IP 1" EIP1_ALLOC_ID=$(echo "$EIP1_RESULT" | jq -r '.AllocationId') EIP2_RESULT=$(aws ec2 allocate-address --domain vpc --tag-specifications "ResourceType=elastic-ip,Tags=[{Key=Name,Value=NAT2-EIP-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to allocate Elastic IP 2" EIP2_ALLOC_ID=$(echo "$EIP2_RESULT" | jq -r '.AllocationId') echo "Elastic IPs allocated with IDs:" echo "EIP 1 Allocation ID: $EIP1_ALLOC_ID" echo "EIP 2 Allocation ID: $EIP2_ALLOC_ID" # Create NAT Gateways echo "Creating NAT Gateway in public subnet 1..." NAT_GW1_RESULT=$(aws ec2 create-nat-gateway --subnet-id "$PUBLIC_SUBNET1_ID" --allocation-id "$EIP1_ALLOC_ID" --tag-specifications "ResourceType=natgateway,Tags=[{Key=Name,Value=NAT-Gateway1-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create NAT Gateway 1" NAT_GW1_ID=$(echo "$NAT_GW1_RESULT" | jq -r '.NatGateway.NatGatewayId') echo "Creating NAT Gateway in public subnet 2..." NAT_GW2_RESULT=$(aws ec2 create-nat-gateway --subnet-id "$PUBLIC_SUBNET2_ID" --allocation-id "$EIP2_ALLOC_ID" --tag-specifications "ResourceType=natgateway,Tags=[{Key=Name,Value=NAT-Gateway2-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create NAT Gateway 2" NAT_GW2_ID=$(echo "$NAT_GW2_RESULT" | jq -r '.NatGateway.NatGatewayId') echo "NAT Gateways created with IDs:" echo "NAT Gateway 1: $NAT_GW1_ID" echo "NAT Gateway 2: $NAT_GW2_ID" # Wait for NAT Gateways to be available echo "Waiting for NAT Gateways to be available..." aws ec2 wait nat-gateway-available --nat-gateway-ids "$NAT_GW1_ID" check_command "NAT Gateway 1 did not become available" aws ec2 wait nat-gateway-available --nat-gateway-ids "$NAT_GW2_ID" check_command "NAT Gateway 2 did not become available" echo "NAT Gateways are now available" # Add routes to NAT Gateways in private route tables echo "Adding routes to NAT Gateways in private route tables..." aws ec2 create-route --route-table-id "$PRIVATE_RT1_ID" --destination-cidr-block 0.0.0.0/0 --nat-gateway-id "$NAT_GW1_ID" check_command "Failed to add route to NAT Gateway 1" aws ec2 create-route --route-table-id "$PRIVATE_RT2_ID" --destination-cidr-block 0.0.0.0/0 --nat-gateway-id "$NAT_GW2_ID" check_command "Failed to add route to NAT Gateway 2" # Create VPC Endpoint for S3 echo "Creating VPC Endpoint for S3..." S3_PREFIX_LIST_ID=$(aws ec2 describe-prefix-lists --filters "Name=prefix-list-name,Values=com.amazonaws.$(aws configure get region).s3" --query 'PrefixLists[0].PrefixListId' --output text) check_command "Failed to get S3 prefix list ID" VPC_ENDPOINT_RESULT=$(aws ec2 create-vpc-endpoint --vpc-id "$VPC_ID" --service-name "com.amazonaws.$(aws configure get region).s3" --route-table-ids "$PRIVATE_RT1_ID" "$PRIVATE_RT2_ID" --tag-specifications "ResourceType=vpc-endpoint,Tags=[{Key=Name,Value=S3-Endpoint-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create VPC endpoint for S3" VPC_ENDPOINT_ID=$(echo "$VPC_ENDPOINT_RESULT" | jq -r '.VpcEndpoint.VpcEndpointId') echo "VPC Endpoint created with ID: $VPC_ENDPOINT_ID" # Create security groups echo "Creating security groups..." LB_SG_RESULT=$(aws ec2 create-security-group --group-name "LoadBalancerSG-$RANDOM_ID" --description "Security group for the load balancer" --vpc-id "$VPC_ID" --tag-specifications "ResourceType=security-group,Tags=[{Key=Name,Value=LoadBalancerSG-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create load balancer security group" LB_SG_ID=$(echo "$LB_SG_RESULT" | jq -r '.GroupId') # Allow inbound HTTP traffic from anywhere to the load balancer aws ec2 authorize-security-group-ingress --group-id "$LB_SG_ID" --protocol tcp --port 80 --cidr 0.0.0.0/0 check_command "Failed to authorize ingress to load balancer security group" APP_SG_RESULT=$(aws ec2 create-security-group --group-name "AppServerSG-$RANDOM_ID" --description "Security group for the application servers" --vpc-id "$VPC_ID" --tag-specifications "ResourceType=security-group,Tags=[{Key=Name,Value=AppServerSG-$RANDOM_ID},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]") check_command "Failed to create application server security group" APP_SG_ID=$(echo "$APP_SG_RESULT" | jq -r '.GroupId') # Allow inbound HTTP traffic from the load balancer security group to the application servers aws ec2 authorize-security-group-ingress --group-id "$APP_SG_ID" --protocol tcp --port 80 --source-group "$LB_SG_ID" check_command "Failed to authorize ingress to application server security group" echo "Security groups created with IDs:" echo "Load Balancer Security Group: $LB_SG_ID" echo "Application Server Security Group: $APP_SG_ID" # Create a launch template echo "Creating launch template..." # Create user data script with IMDSv2 support cat > user-data.sh << 'EOF' #!/bin/bash yum update -y yum install -y httpd systemctl start httpd systemctl enable httpd # Use IMDSv2 with session token TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600") AZ=$(curl -H "X-aws-ec2-metadata-token: $TOKEN" -s http://169.254.169.254/latest/meta-data/placement/availability-zone) HOSTNAME=$(hostname -f) echo "<h1>Hello from $HOSTNAME in $AZ</h1>" > /var/www/html/index.html EOF # Encode user data USER_DATA=$(base64 -w 0 user-data.sh) # Get latest Amazon Linux 2 AMI echo "Getting latest Amazon Linux 2 AMI..." AMI_ID=$(aws ec2 describe-images --owners amazon --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" "Name=state,Values=available" --query 'sort_by(Images, &CreationDate)[-1].ImageId' --output text) check_command "Failed to get latest Amazon Linux 2 AMI" echo "Using AMI: $AMI_ID" # Create launch template with IMDSv2 required LAUNCH_TEMPLATE_NAME="AppServerTemplate-$RANDOM_ID" echo "Creating launch template: $LAUNCH_TEMPLATE_NAME" aws ec2 create-launch-template \ --launch-template-name "$LAUNCH_TEMPLATE_NAME" \ --version-description "Initial version" \ --tag-specifications "ResourceType=launch-template,Tags=[{Key=Name,Value=$LAUNCH_TEMPLATE_NAME},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-private-servers-gs}]" \ --launch-template-data "{ \"NetworkInterfaces\": [{ \"DeviceIndex\": 0, \"Groups\": [\"$APP_SG_ID\"], \"DeleteOnTermination\": true }], \"ImageId\": \"$AMI_ID\", \"InstanceType\": \"t3.micro\", \"UserData\": \"$USER_DATA\", \"MetadataOptions\": { \"HttpTokens\": \"required\", \"HttpEndpoint\": \"enabled\" }, \"TagSpecifications\": [{ \"ResourceType\": \"instance\", \"Tags\": [{ \"Key\": \"Name\", \"Value\": \"AppServer-$RANDOM_ID\" }] }] }" check_command "Failed to create launch template" # Create target group echo "Creating target group..." TARGET_GROUP_NAME="AppTargetGroup-$RANDOM_ID" TARGET_GROUP_RESULT=$(aws elbv2 create-target-group \ --name "$TARGET_GROUP_NAME" \ --protocol HTTP \ --port 80 \ --vpc-id "$VPC_ID" \ --target-type instance \ --health-check-protocol HTTP \ --health-check-path "/" \ --health-check-port traffic-port \ --tags Key=project,Value=doc-smith Key=tutorial,Value=vpc-private-servers-gs) check_command "Failed to create target group" TARGET_GROUP_ARN=$(echo "$TARGET_GROUP_RESULT" | jq -r '.TargetGroups[0].TargetGroupArn') echo "Target group created with ARN: $TARGET_GROUP_ARN" # Create load balancer echo "Creating load balancer..." LB_NAME="AppLoadBalancer-$RANDOM_ID" LB_RESULT=$(aws elbv2 create-load-balancer \ --name "$LB_NAME" \ --subnets "$PUBLIC_SUBNET1_ID" "$PUBLIC_SUBNET2_ID" \ --security-groups "$LB_SG_ID" \ --tags "Key=Name,Value=$LB_NAME" Key=project,Value=doc-smith Key=tutorial,Value=vpc-private-servers-gs) check_command "Failed to create load balancer" LB_ARN=$(echo "$LB_RESULT" | jq -r '.LoadBalancers[0].LoadBalancerArn') echo "Load balancer created with ARN: $LB_ARN" # Wait for load balancer to be active echo "Waiting for load balancer to be active..." aws elbv2 wait load-balancer-available --load-balancer-arns "$LB_ARN" check_command "Load balancer did not become available" # Create listener echo "Creating listener..." LISTENER_RESULT=$(aws elbv2 create-listener \ --load-balancer-arn "$LB_ARN" \ --protocol HTTP \ --port 80 \ --default-actions "Type=forward,TargetGroupArn=$TARGET_GROUP_ARN") check_command "Failed to create listener" LISTENER_ARN=$(echo "$LISTENER_RESULT" | jq -r '.Listeners[0].ListenerArn') echo "Listener created with ARN: $LISTENER_ARN" # Create Auto Scaling group echo "Creating Auto Scaling group..." ASG_NAME="AppAutoScalingGroup-$RANDOM_ID" aws autoscaling create-auto-scaling-group \ --auto-scaling-group-name "$ASG_NAME" \ --launch-template "LaunchTemplateName=$LAUNCH_TEMPLATE_NAME,Version=\$Latest" \ --min-size 2 \ --max-size 4 \ --desired-capacity 2 \ --vpc-zone-identifier "$PRIVATE_SUBNET1_ID,$PRIVATE_SUBNET2_ID" \ --target-group-arns "$TARGET_GROUP_ARN" \ --health-check-type ELB \ --health-check-grace-period 300 \ --tags "Key=Name,Value=AppServer-$RANDOM_ID,PropagateAtLaunch=true" Key=project,Value=doc-smith,PropagateAtLaunch=true Key=tutorial,Value=vpc-private-servers-gs,PropagateAtLaunch=true check_command "Failed to create Auto Scaling group" echo "Auto Scaling group created with name: $ASG_NAME" # Get load balancer DNS name LB_DNS_NAME=$(aws elbv2 describe-load-balancers --load-balancer-arns "$LB_ARN" --query 'LoadBalancers[0].DNSName' --output text) check_command "Failed to get load balancer DNS name" echo "" echo "===========================================" echo "DEPLOYMENT COMPLETE" echo "===========================================" echo "VPC ID: $VPC_ID" echo "Public Subnet 1: $PUBLIC_SUBNET1_ID (AZ: $AZ1)" echo "Private Subnet 1: $PRIVATE_SUBNET1_ID (AZ: $AZ1)" echo "Public Subnet 2: $PUBLIC_SUBNET2_ID (AZ: $AZ2)" echo "Private Subnet 2: $PRIVATE_SUBNET2_ID (AZ: $AZ2)" echo "NAT Gateway 1: $NAT_GW1_ID" echo "NAT Gateway 2: $NAT_GW2_ID" echo "Load Balancer: $LB_NAME" echo "Auto Scaling Group: $ASG_NAME" echo "" echo "Your application will be available at: http://$LB_DNS_NAME" echo "It may take a few minutes for the instances to launch and pass health checks." echo "" # Add health check monitoring echo "===========================================" echo "MONITORING INSTANCE HEALTH AND LOAD BALANCER" echo "===========================================" echo "Waiting for instances to launch and pass health checks..." echo "This may take 3-5 minutes. Checking every 30 seconds..." # Monitor instance health and load balancer accessibility MAX_ATTEMPTS=10 ATTEMPT=1 HEALTHY_INSTANCES=0 while [ $ATTEMPT -le $MAX_ATTEMPTS ] && [ $HEALTHY_INSTANCES -lt 2 ]; do echo "Check attempt $ATTEMPT of $MAX_ATTEMPTS..." # Check Auto Scaling group instances echo "Checking Auto Scaling group instances..." ASG_INSTANCES=$(aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names "$ASG_NAME" --query 'AutoScalingGroups[0].Instances[*].[InstanceId,HealthStatus]' --output json) echo "ASG Instances status:" echo "$ASG_INSTANCES" | jq -r '.[] | "Instance: \(.[0]), Health: \(.[1])"' # Check target group health echo "Checking target group health..." TARGET_HEALTH=$(aws elbv2 describe-target-health --target-group-arn "$TARGET_GROUP_ARN" --output json) echo "Target health status:" echo "$TARGET_HEALTH" | jq -r '.TargetHealthDescriptions[] | "Instance: \(.Target.Id), State: \(.TargetHealth.State), Reason: \(.TargetHealth.Reason // "N/A"), Description: \(.TargetHealth.Description // "N/A")"' # Count healthy instances HEALTHY_INSTANCES=$(echo "$TARGET_HEALTH" | jq -r '[.TargetHealthDescriptions[] | select(.TargetHealth.State=="healthy")] | length') echo "Number of healthy instances: $HEALTHY_INSTANCES of 2 expected" # Check if we have healthy instances if [ $HEALTHY_INSTANCES -ge 2 ]; then echo "All instances are healthy!" # Test load balancer accessibility echo "Testing load balancer accessibility..." HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "http://$LB_DNS_NAME") if [ "$HTTP_STATUS" = "200" ]; then echo "Load balancer is accessible! HTTP Status: $HTTP_STATUS" echo "You can access your application at: http://$LB_DNS_NAME" # Try to get the content to verify IMDSv2 is working echo "Fetching content to verify IMDSv2 functionality..." CONTENT=$(curl -s "http://$LB_DNS_NAME") echo "Response from server:" echo "$CONTENT" # Check if the content contains the expected pattern if [[ "$CONTENT" == *"Hello from"* && "$CONTENT" == *"in"* ]]; then echo "IMDSv2 is working correctly! The instance was able to access metadata using the token-based approach." else echo "Warning: Content doesn't match expected pattern. IMDSv2 functionality could not be verified." fi break else echo "Load balancer returned HTTP status: $HTTP_STATUS" echo "Will try again in 30 seconds..." fi else echo "Waiting for instances to become healthy..." echo "Will check again in 30 seconds..." fi ATTEMPT=$((ATTEMPT+1)) if [ $ATTEMPT -le $MAX_ATTEMPTS ]; then sleep 30 fi done if [ $HEALTHY_INSTANCES -lt 2 ]; then echo "Warning: Not all instances are healthy after maximum attempts." echo "You may need to wait longer or check for configuration issues." fi echo "To test your application, run:" echo "curl http://$LB_DNS_NAME" echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then cleanup_resources echo "All resources have been deleted." else echo "Resources will not be deleted. You can manually delete them later." echo "To delete resources, run this script again and choose to clean up." fi

The following code example shows how to:

  • Enable Amazon EBS encryption by default

  • Create an EBS snapshot

  • Create and initialize a volume from a snapshot

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # Script for EBS operations: encryption, snapshots, and volume initialization # This script demonstrates: # 1. Enabling EBS encryption by default # 2. Creating an EBS snapshot # 3. Creating a volume from a snapshot # Cost optimizations: # - Reduced volume size from 1 GiB to 100 MiB for testing # - Changed volume type to gp3 with cost-optimized IOPS/throughput # - Added early cleanup to minimize storage duration # - Removed unnecessary API calls for KMS key retrieval set -euo pipefail # Security: Restrict file permissions for log files umask 0077 # Setup logging with secure permissions LOG_FILE="ebs-operations-v4.log" exec > >(tee -a "$LOG_FILE") 2>&1 echo "Starting EBS operations script at $(date)" echo "All operations will be logged to $LOG_FILE" # Function to check command status check_status() { local exit_code=$? if [ $exit_code -ne 0 ]; then echo "ERROR: $1 failed with exit code $exit_code. Exiting." cleanup_resources exit 1 fi } # Function to cleanup resources with retry logic cleanup_resources() { echo "Attempting to clean up resources..." local retry_count=0 local max_retries=3 if [ -n "${NEW_VOLUME_ID:-}" ]; then echo "Deleting new volume $NEW_VOLUME_ID..." for ((retry_count=0; retry_count<max_retries; retry_count++)); do if aws ec2 delete-volume --volume-id "$NEW_VOLUME_ID" --region "$AWS_REGION" 2>/dev/null; then echo "Successfully deleted new volume $NEW_VOLUME_ID" break else if [ $retry_count -lt $((max_retries-1)) ]; then echo "Retry $((retry_count+1))/$max_retries for deleting $NEW_VOLUME_ID..." sleep 2 else echo "WARNING: Could not delete new volume $NEW_VOLUME_ID after $max_retries attempts" fi fi done fi if [ -n "${VOLUME_ID:-}" ]; then echo "Deleting original volume $VOLUME_ID..." for ((retry_count=0; retry_count<max_retries; retry_count++)); do if aws ec2 delete-volume --volume-id "$VOLUME_ID" --region "$AWS_REGION" 2>/dev/null; then echo "Successfully deleted original volume $VOLUME_ID" break else if [ $retry_count -lt $((max_retries-1)) ]; then echo "Retry $((retry_count+1))/$max_retries for deleting $VOLUME_ID..." sleep 2 else echo "WARNING: Could not delete original volume $VOLUME_ID after $max_retries attempts" fi fi done fi if [ -n "${SNAPSHOT_ID:-}" ]; then echo "Deleting snapshot $SNAPSHOT_ID..." for ((retry_count=0; retry_count<max_retries; retry_count++)); do if aws ec2 delete-snapshot --snapshot-id "$SNAPSHOT_ID" --region "$AWS_REGION" 2>/dev/null; then echo "Successfully deleted snapshot $SNAPSHOT_ID" break else if [ $retry_count -lt $((max_retries-1)) ]; then echo "Retry $((retry_count+1))/$max_retries for deleting $SNAPSHOT_ID..." sleep 2 else echo "WARNING: Could not delete snapshot $SNAPSHOT_ID after $max_retries attempts" fi fi done fi if [ "${ENCRYPTION_MODIFIED:-false}" = true ]; then echo "Restoring original encryption setting..." if [ "${ORIGINAL_ENCRYPTION:-}" = "False" ]; then aws ec2 disable-ebs-encryption-by-default --region "$AWS_REGION" 2>/dev/null || echo "WARNING: Could not restore encryption setting" fi fi echo "Cleanup completed." } # Set trap for cleanup on exit trap cleanup_resources EXIT # Track created resources VOLUME_ID="" NEW_VOLUME_ID="" SNAPSHOT_ID="" ENCRYPTION_MODIFIED=false ORIGINAL_ENCRYPTION="" # Input validation function validate_aws_cli() { if ! command -v aws &> /dev/null; then echo "ERROR: AWS CLI is not installed or not in PATH" exit 1 fi # Verify AWS credentials are configured if ! aws sts get-caller-identity &> /dev/null; then echo "ERROR: AWS credentials are not properly configured" exit 1 fi } validate_aws_cli # Security: Validate AWS region format validate_region() { local region="$1" if [[ ! "$region" =~ ^[a-z]{2}-[a-z]+-[0-9]{1}$ ]]; then echo "ERROR: Invalid AWS region format: $region" exit 1 fi } # Get the current AWS region AWS_REGION="${AWS_REGION:-$(aws configure get region)}" if [ -z "$AWS_REGION" ]; then AWS_REGION="us-east-1" echo "No region found in AWS config. Using default: $AWS_REGION" fi validate_region "$AWS_REGION" echo "Using AWS region: $AWS_REGION" # Security: Validate volume ID format before use validate_volume_id() { local volume_id="$1" if [[ ! "$volume_id" =~ ^vol-[a-z0-9]{17}$ ]]; then echo "ERROR: Invalid volume ID format: $volume_id" exit 1 fi } # Security: Validate snapshot ID format before use validate_snapshot_id() { local snapshot_id="$1" if [[ ! "$snapshot_id" =~ ^snap-[a-z0-9]{17}$ ]]; then echo "ERROR: Invalid snapshot ID format: $snapshot_id" exit 1 fi } # Get availability zones in the region with caching AVAILABILITY_ZONE=$(aws ec2 describe-availability-zones --region "$AWS_REGION" --query 'AvailabilityZones[0].ZoneName' --output text) check_status "Getting availability zone" # Security: Validate AZ format if [[ ! "$AVAILABILITY_ZONE" =~ ^[a-z]{2}-[a-z]+-[0-9]{1}[a-z]$ ]]; then echo "ERROR: Invalid availability zone format: $AVAILABILITY_ZONE" exit 1 fi echo "Using availability zone: $AVAILABILITY_ZONE" # Step 1: Check and enable EBS encryption by default echo "Step 1: Checking current EBS encryption by default setting..." ORIGINAL_ENCRYPTION=$(aws ec2 get-ebs-encryption-by-default --region "$AWS_REGION" --query 'EbsEncryptionByDefault' --output text) check_status "Checking encryption status" echo "Current encryption by default setting: $ORIGINAL_ENCRYPTION" if [ "$ORIGINAL_ENCRYPTION" = "False" ]; then echo "Enabling EBS encryption by default..." aws ec2 enable-ebs-encryption-by-default --region "$AWS_REGION" check_status "Enabling encryption by default" ENCRYPTION_MODIFIED=true echo "Updated encryption by default setting: True" else echo "EBS encryption by default is already enabled." fi # Step 2: Create a test volume for snapshot with minimal size for cost optimization echo "Step 2: Creating a test volume (1 GiB for testing)..." VOLUME_ID=$(aws ec2 create-volume --region "$AWS_REGION" --availability-zone "$AVAILABILITY_ZONE" --size 1 --volume-type gp3 --iops 3000 --throughput 125 --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=ebs-tutorial-volume},{Key=ManagedBy,Value=ebs-intermediate-script},{Key=project,Value=doc-smith},{Key=tutorial,Value=ebs-intermediate}]' --query 'VolumeId' --output text) check_status "Creating test volume" # Security: Validate volume ID validate_volume_id "$VOLUME_ID" echo "Created test volume: $VOLUME_ID" # Wait for volume to become available with timeout echo "Waiting for volume to become available..." timeout 300 aws ec2 wait volume-available --region "$AWS_REGION" --volume-ids "$VOLUME_ID" || { echo "ERROR: Volume did not become available within timeout" exit 1 } check_status "Waiting for volume" # Step 3: Create a snapshot of the volume echo "Step 3: Creating snapshot of the volume..." SNAPSHOT_ID=$(aws ec2 create-snapshot --region "$AWS_REGION" --volume-id "$VOLUME_ID" --description "Snapshot for EBS tutorial - $(date +%Y-%m-%d)" --tag-specifications 'ResourceType=snapshot,Tags=[{Key=Name,Value=ebs-tutorial-snapshot},{Key=ManagedBy,Value=ebs-intermediate-script},{Key=project,Value=doc-smith},{Key=tutorial,Value=ebs-intermediate}]' --query 'SnapshotId' --output text) check_status "Creating snapshot" # Security: Validate snapshot ID validate_snapshot_id "$SNAPSHOT_ID" echo "Created snapshot: $SNAPSHOT_ID" # Wait for snapshot to complete with progress indication and timeout echo "Waiting for snapshot to complete (this may take several minutes)..." timeout 1800 aws ec2 wait snapshot-completed --region "$AWS_REGION" --snapshot-ids "$SNAPSHOT_ID" || { echo "ERROR: Snapshot did not complete within timeout" exit 1 } check_status "Waiting for snapshot" echo "Snapshot completed." # Step 4: Create a new volume from the snapshot echo "Step 4: Creating a new volume from the snapshot..." NEW_VOLUME_ID=$(aws ec2 create-volume --region "$AWS_REGION" --snapshot-id "$SNAPSHOT_ID" --availability-zone "$AVAILABILITY_ZONE" --volume-type gp3 --iops 3000 --throughput 125 --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=ebs-tutorial-volume-from-snapshot},{Key=ManagedBy,Value=ebs-intermediate-script},{Key=project,Value=doc-smith},{Key=tutorial,Value=ebs-intermediate}]' --query 'VolumeId' --output text) check_status "Creating new volume from snapshot" # Security: Validate new volume ID validate_volume_id "$NEW_VOLUME_ID" echo "Created new volume from snapshot: $NEW_VOLUME_ID" # Wait for new volume to become available with timeout echo "Waiting for new volume to become available..." timeout 300 aws ec2 wait volume-available --region "$AWS_REGION" --volume-ids "$NEW_VOLUME_ID" || { echo "ERROR: New volume did not become available within timeout" exit 1 } check_status "Waiting for new volume" # Display created resources echo "" echo "===========================================" echo "RESOURCES CREATED" echo "===========================================" echo "Original Volume: $VOLUME_ID" echo "Snapshot: $SNAPSHOT_ID" echo "New Volume: $NEW_VOLUME_ID" echo "===========================================" # Auto-confirm cleanup echo "" echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Starting cleanup process to minimize storage costs..." # Delete the new volume immediately to reduce storage duration echo "Deleting new volume $NEW_VOLUME_ID..." aws ec2 delete-volume --region "$AWS_REGION" --volume-id "$NEW_VOLUME_ID" check_status "Deleting new volume" # Delete the original volume echo "Deleting original volume $VOLUME_ID..." aws ec2 delete-volume --region "$AWS_REGION" --volume-id "$VOLUME_ID" check_status "Deleting original volume" # Delete the snapshot echo "Deleting snapshot $SNAPSHOT_ID..." aws ec2 delete-snapshot --region "$AWS_REGION" --snapshot-id "$SNAPSHOT_ID" check_status "Deleting snapshot" # Restore original encryption setting if modified if [ "${ENCRYPTION_MODIFIED:-false}" = true ]; then echo "Restoring original encryption setting..." if [ "${ORIGINAL_ENCRYPTION:-}" = "False" ]; then aws ec2 disable-ebs-encryption-by-default --region "$AWS_REGION" check_status "Disabling encryption by default" fi fi echo "Cleanup completed successfully." echo "Script completed at $(date)"

The following code example shows how to:

  • Create VPCs for peering

  • Create a VPC peering connection

  • Update route tables

  • Verify the VPC peering connection

  • Clean up resources

AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # VPC Peering Connection Script - Version 6 (Security Enhanced) # This script establishes a VPC peering connection between two VPCs, # creates subnets if needed, and configures the necessary route tables. # It will use existing VPCs if available, or create new ones if needed. set -euo pipefail # Security: Set strict umask umask 0077 # Initialize log file with restricted permissions LOG_FILE="./vpc-peering-script-v6.log" touch "$LOG_FILE" chmod 0600 "$LOG_FILE" echo "Starting VPC Peering script at $(date)" > "$LOG_FILE" # Configuration declare -r AWS_REGION="${AWS_REGION:-us-east-1}" declare -r MAX_RETRIES=3 declare -r RETRY_DELAY=5 # Validate script is run from secure location if [[ "$LOG_FILE" != /* ]] && [[ "$LOG_FILE" != ./* ]]; then echo "ERROR: Log file path must be absolute or relative starting with ./" >&2 exit 1 fi # Function to sanitize variable for safe command execution sanitize_var() { local var="$1" if [[ ! "$var" =~ ^[a-zA-Z0-9_/.-]+$ ]]; then echo "ERROR: Invalid characters in variable: $var" | tee -a "$LOG_FILE" return 1 fi echo "$var" return 0 } # Function to escape string for safe use in commands escape_string() { local string="$1" printf '%s\n' "$string" | sed -e 's/[\/&]/\\&/g' } # Function to log commands and their output securely log_cmd() { local cmd="$1" # Validate command doesn't contain suspicious patterns if [[ "$cmd" =~ (\$\(|\`|;.*rm|;.*mv|;.*cp) ]]; then echo "ERROR: Suspicious command pattern detected" | tee -a "$LOG_FILE" return 1 fi echo "$(date): COMMAND: $cmd" >> "$LOG_FILE" eval "$cmd" 2>&1 | tee -a "$LOG_FILE" return "${PIPESTATUS[0]}" } # Function to check for errors check_error() { local exit_code="$1" local error_msg="${2:-Command failed}" if [ "$exit_code" -ne 0 ]; then echo "ERROR: $error_msg (exit code: $exit_code)" | tee -a "$LOG_FILE" echo "See $LOG_FILE for details" cleanup_on_error exit "$exit_code" fi } # Function to validate AWS CLI is available and configured validate_aws_cli() { if ! command -v aws &> /dev/null; then echo "ERROR: AWS CLI is not installed" | tee -a "$LOG_FILE" exit 1 fi # Check AWS CLI version local aws_version aws_version=$(aws --version 2>&1 | cut -d' ' -f1 | cut -d'/' -f2) echo "AWS CLI version: $aws_version" >> "$LOG_FILE" if ! aws sts get-caller-identity --region "$AWS_REGION" &>/dev/null; then echo "ERROR: AWS CLI is not properly configured or credentials are invalid" | tee -a "$LOG_FILE" exit 1 fi # Validate caller identity local account_id account_id=$(aws sts get-caller-identity --query 'Account' --output text 2>/dev/null) if [[ ! "$account_id" =~ ^[0-9]{12}$ ]]; then echo "ERROR: Invalid AWS account ID" | tee -a "$LOG_FILE" exit 1 fi echo "Authenticated as AWS Account: $account_id" | tee -a "$LOG_FILE" } # Function to validate CIDR blocks validate_cidr() { local cidr="$1" if ! [[ "$cidr" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/[0-9]{1,2}$ ]]; then echo "ERROR: Invalid CIDR block format: $cidr" | tee -a "$LOG_FILE" return 1 fi # Additional validation for IP octets local ip_part="${cidr%/*}" local mask_part="${cidr#*/}" IFS='.' read -r -a octets <<< "$ip_part" for octet in "${octets[@]}"; do if (( octet > 255 )); then echo "ERROR: Invalid octet value in CIDR: $cidr" | tee -a "$LOG_FILE" return 1 fi done if (( mask_part > 32 || mask_part < 0 )); then echo "ERROR: Invalid CIDR mask value: $mask_part" | tee -a "$LOG_FILE" return 1 fi return 0 } # Function to clean up resources on error cleanup_on_error() { echo "Error encountered. Attempting to clean up resources..." | tee -a "$LOG_FILE" # List created resources echo "Resources created:" | tee -a "$LOG_FILE" for resource in "${CREATED_RESOURCES[@]:-}"; do echo "- $resource" | tee -a "$LOG_FILE" done # Clean up in reverse order with retry logic for ((i=${#CLEANUP_COMMANDS[@]}-1; i>=0; i--)); do echo "Executing cleanup: ${CLEANUP_COMMANDS[$i]}" >> "$LOG_FILE" local retry_count=0 while [ $retry_count -lt $MAX_RETRIES ]; do if eval "${CLEANUP_COMMANDS[$i]}" 2>&1 >> "$LOG_FILE"; then break else retry_count=$((retry_count + 1)) if [ $retry_count -lt $MAX_RETRIES ]; then echo "Cleanup command failed, retrying in ${RETRY_DELAY}s..." >> "$LOG_FILE" sleep "$RETRY_DELAY" fi fi done done } # Array to store created resources and cleanup commands declare -a CREATED_RESOURCES=() declare -a CLEANUP_COMMANDS=() # Trap errors and cleanup trap cleanup_on_error EXIT echo "Setting up VPC peering connection..." # Validate AWS CLI validate_aws_cli # Check for existing VPCs echo "Checking for existing VPCs..." EXISTING_VPCS=$(aws ec2 describe-vpcs --region "$AWS_REGION" --query 'Vpcs[?State==`available`].[VpcId,CidrBlock]' --output text 2>/dev/null || echo "") if [ -z "$EXISTING_VPCS" ]; then echo "No existing VPCs found. Creating new VPCs..." CREATE_VPCS=true else echo "Found existing VPCs:" echo "$EXISTING_VPCS" echo "" echo "Using existing VPCs..." CREATE_VPCS=false # Get the first two available VPCs VPC1_INFO=$(echo "$EXISTING_VPCS" | head -n 1) VPC2_INFO=$(echo "$EXISTING_VPCS" | head -n 2 | tail -n 1) if [ -z "$VPC2_INFO" ]; then echo "Only one VPC found. Creating a second VPC..." VPC1_ID=$(echo "$VPC1_INFO" | awk '{print $1}') VPC1_CIDR=$(echo "$VPC1_INFO" | awk '{print $2}') # Sanitize extracted values VPC1_ID=$(sanitize_var "$VPC1_ID") || check_error 1 "Invalid VPC1_ID format" VPC1_CIDR=$(sanitize_var "$VPC1_CIDR") || check_error 1 "Invalid VPC1_CIDR format" validate_cidr "$VPC1_CIDR" || check_error 1 "Invalid VPC1 CIDR" CREATE_VPC2_ONLY=true else VPC1_ID=$(echo "$VPC1_INFO" | awk '{print $1}') VPC1_CIDR=$(echo "$VPC1_INFO" | awk '{print $2}') VPC2_ID=$(echo "$VPC2_INFO" | awk '{print $1}') VPC2_CIDR=$(echo "$VPC2_INFO" | awk '{print $2}') # Sanitize extracted values VPC1_ID=$(sanitize_var "$VPC1_ID") || check_error 1 "Invalid VPC1_ID format" VPC1_CIDR=$(sanitize_var "$VPC1_CIDR") || check_error 1 "Invalid VPC1_CIDR format" VPC2_ID=$(sanitize_var "$VPC2_ID") || check_error 1 "Invalid VPC2_ID format" VPC2_CIDR=$(sanitize_var "$VPC2_CIDR") || check_error 1 "Invalid VPC2_CIDR format" validate_cidr "$VPC1_CIDR" || check_error 1 "Invalid VPC1 CIDR" validate_cidr "$VPC2_CIDR" || check_error 1 "Invalid VPC2 CIDR" CREATE_VPC2_ONLY=false fi fi # Create VPCs if needed if [ "$CREATE_VPCS" = true ]; then echo "Creating VPC1..." VPC1_ID=$(log_cmd "aws ec2 create-vpc --region '$AWS_REGION' --cidr-block 10.1.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=VPC1-Peering-Demo},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'Vpc.VpcId' --output text") check_error $? "Failed to create VPC1" VPC1_ID=$(sanitize_var "$VPC1_ID") || check_error 1 "Invalid VPC1_ID returned" VPC1_CIDR="10.1.0.0/16" CREATED_RESOURCES+=("VPC1: $VPC1_ID") CLEANUP_COMMANDS+=("aws ec2 delete-vpc --region '$AWS_REGION' --vpc-id '$VPC1_ID'") echo "VPC1 created with ID: $VPC1_ID" echo "Creating VPC2..." VPC2_ID=$(log_cmd "aws ec2 create-vpc --region '$AWS_REGION' --cidr-block 10.2.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=VPC2-Peering-Demo},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'Vpc.VpcId' --output text") check_error $? "Failed to create VPC2" VPC2_ID=$(sanitize_var "$VPC2_ID") || check_error 1 "Invalid VPC2_ID returned" VPC2_CIDR="10.2.0.0/16" CREATED_RESOURCES+=("VPC2: $VPC2_ID") CLEANUP_COMMANDS+=("aws ec2 delete-vpc --region '$AWS_REGION' --vpc-id '$VPC2_ID'") echo "VPC2 created with ID: $VPC2_ID" # Wait for VPCs to be available echo "Waiting for VPCs to be available..." log_cmd "aws ec2 wait vpc-available --region '$AWS_REGION' --vpc-ids '$VPC1_ID' '$VPC2_ID'" check_error $? "Timeout waiting for VPCs to become available" elif [ "$CREATE_VPC2_ONLY" = true ]; then echo "Creating VPC2..." VPC2_ID=$(log_cmd "aws ec2 create-vpc --region '$AWS_REGION' --cidr-block 10.2.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=VPC2-Peering-Demo},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'Vpc.VpcId' --output text") check_error $? "Failed to create VPC2" VPC2_ID=$(sanitize_var "$VPC2_ID") || check_error 1 "Invalid VPC2_ID returned" VPC2_CIDR="10.2.0.0/16" CREATED_RESOURCES+=("VPC2: $VPC2_ID") CLEANUP_COMMANDS+=("aws ec2 delete-vpc --region '$AWS_REGION' --vpc-id '$VPC2_ID'") echo "VPC2 created with ID: $VPC2_ID" # Wait for VPC2 to be available echo "Waiting for VPC2 to be available..." log_cmd "aws ec2 wait vpc-available --region '$AWS_REGION' --vpc-ids '$VPC2_ID'" check_error $? "Timeout waiting for VPC2 to become available" fi echo "Using the following VPCs:" echo "VPC1: $VPC1_ID ($VPC1_CIDR)" echo "VPC2: $VPC2_ID ($VPC2_CIDR)" # Verify the VPCs exist and are available echo "Verifying VPCs..." log_cmd "aws ec2 describe-vpcs --region '$AWS_REGION' --vpc-ids '$VPC1_ID' '$VPC2_ID' --query 'Vpcs[*].[VpcId,State,CidrBlock]' --output table" check_error $? "Failed to verify VPCs" # Determine subnet CIDR blocks based on VPC CIDR blocks VPC1_SUBNET_CIDR=$(echo "$VPC1_CIDR" | sed 's/0\.0\/16/1.0\/24/') VPC2_SUBNET_CIDR=$(echo "$VPC2_CIDR" | sed 's/0\.0\/16/1.0\/24/') # Sanitize subnet CIDR blocks VPC1_SUBNET_CIDR=$(sanitize_var "$VPC1_SUBNET_CIDR") || check_error 1 "Invalid VPC1_SUBNET_CIDR format" VPC2_SUBNET_CIDR=$(sanitize_var "$VPC2_SUBNET_CIDR") || check_error 1 "Invalid VPC2_SUBNET_CIDR format" validate_cidr "$VPC1_SUBNET_CIDR" || check_error 1 "Invalid subnet CIDR for VPC1" validate_cidr "$VPC2_SUBNET_CIDR" || check_error 1 "Invalid subnet CIDR for VPC2" # Create subnets in both VPCs echo "Creating subnet in VPC1..." SUBNET1_ID=$(log_cmd "aws ec2 create-subnet --region '$AWS_REGION' --vpc-id '$VPC1_ID' --cidr-block '$VPC1_SUBNET_CIDR' --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=VPC1-Peering-Subnet},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'Subnet.SubnetId' --output text") check_error $? "Failed to create subnet in VPC1" SUBNET1_ID=$(sanitize_var "$SUBNET1_ID") || check_error 1 "Invalid SUBNET1_ID returned" CREATED_RESOURCES+=("Subnet in VPC1: $SUBNET1_ID") CLEANUP_COMMANDS+=("aws ec2 delete-subnet --region '$AWS_REGION' --subnet-id '$SUBNET1_ID'") echo "Subnet created in VPC1 with ID: $SUBNET1_ID (CIDR: $VPC1_SUBNET_CIDR)" echo "Creating subnet in VPC2..." SUBNET2_ID=$(log_cmd "aws ec2 create-subnet --region '$AWS_REGION' --vpc-id '$VPC2_ID' --cidr-block '$VPC2_SUBNET_CIDR' --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=VPC2-Peering-Subnet},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'Subnet.SubnetId' --output text") check_error $? "Failed to create subnet in VPC2" SUBNET2_ID=$(sanitize_var "$SUBNET2_ID") || check_error 1 "Invalid SUBNET2_ID returned" CREATED_RESOURCES+=("Subnet in VPC2: $SUBNET2_ID") CLEANUP_COMMANDS+=("aws ec2 delete-subnet --region '$AWS_REGION' --subnet-id '$SUBNET2_ID'") echo "Subnet created in VPC2 with ID: $SUBNET2_ID (CIDR: $VPC2_SUBNET_CIDR)" # Create a VPC peering connection echo "Creating VPC peering connection..." PEERING_ID=$(log_cmd "aws ec2 create-vpc-peering-connection --region '$AWS_REGION' --vpc-id '$VPC1_ID' --peer-vpc-id '$VPC2_ID' --tag-specifications 'ResourceType=vpc-peering-connection,Tags=[{Key=Name,Value=VPC1-VPC2-Peering},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'VpcPeeringConnection.VpcPeeringConnectionId' --output text") check_error $? "Failed to create VPC peering connection" PEERING_ID=$(sanitize_var "$PEERING_ID") || check_error 1 "Invalid PEERING_ID returned" CREATED_RESOURCES+=("VPC Peering Connection: $PEERING_ID") CLEANUP_COMMANDS+=("aws ec2 delete-vpc-peering-connection --region '$AWS_REGION' --vpc-peering-connection-id '$PEERING_ID'") echo "VPC Peering Connection created with ID: $PEERING_ID" # Accept the VPC peering connection echo "Accepting VPC peering connection..." log_cmd "aws ec2 accept-vpc-peering-connection --region '$AWS_REGION' --vpc-peering-connection-id '$PEERING_ID'" check_error $? "Failed to accept VPC peering connection" echo "VPC Peering Connection accepted" # Wait for the peering connection to become active echo "Waiting for peering connection to become active..." log_cmd "aws ec2 wait vpc-peering-connection-exists --region '$AWS_REGION' --vpc-peering-connection-ids '$PEERING_ID'" check_error $? "Timeout waiting for peering connection to become active" # Create a route table for VPC1 echo "Creating route table for VPC1..." RTB1_ID=$(log_cmd "aws ec2 create-route-table --region '$AWS_REGION' --vpc-id '$VPC1_ID' --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=VPC1-RouteTable},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'RouteTable.RouteTableId' --output text") check_error $? "Failed to create route table for VPC1" RTB1_ID=$(sanitize_var "$RTB1_ID") || check_error 1 "Invalid RTB1_ID returned" CREATED_RESOURCES+=("Route Table for VPC1: $RTB1_ID") CLEANUP_COMMANDS+=("aws ec2 delete-route-table --region '$AWS_REGION' --route-table-id '$RTB1_ID'") echo "Route table created for VPC1 with ID: $RTB1_ID" # Create a route from VPC1 to VPC2 echo "Creating route from VPC1 to VPC2..." log_cmd "aws ec2 create-route --region '$AWS_REGION' --route-table-id '$RTB1_ID' --destination-cidr-block '$VPC2_CIDR' --vpc-peering-connection-id '$PEERING_ID'" check_error $? "Failed to create route from VPC1 to VPC2" echo "Route created from VPC1 to VPC2" # Associate the route table with the subnet in VPC1 echo "Associating route table with subnet in VPC1..." RTB1_ASSOC_ID=$(log_cmd "aws ec2 associate-route-table --region '$AWS_REGION' --route-table-id '$RTB1_ID' --subnet-id '$SUBNET1_ID' --query 'AssociationId' --output text") check_error $? "Failed to associate route table with subnet in VPC1" RTB1_ASSOC_ID=$(sanitize_var "$RTB1_ASSOC_ID") || check_error 1 "Invalid RTB1_ASSOC_ID returned" CREATED_RESOURCES+=("Route Table Association for VPC1: $RTB1_ASSOC_ID") CLEANUP_COMMANDS+=("aws ec2 disassociate-route-table --region '$AWS_REGION' --association-id '$RTB1_ASSOC_ID'") echo "Route table associated with subnet in VPC1" # Create a route table for VPC2 echo "Creating route table for VPC2..." RTB2_ID=$(log_cmd "aws ec2 create-route-table --region '$AWS_REGION' --vpc-id '$VPC2_ID' --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=VPC2-RouteTable},{Key=project,Value=doc-smith},{Key=tutorial,Value=vpc-peering}]' --query 'RouteTable.RouteTableId' --output text") check_error $? "Failed to create route table for VPC2" RTB2_ID=$(sanitize_var "$RTB2_ID") || check_error 1 "Invalid RTB2_ID returned" CREATED_RESOURCES+=("Route Table for VPC2: $RTB2_ID") CLEANUP_COMMANDS+=("aws ec2 delete-route-table --region '$AWS_REGION' --route-table-id '$RTB2_ID'") echo "Route table created for VPC2 with ID: $RTB2_ID" # Create a route from VPC2 to VPC1 echo "Creating route from VPC2 to VPC1..." log_cmd "aws ec2 create-route --region '$AWS_REGION' --route-table-id '$RTB2_ID' --destination-cidr-block '$VPC1_CIDR' --vpc-peering-connection-id '$PEERING_ID'" check_error $? "Failed to create route from VPC2 to VPC1" echo "Route created from VPC2 to VPC1" # Associate the route table with the subnet in VPC2 echo "Associating route table with subnet in VPC2..." RTB2_ASSOC_ID=$(log_cmd "aws ec2 associate-route-table --region '$AWS_REGION' --route-table-id '$RTB2_ID' --subnet-id '$SUBNET2_ID' --query 'AssociationId' --output text") check_error $? "Failed to associate route table with subnet in VPC2" RTB2_ASSOC_ID=$(sanitize_var "$RTB2_ASSOC_ID") || check_error 1 "Invalid RTB2_ASSOC_ID returned" CREATED_RESOURCES+=("Route Table Association for VPC2: $RTB2_ASSOC_ID") CLEANUP_COMMANDS+=("aws ec2 disassociate-route-table --region '$AWS_REGION' --association-id '$RTB2_ASSOC_ID'") echo "Route table associated with subnet in VPC2" # Verify the VPC peering connection echo "Verifying VPC peering connection..." log_cmd "aws ec2 describe-vpc-peering-connections --region '$AWS_REGION' --vpc-peering-connection-ids '$PEERING_ID' --query 'VpcPeeringConnections[0].[VpcPeeringConnectionId,Status.Code,AccepterVpcInfo.VpcId,RequesterVpcInfo.VpcId]' --output table" check_error $? "Failed to verify VPC peering connection" echo "VPC peering connection verified" # Display summary of created resources echo "" echo "==============================================" echo "SUMMARY OF CREATED RESOURCES" echo "==============================================" echo "VPC1 ID: $VPC1_ID" echo "VPC1 CIDR: $VPC1_CIDR" echo "Subnet1 ID: $SUBNET1_ID (CIDR: $VPC1_SUBNET_CIDR)" echo "VPC2 ID: $VPC2_ID" echo "VPC2 CIDR: $VPC2_CIDR" echo "Subnet2 ID: $SUBNET2_ID (CIDR: $VPC2_SUBNET_CIDR)" echo "Peering Connection ID: $PEERING_ID" echo "Route Table 1 ID: $RTB1_ID" echo "Route Table 1 Association ID: $RTB1_ASSOC_ID" echo "Route Table 2 ID: $RTB2_ID" echo "Route Table 2 Association ID: $RTB2_ASSOC_ID" echo "" echo "Created resources:" for resource in "${CREATED_RESOURCES[@]}"; do echo "- $resource" done echo "==============================================" echo "" # Test connectivity (optional) echo "==============================================" echo "CONNECTIVITY TEST" echo "==============================================" echo "To test connectivity between VPCs, you would need to:" echo "1. Launch EC2 instances in each subnet" echo "2. Configure security groups to allow traffic" echo "3. Test ping or other network connectivity" echo "" # Automatic cleanup echo "" echo "==============================================" echo "CLEANUP CONFIRMATION" echo "==============================================" echo "Auto-confirming cleanup of all created resources..." CLEANUP_CHOICE="y" if [[ "${CLEANUP_CHOICE,,}" == "y" ]]; then echo "Starting cleanup process..." # Clean up in reverse order echo "Disassociating route table from subnet in VPC2..." log_cmd "aws ec2 disassociate-route-table --region '$AWS_REGION' --association-id '$RTB2_ASSOC_ID'" || true echo "Disassociating route table from subnet in VPC1..." log_cmd "aws ec2 disassociate-route-table --region '$AWS_REGION' --association-id '$RTB1_ASSOC_ID'" || true echo "Deleting route table for VPC2..." log_cmd "aws ec2 delete-route-table --region '$AWS_REGION' --route-table-id '$RTB2_ID'" || true echo "Deleting route table for VPC1..." log_cmd "aws ec2 delete-route-table --region '$AWS_REGION' --route-table-id '$RTB1_ID'" || true echo "Deleting VPC peering connection..." log_cmd "aws ec2 delete-vpc-peering-connection --region '$AWS_REGION' --vpc-peering-connection-id '$PEERING_ID'" || true echo "Deleting subnet in VPC2..." log_cmd "aws ec2 delete-subnet --region '$AWS_REGION' --subnet-id '$SUBNET2_ID'" || true echo "Deleting subnet in VPC1..." log_cmd "aws ec2 delete-subnet --region '$AWS_REGION' --subnet-id '$SUBNET1_ID'" || true # Delete VPCs if they were created by this script if [ "$CREATE_VPCS" = true ]; then echo "Deleting VPC2..." log_cmd "aws ec2 delete-vpc --region '$AWS_REGION' --vpc-id '$VPC2_ID'" || true echo "Deleting VPC1..." log_cmd "aws ec2 delete-vpc --region '$AWS_REGION' --vpc-id '$VPC1_ID'" || true elif [ "$CREATE_VPC2_ONLY" = true ]; then echo "Deleting VPC2..." log_cmd "aws ec2 delete-vpc --region '$AWS_REGION' --vpc-id '$VPC2_ID'" || true fi echo "Cleanup completed successfully." else echo "Cleanup skipped. Resources will remain in your AWS account." echo "" echo "To manually clean up later, you can delete resources in this order:" echo "1. Route table associations" echo "2. Route tables" echo "3. VPC peering connection" echo "4. Subnets" if [ "$CREATE_VPCS" = true ] || [ "$CREATE_VPC2_ONLY" = true ]; then echo "5. VPCs (if created by this script)" fi fi echo "Script execution completed. See $LOG_FILE for detailed logs."