There are more AWS SDK examples available in the AWS Doc SDK Examples
Creating an Amazon ECS service for the EC2 launch type
The following code example shows how to:
Create an ECS cluster
Create and monitor a service
Clean up resources
- Bash
-
- 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 "$@"-
For API details, see the following topics in AWS CLI Command Reference.
-
Create an Amazon ECS Linux task for the Fargate launch type
Creating an Amazon RDS DB instance