목표 : AWS ECR - ECS를 활용하여 Docker 이미지 CI/CD

전체 플로우

코드를 main branch에 push
→ GitHub Actions가 이미지 빌드 & ECR push
→ ECS 서비스 업데이트 
→ ALB 헬스체크 통과 시 트래픽 전환

  1. 사전 세팅

    가정)
    Region : ap-northeast-2
    app port : 8080
    RDS : created
    
    1. ECR 레포지토리 생성

      콘솔] ECR → Repositories → Create repository

      예)
      Name: plus-app
      Tag immutability: Mutable (학습용)
      Scan on push: Enabled
      
      • 생성 후 URI 기록

        {ACCOUNT_ID}.dkr.ecr.ap-northeast-2.amazonaws.com/plus-app

    2. 네트워크 설정

      • 목적: ALB와 ECS(Fargate) task가 인터넷과 통신 가능
        • 설명
      1. VPC

        콘솔] VPC→ Your VPCs → Create VPC

        예)
        VPC-only
        Name: plus-vpc
        IPv4 CIDR: 10.0.0.0/16 
        
        Create
        

      콘솔] Subnets → Create subnet

      1. 퍼블릭 서브넷 2개 (AZ 다르게)

        예)
        VPC: plus-vpc (위의 VPC)
        Subnet 1
        	Name: public-a
        	AZ: ap-northeast-2a
        	IPv4 CIDR block: 10.0.1.0/24
        => Add new subnet
        Subnet 2
        	Name: public-c
        	AZ: ap-northeast-2c
        	IPv4 CIDR block: 10.0.1.0/24
        	
        Create
        
      2. Internet Gateway

        콘솔] Internet gateways → Create internet gateway

        예)
        Name: plus-igw
        
        • 생성 후 VPC에 attach (Actions → Attach to VPC → <plus-vpc>)
      3. Route Table (퍼블릭용) 생성

        콘솔] Route tables → Create route table

        예)
        Name: public-rt
        VPC: plus-vpc
        
        • 생성된 RT에 Route 수정 (상단 **Routes** 탭 → **Edit routes**)

          Add route
          Destination: 0.0.0.0/0
          Target: plus-igw 
          
          • 설명
        • 생성된 RT에 Subnet 추가 (**Subnet associations** → **Edit subnet associations**)

          • 위의 서브넷 2개 연결 (public-a, public-c)
          • 설명
    3. Security Group (SG) 설정

      • 목적: 최소한의 In/Out 규칙
      • 설명

      콘솔] **EC2** → 왼쪽 **Security Groups** → **Create security group**

      1. ALB-SG

        예)
        VPC: plus-vpc
        Inbound: HTTP 80 from 0.0.0.0/0 (또는 HTTPS 443)
        Outbound: 0.0.0.0/0 허용 (기본)
        
      2. ECS-TASK-SG

        예)
        VPC: plus-vpc
        Inbound: TCP 8080 from **ALB-SG** (Source에 SG 지정)
        Outbound: 0.0.0.0/0 허용 (기본)
        

        Task SG는 8080을 ALB-SG에서만 받도록 설정!! (직접 외부 공개 X)

    4. (옵션) CloudWatch Log Group

      • 목적: 컨테이너 로그 저장

      콘솔] CloudWatch → Logs → Create log group

      예)
      Name: /ecs/plus-app
      
    5. IAM Role

      1. Task Execution Role

        • 용도: ECR에서 이미지 풀, CloudWatch Logs 전송

        콘솔] IAM → Roles → Create role

        예)
        Name: ecsTaskExecutionRole
        Trusted entity: AWS service -> Elastic Container Service -> Elastic Container Service Task
        Permissions: AmazonECSTaskExecutionRolePolicy (AWS 관리형)
        
        • 생성 후 ARN 기록

          arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole

      2. Task Role

        • 용도: 애플리케이션에서 AWS API 접근 필요 시 (SSM, S3 등) 붙임
        예)
        Name: ecsAppTaskRole
        Trusted entity: AWS service -> Elastic Container Service -> Elastic Container Service Task
        Permission: None (추후에 attach 예정)
        
      3. Github OIDC 배포 Role

        • 용도: CI가 AWS에 로그인 없이 임시 권한으로 배포
        1. Identity providers

          콘솔] IAM → Identity providers → Add provider

          Provider type: OpenID Connect
          Provider URL: <https://token.actions.githubusercontent.com>
          Audience: sts.amazonaws.com
          
        2. Role 생성

          예)
          Name: GitHubActionsEcsDeployRole
          Trusted entity: Web identity → 위 OIDC 선택
          GitHub Organization: 깃헙 소유자 (조직 레포면 org 명, 개인 레포면 사용자명)
          
          • 생성 후 해당 Role의 Trust policy 조건에 Repo/Branch 제한 추가

            {
                "Version": "2012-10-17",
                "Statement": [
                    {
                        "Effect": "Allow",
                        "Principal": {
                            "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
                        },
                        "Action": "sts:AssumeRoleWithWebIdentity",
                        "Condition": {
                            "StringEquals": {
                                "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
                            },
                            "StringLike": {
                                "token.actions.githubusercontent.com:sub": "repo:<YOUR_GH_USER>/<YOUR_GH_REPO>:ref:refs/heads/main"
                            }
                        }
                    }
                ]
            }
            
          • 해당 Role의 Permissions policy

            {
            	"Version": "2012-10-17",
            	"Statement": [
            		{
            			"Sid": "EcrGetAuthToken",
            			"Effect": "Allow",
            			"Action": "ecr:GetAuthorizationToken",
            			"Resource": "*"
            		},
            		{
            			"Sid": "EcrPushPull",
            			"Effect": "Allow",
            			"Action": [
            				"ecr:BatchCheckLayerAvailability",
            				"ecr:CompleteLayerUpload",
            				"ecr:UploadLayerPart",
            				"ecr:InitiateLayerUpload",
            				"ecr:PutImage",
            				"ecr:BatchGetImage"
            			],
            			"Resource": "arn:aws:ecr:ap-northeast-2:<ACCOUNT_ID>:repository/plus-app"
            		},
            		{
            			"Sid": "EcsRegisterAndDeploy",
            			"Effect": "Allow",
            			"Action": [
            				"ecs:RegisterTaskDefinition",
            				"ecs:DescribeTaskDefinition",
            				"ecs:DescribeServices",
            				"ecs:UpdateService"
            			],
            			"Resource": "*"
            		},
            		{
            			"Sid": "PassOnlyTaskRoles",
            			"Effect": "Allow",
            			"Action": "iam:PassRole",
            			"Resource": [
            				"arn:aws:iam::<ACCOUNT_ID>:role/ecsTaskExecutionRole",
            				"arn:aws:iam::<ACCOUNT_ID>:role/ecsAppTaskRole"
            			],
            			"Condition": {
            				"StringEquals": {
            					"iam:PassedToService": "ecs-tasks.amazonaws.com"
            				}
            			}
            		}
            	]
            }
            

            ecr:GetAuthorizationToken의 Allow를 “*”로 설정하지 않을 시 아래와 같은 오류 발생함 Error: User: arn:aws:sts::<ACCOUNT_ID>:assumed-role/GitHubActionsEcsDeployRole/GitHubActions is not authorized to perform: ecr:GetAuthorizationToken on resource: * because no identity-based policy allows the ecr:GetAuthorizationToken action

          • 생성 후 role ARN 기록

            arn:aws:iam::<ACCOUNT_ID>:role/GitHubActionsEcsDeployRole

    6. ECS 클러스터 생성 (Fargate)

      콘솔] ECS → Clusters → Create cluster

      예)
      Name: plus-ecs-cluster
      옵션: 기본값
      
    7. ECS Task Definition 생성

      콘솔] ECS → Task definitions → Create new task definition

      예)
      Name: plus-task
      Launch type: Fargate
      Task role: ecsAppTaskRole(없으면 None)
      Task execution role: ecsTaskExecutionRole
      Task size: CPU 1 vCPU, Memory 2GB
      Container 추가:
      	Name: app
      	Image: (임시) public.ecr.aws/amazonlinux/amazonlinux:latest (나중에 CI로 덮어씀)
      	Port mappings: 8080/tcp
      	Log configuration: awslogs
      		Log group: /ecs/plus-app
      		Region: ap-northeast-2
      		Stream prefix: ecs
      	Health check:
      		Command: CMD-SHELL
      		Value: curl -f <http://localhost:8080/actuator/health> || exit 1
      		Interval: 15s
      		Timeout: 5s
      		Retries: 3
      		Start period: 30s
      
      Create
      
      • Environement variables

        Key Type 예시
        SPRING_PROFILES_ACTIVE value prod
        SPRING_DATASOURCE_URL value jdbc:mysql://<RDS_ENDPOINT>:3306/<DB_NAME>?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Seoul
        SPRING_DATASOURCE_USERNAME value <DB_USERNAME>
        SPRING_DATASOURCE_PASSWORD valueFrom arn:aws:secretsmanager:ap-northeast-2:<ACCOUNT_ID>:secret:prod/db/password-<DB_PW_ARN>:SPRING_DATASOURCE_PASSWORD::
        SPRING_JPA_HIBERNATE_DDL_AUTO value update
        SERVER_PORT value 8080
        LOGGING_LEVEL_ROOT value INFO
        JAVA_OPTS value -XX:+UseContainerSupport -XX:MaxRAMPercentage=75 -Duser.timezone=Asia/Seoul
        JWT_SECRET_KEY valueFrom arn:aws:secretsmanager:ap-northeast-2:<ACCOUNT_ID>:secret:prod/app/jwt-<JWT_ARN>:JWT_SECRET_KEY::
        (S3 사용 시) APP_S3_BUCKET value spring-plus-bucket-2025 (본인의 S3 bucket)
        (S3 사용 시) APP_S3_BASE_FOLDER value profiles (본인의 S3 폴더)
        (S3 사용 시) APP_S3_PRESIGN_TTL_SECONDS value 600

        비밀 값은 Secrets Manager의 valueFrom으로 주입하는 것이 좋음 (위 표의 DB_PW, JWT_KEY)

        Secret 값 추가 방법

    8. ALB + Target Group + ECS Service

    9. S3 사용 시 ecsAppTaskRole에 권한 부여 필요

  2. Spring Boot Dockerfile : repo root에 Dockerfile 생성

  3. GitHub Actions

  4. RDS VPC 설정

  5. 서비스 재배포


그 외 발생 가능한 오류