컨테이너 서비스 연결 구성하기

컨테이너 서비스 연결 구성하기

이 코드 예제에서는 ECS Service Connect를 사용해 컨테이너 서비스 연결을 구성하는 방법을 보여드려요. 다음 작업을 다루어요.

출처: 문서

본문

  • VPC 인프라(기본 VPC 사용) 구성
  • 로깅(CloudWatch 로그 그룹) 설정
  • ECS 클러스터 생성
  • IAM 역할 구성 (ecsTaskExecutionRole, ecsTaskRole)
  • Service Connect로 서비스 생성
  • 배포 검증
  • 리소스 정리

Bash (AWS CLI with Bash script)

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

이 스크립트는 기본 VPC를 사용해 ECS 클러스터를 만들고 Service Connect로 nginx 서비스를 배포해요.

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

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

  • AttachRolePolicy, AuthorizeSecurityGroupIngress, CreateCluster, CreateLogGroup, CreateRole, CreateSecurityGroup, CreateService, DeleteCluster, DeleteLogGroup, DeleteNamespace, DeleteRole, DeleteSecurityGroup, DeleteService (AWS Cloud Map), DeleteService (Amazon ECS), DeregisterTaskDefinition, DescribeClusters, DescribeServices, DescribeSubnets, DescribeTasks, DescribeVpcs, DetachRolePolicy, GetCallerIdentity, GetRole, ListNamespaces, ListServices, ListTaskDefinitions, ListTasks, RegisterTaskDefinition, UpdateService, Wait (Amazon EC2), Wait (Amazon ECS)

더 알아보기 (Learn more)