There are more AWS SDK examples available in the AWS Doc SDK Examples
AWS STS 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 AWS STS.
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.
Actions
The following code example shows how to use AssumeRole.
- 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 iecho # # This function enables the script to display the specified text only if # the global variable $VERBOSE is set to true. ############################################################################### function iecho() { if [[ $VERBOSE == true ]]; then echo "$@" fi } ############################################################################### # function errecho # # This function outputs everything sent to it to STDERR (standard error output). ############################################################################### function errecho() { printf "%s\n" "$*" 1>&2 } ############################################################################### # function sts_assume_role # # This function assumes a role in the AWS account and returns the temporary # credentials. # # Parameters: # -n role_session_name -- The name of the session. # -r role_arn -- The ARN of the role to assume. # # Returns: # [access_key_id, secret_access_key, session_token] # And: # 0 - If successful. # 1 - If an error occurred. ############################################################################### function sts_assume_role() { local role_session_name role_arn response local option OPTARG # Required to use getopts command in a function. # bashsupport disable=BP5008 function usage() { echo "function sts_assume_role" echo "Assumes a role in the AWS account and returns the temporary credentials:" echo " -n role_session_name -- The name of the session." echo " -r role_arn -- The ARN of the role to assume." echo "" } while getopts n:r:h option; do case "${option}" in n) role_session_name=${OPTARG} ;; r) role_arn=${OPTARG} ;; h) usage return 0 ;; \?) echo "Invalid parameter" usage return 1 ;; esac done response=$(aws sts assume-role \ --role-session-name "$role_session_name" \ --role-arn "$role_arn" \ --output text \ --query "Credentials.[AccessKeyId, SecretAccessKey, SessionToken]") local error_code=${?} if [[ $error_code -ne 0 ]]; then aws_cli_error_log $error_code errecho "ERROR: AWS reports create-role operation failed.\n$response" return 1 fi echo "$response" return 0 }-
For API details, see AssumeRole in AWS CLI Command Reference.
-
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 "$@"-
For API details, see the following topics in AWS CLI Command Reference.
-
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)"-
For API details, see the following topics in AWS CLI Command Reference.
-