서버리스 실행 유형을 위한 컨테이너 태스크 생성하기

서버리스 실행 유형을 위한 컨테이너 태스크 생성하기

이 코드 예제에서는 Fargate 실행 유형으로 컨테이너 태스크를 만들고 배포하는 방법을 보여드려요. 다음 작업을 다루어요.

출처: 문서

본문

  • ECS 클러스터 생성
  • 태스크 정의 생성
  • ECS 서비스 생성
  • 리소스 정리

Bash (AWS CLI with Bash script)

Note GitHub에 더 많은 내용이 있어요. 전체 예제와 설정·실행 방법은 Sample developer tutorials 저장소에서 확인할 수 있어요.

이 스크립트는 Fargate 실행 유형으로 ECS 클러스터·태스크 정의·서비스를 생성해요. 리소스 의존성을 올바르게 처리하면서 정리하는 기능이 포함돼 있어요.

#!/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)"

API 상세는 AWS CLI Command Reference의 다음 항목을 참고해요.

  • AttachRolePolicy, AuthorizeSecurityGroupIngress, CreateCluster, CreateRole, CreateSecurityGroup, CreateService, DeleteCluster, DeleteSecurityGroup, DeleteService, DeregisterTaskDefinition, DescribeNetworkInterfaces, DescribeSecurityGroups, DescribeServices, DescribeSubnets, DescribeTasks, DescribeVpcs, GetCallerIdentity, GetRole, ListTaskDefinitions, ListTasks, RegisterTaskDefinition, UpdateService, Wait

더 알아보기 (Learn more)