기존에는 운영서버와 개발서버가 분리되어 있지 않았다. 기존 시스템과 동일하게 배포하기 위해서 ECS, RDS, ALB, Route 53과 같은 AWS 서비스들을 활용해서 배포를 진행했다. 배포 이전에 ECS 개념에 대해서 정리도 했었고 많이 찾아봤었다. 하지만 단순히 개념을 공부하는 것과 실제로 연결해 운영하는 것은 별개인 것 같다.
ECS 도입이나 ECS와 Github Actions를 활용한 글은 많지만, 그 외 다른 AWS 서비스들까지 함께 활용해 전체 배포 자동화 구축에 대해 다룬 글은 드물었다. 혼자 여러 번 헤매다 보니, 이번 기회에 AWS ECS를 통한 배포를 기반으로 RDS, ALB을 함께 활용한 전체적인 배포 자동화 구축 과정을 꼼꼼히 정리해보려 한다.
1. AWS ERC 생성

Amazon ECR(Elastic Container Registry)은 AWS가 제공하는 완전 관리형 컨테이너 이미지 저장소이다. 쉽게 말하면, AWS에서 제공하는 Docker Hub와 같다. ECR은 프라이빗 저장소로, Docker 이미지를 안전하게 저장, 관리, 배포할 수 있도록 해주며 AWS의 다양한 서비스와 IAM과 원활하게 통합되어 이미지 보안과 접근 제어를 쉽게 할 수 있다.
ECS를 사용해 컨테이너 기반 애플리케이션을 실행하려면 먼저 컨테이너 이미지를 준비해야 한다. 이 이미지를 ECR에 업로드하고, 태스크 정의에서 해당 ECR 이미지 URL을 지정하면 ECS는 이 이미지를 기반으로 컨테이너를 실행한다. 즉, ECR과 ECS는 연동되어 최신 이미지를 쉽게 배포하고 관리할 수 있도록 해준다.
따라서 ERC를 먼저 생성해야한다. 간단하게 리포지토리 이름만 정해주면 된다. namespace/repo-name으로 작성하라 되어있지만 나는 간단하게 service name-server로 지정했다. 이미지 태그 지정은 도커 이미지에 버전을 부여하는 방법이다. 예를 들어 "image:latest"에서 "latest"가 태그이다. mutable 태그는 동일 태그에 새로운 이미지를 덮어쓸 수 있도록 허용하는 반면, immutable 태그는 한 번 지정된 태그를 변경할 수 없게 하여 특정 이미지 버전을 보존한다. 나는 따로 태그 지정을 해주지 않고 latest로 계속 사용할 예정이라 덮어쓸 수 있도록 허용했으며, 암호화도 표준으로 설정했다.

Repository가 생성됐다면, 생성된 리포지토리에 이미지를 하나 푸시해놓는다. 물론, 우리는 GitHub Actions를 통해 이미지 푸시를 자동화할 예정이지만, 자동화 이전에 ECS가 잘돌아가는지 확인해보기 위해서 미리 하나 올려놓는 것이다.
만들어진 리포지토리에 푸시 명령 보기 버튼을 클릭하면 각 OS에 맞는 푸시 명령어가 나와있다. 해당 프로젝트에 Dockerfile이 있는 루트에서 실행해서 이미지가 잘 올라가는지 미리 확인해보면 된다. 이미지가 잘 올라갔다면 해당 리포지토리에 image가 하나 생길 것이다. (올리는 방법은 매우 친절하게 써있기 때문에 생략하겠다. 대략적으로 Dockerfile을 읽어서 이미지를 빌드하고, 태그를 변경해서 AWS 리포지토리로 푸시하는 순서이다.)
2. AWS ECS 클러스터 생성

클러스터는 ECS에서 컨테이너 태스크를 논리적으로 그룹화하는 역할이다. 클러스터 생성 시 이름만 입력하면 생성된다. 클러스터는 논리적 그룹화 개념이므로, 태스크 정의 등록 순서와 큰 관계가 없다.
3. AWS ECS 테스크 정의 생성

태스크 정의는 컨테이너를 어떻게 실행할지에 대한 세부 설정을 포함하는 JSON 파일이다. 태스크 정의 생성 화면에서 태스크 정의 이름과 인프라 옵션(Fargate, EC2)을 선택한다.
나는 인프라 운영의 간편함을 위해 Fargate를 선택했다. Fargate는 서버리스 환경으로, 인프라 관리를 AWS에 전적으로 위임할 수 있어 운영이 단순해진다. EC2 인스턴스는 사용자가 직접 관리해야 하므로, 세밀한 제어는 가능하지만 관리 부담이 있다.
테스크 크기(CPU 단위와 메모리 크기를 지정)할 수 있으며, 각자 프로젝트 환경에 맞게 설정해주면 된다. 추가로 태스크 역할을 ecsTaskExecutionRole로 설정해줬다.

해당 화면은 ECS 태스크 정의에서 컨테이너를 어떻게 실행할지 세부적으로 설정하는 부분이다.
1. 컨테이너 이름 및 이미지 URL
컨테이너 이름을 정하고, 사용할 도커 이미지를 지정한다. 여기서 이미지 URI엔 위에서 생성한 ECR의 URI를 작성해주면 된다. repository/image:tag 형식으로 입력한다. 나는 tag를 :latest로 지정해주었다.
2. 포트매핑
포트 매핑을 추가하여 컨테이너가 호스트의 포트에 액세스하여 트래픽을 보내거나 받을 수 있도록 설정하는 것이다. 나는 SpringBoot의 포트인 8080을 열어주었고, MySQL(AWS RDS)을 사용하기 때문에 MySQL포트인 3306포트도 열여주었다.
3. 리소스 설정
컨테이너가 사용할 CPU 단위와 메모리 크기를 지정한다. (GPU가 필요한 경우 GPU 리소스를 설정할 수도 있지만, 이는 EC2 기반 GPU 인스턴스에서만 가능하다.)
4. 환경 변수, 시크릿
애플리케이션이 필요로 하는 환경 변수를 직접 입력하거나, 외부 파일(환경 파일) 또는 Secrets Manager를 통해 주입할 수 있다. DB 접속 정보나 API 키 같은 민감 정보는 보통 AWS Secrets Manager 또는 SSM Parameter Store를 사용한다. 하지만 나는 Github의 Secret 키를 통해 관리하기 때문에 따로 설정해두진 않았다.
더 많은 옵션이 있지만 이 정도 설정을 하고 생성을 해줬다. 태스크 정의를 생성하게 되면, 이 태스크 정의를 사용해서 서비스를 배포하거나 태스크 실행을 할 수 있다.
4. AWS ECS 서비스 생성

이전에 만들어진 태스크 정의에서 배포 -> 서비스 생성을 진행하면 해당 태스크로 서비스를 생성할 수 있다. 환경(기존 클러스터) 선택 란에서 위에 생성한 클러스터를 선택해 주면 해당 클러스터(그룹) 안에 서비스가 생성된다. 원하는 태스크 개수부터 배포 옵션까지 자세히 설정할 수 있지만 나는 따로 변경하진 않았다.
네트워킹 설정 란에서는 각 서비스가 안전하고 효율적으로 통신할 수 있도록 VPC를 별도로 설정해 줬다. VPC를 사용하면 서브넷, 보안 그룹, 라우팅 테이블 등을 통해 네트워크 트래픽을 세밀하게 제어할 수 있다. 또한, 외부와 내부 리소스 간의 접근을 분리하여 보안을 강화할 수 있다. VPC와 같은 AWS 네트워크에 관한 자세한 내용은 해당 포스팅을 참고하길 바란다!
4-1. VPC 생성


Classless Inter-Domain Routing(CIDR)은 인터넷상의 데이터 라우팅 효율성을 향상시키는 IP 주소 할당 방법이다. CIDR을 사용하여 네트워크에 유연하고 효율적으로 IP 주소를 할당한다. 관한 자세한 내용은 https://aws.amazon.com/ko/what-is/cidr/ 공식 문서를 확인하면 된다.
Virtual Private Cloud(VPC)의 IP 주소는 Classless Inter-Domain Routing(CIDR) 표기법을 사용하여 표시된다. VPC에는 연결된 IPv4 CIDR 블록이 있어하며, 선택에 따라 추가 IPv4 CIDR 블록과 하나 이상의 IPv6 CIDR 블록을 연결할 수 있다. (공식 문서: https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-cidr-blocks.html)
VPC만 생성하고 서브넷이나 라우팅 테이블을 각각 생성해도 되지만, VPC 등 을 선택하면 나머지 요소들도 알아서 자동으로 생성해 준다. 가용영역 2, 퍼블릭 서브넷과 프라이빗 서브넷도 각각 두 개씩 생성했으며, CIDR 블록은 임의로 해두긴 했는데 이 주소 또한 계산을 해서 설정해 주면 된다. (CIDR 계산기이다. https://cidr.xyz/)
4-2. 보안 그룹 생성

보안그룹의 VPC를 위에서 생성한 VPC로 설정해 둔 다음, 인바운드 규칙을 설정해 준다. 나는 http, https, mysql, ssh, 8080(Spring) 포트를 열어줬다.
4-3. 로드 밸런서 생성


이 실습에서 LB까지 다루긴 너무 긴 관계로 난 서비스 서버와 admin 서버의 트래픽을 분산하기 위함과 https 보안 인증과 dns 적용을 쉽게 하기 위해서 적용했다. 나중에 로드밸런서에 대해서 자세히 다뤄보겠다..!! 아마도..ㅎㅎ
이름과 생성해 준 VPC를 선택 해주고 가용 영역(ALB가 여러 가용 영역에 걸쳐 트래픽을 고르게 분산시켜 준다.)으로 Public 서브넷 두 개를 선택해 줬다. 보안그룹까지 위에서 생성한 보안 그룹을 선택해줬다.
리스너 및 라우팅을 연결하기 위해선 대상그룹을 생성해줘야 하는데 IP주소 유형으로 선택해 줬으며, 위에서 만든 VPC를 설정해 줬다.

결국 이 과정을 중간에 넣었던 건 서비스를 생성하면서 로드 밸런싱 설정을 같이 해줘야 하기 때문에 미리 도입했다. 로드 밸런서까지 서비스에 연결을 해줬다면 서비스를 생성하면 된다.
사실 서비스가 생성되면 서비스에서 설정된 방식으로 태스크가 실행이 될 것이다. 서비스에 설정된 ECR에서 태그에 맞는 이미지를 가져와서 지정한 서버(Fargate)로 실행을 시켜준다.
5. AWS RDS 생성
DB 생성에서는 각자 필요한 Database를 선택해서 생성할 때, VPC와 보안그룹만 위에 생성해 준 것들로 지정하면 된다. 생략하겠다 ㅎㅎ
6. Github Actions
name: CICD Deploy to Amazon ECS - dev
on:
push:
branches: [ "dev" ]
env:
AWS_REGION: ap-northeast-2
ECR_REPOSITORY: ${{ secrets.DEV_ECR_REPO }}
ECS_SERVICE: ${{ secrets.DEV_ECR_SV }}
ECS_CLUSTER: ${{ secrets.DEV_ECR_CS }}
ECS_TASK_DEFINITION: task-definition.json
CONTAINER_NAME: ${{ secrets.DEV_ECR_CONTAINER }}
permissions:
contents: read
jobs:
ci:
name: Push Docker Image to DockerHub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 23
uses: actions/setup-java@v4
with:
java-version: '23'
distribution: 'corretto'
- name: Gradle Cache
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/build.gradle') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Create application.yml
run: |
mkdir -p src/main/resources
echo "${{ secrets.CD_DEV_APPLICATION }}" > src/main/resources/application.yml
cat src/main/resources/application.yml
shell: bash
- name: Build with Gradle
run: |
chmod +x gradlew
./gradlew clean build -x test --parallel --build-cache --daemon
shell: bash
- name: Docker Login
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKER_LOGIN_USERNAME }}
password: ${{ secrets.DOCKER_LOGIN_ACCESSTOKEN }}
- name: Docker Image Build and Push
run: |
DOCKER_BUILDKIT=1 docker build --cache-from dockerhub/repo --platform linux/amd64 -t dockerhub/repo .
docker push dockerhub/repo
cd:
needs: ci
name: ECS for Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Checkout
uses: actions/checkout@v4
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Pull Docker image from Docker Hub
run: |
echo "Pulling image from Docker Hub..."
docker pull dockerhub/repo:latest
- name: Tag Docker image for ECR
run: |
echo "Tagging image for Amazon ECR..."
docker tag dockerhub/repo:latest ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
- name: Push Docker image to Amazon ECR
run: |
echo "Pushing image to Amazon ECR..."
docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
- name: Remove enableFaultInjection from task-definition.json
shell: bash
run: |
jq 'del(.enableFaultInjection)' task-definition.json > task-definition-fixed.json
mv task-definition-fixed.json task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
FROM amazoncorretto:23
COPY build/libs/main-0.0.1-SNAPSHOT.jar favy-main.jar
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "-Dspring.profiles.active=dev", "favy-main.jar"]
다음은 이 워크플로우가 dev 브랜치에 코드가 push 될 때 Amazon ECS에 최신 이미지를 배포하는 과정을 설명한 내용이다. (나는 dev 브랜치를 개발서버로, main 브랜치를 운영서버로 두고 연결해 놨다.)
CI 작업
- JDK 23을 설정하고 Gradle 캐시를 구성한다.
- 애플리케이션 설정 파일(application.yml)을 생성하고, 빌드를 통해 JAR 파일을 생성한다.
- 도커 로그인을 하고, Dockerfile을 기반으로 이미지를 빌드한 후 Docker Hub에 푸시한다.
CD 작업
- AWS 자격 증명을 한다.
- 소스 코드를 다시 체크아웃한 후 Amazon ECR에 로그인한다.
- Docker Hub에서 이미지를 pull 하고, 이를 Amazon ECR 형식으로 태그 한 후 ECR에 푸시한다.
- 태스크 정의 파일(task-definition.json)에서 enableFaultInjection 키를 제거하여 수정한다.
- 수정된 태스크 정의 파일의 containerDefinitions 섹션에서, 새 이미지 URL을 반영한다.
- 업데이트된 태스크 정의를 기반으로, 지정된 ECS 서비스와 클러스터에 배포하여 서비스가 새 이미지를 사용하도록 한다.
간단히 말하면, CI 단계에서는 소스 코드 변경 시 빌드를 진행하고 생성된 JAR 파일을 기반으로 Dockerfile로 이미지를 생성한다. 이 이미지를 Docker Hub에 푸시하는 작업이 이루어진다. 이후 CD 단계에서는 Docker Hub에서 이미지를 가져와 Amazon ECR에 올리고, 태스크 정의를 수정하여 업데이트된 이미지로 ECS 서비스와 클러스터에 배포한다.
여기서 Docker Hub에 푸시하는 것은 반드시 필요한 과정은 아니다. 바로 빌드한 이미지를 ECR에 올려도 되지만, 추가 기록과 안전장치 개념에서 Docker Hub에 이미지를 저장하도록 구성하였다.
또한, "Remove enableFaultInjection from task-definition.json" 단계는 일반적으로 필요하지 않으나, 내 경우 enableFaultInjection 관련 에러가 발생하여 해당 키를 제거하도록 설정했다.
이렇게 구성함으로써, 코드 변경 시 자동으로 빌드, 이미지 생성, 태스크 정의 업데이트, 그리고 ECS 배포가 이루어진다.
추가로, 나는 application.yml 파일을 GitHub Secret으로 관리한다. RDS를 사용할 경우에는, application.yml 파일 내의 database URL에 AWS RDS 생성 시 제공된 엔드포인트와 사용자 이름, 비밀번호를 동일하게 설정하면 된다.
7. 까진 아니지만 난 ALB에 Route 53을 활용해서 https와 DNS까지 적용했다.
AWS ECS를 도입하면서..
ECS를 처음 도입하면서 여러 에러와 설정 이슈를 겪었다. 맨날 ec2 인스턴스만 생성해서 서버를 돌리기만 했던 나기에 어쩌면 당연할지도..? 생각해 보면 ECS만 알고 진행할 수 있는 게 아니라 기존에 같이 설정해 놓았던 다양한 AWS 서비스들까지 같이 도입해야 하는 상황에서 각각의 AWS 서비스에 대한 이해가 잘 없는 상태였기 때문에 힘들었던 거 같다. 그래도 한 번 고생하고 나니 Admin 서버를 배포하는 것이 쉬워졌다 럭키비키..!
배포하면서 생각보다 시간 지체를 많이 했던 큰 이슈들 중 하나는 DB 연결 오류였다. DB 연결이 안 된다고 나오는 경우, 동일한 VPC 내에 있지 않거나 보안그룹의 인바운드/아웃바운드 규칙을 제대로 설정하지 않아서 발생하는 문제였다. 또한, 데이터베이스 서버에 create database 명령어로 DB를 생성하지 않아서 오류가 발생한 경우도 있었다. 만약 수정 후 빠르게 확인하고 싶다면, 서비스 강제 재배포를 통해 최신 설정을 반영할 수 있다(다만, 이미 운영 중인 서버에서는 주의해야 한다) 혹시라도 DB에러가 난다면 참고하길 바란다!!!!!!

그리고 각 태스크에서 실행되면 실패든 성공이든 로그를 통해 문제 상황을 확인할 수 있다는 점이 좋았다. 로그를 뜯어보면 대략적으로 어느 문제인지 알기 쉬웠다.
결국, ECS를 사용해 보니 결국 편리함이 가장 큰 장점인 것 같다. 배포나 관리를 편하게 하고 싶다면 ECS 도입을 한 번 고려해 보는 것도 좋을 것 같다.
'Infra' 카테고리의 다른 글
[Infra] AWS ECS란? (1) | 2025.03.25 |
---|---|
[Infra] AWS 인프라 입문 (VPC, Subnet, Route Table, NAT Gateway, AZ..) (0) | 2025.03.05 |
[Infra] Nginx란 무엇인가? 그리고 왜 사용하는가? (feat. Apache) (2) | 2024.11.08 |
기존에는 운영서버와 개발서버가 분리되어 있지 않았다. 기존 시스템과 동일하게 배포하기 위해서 ECS, RDS, ALB, Route 53과 같은 AWS 서비스들을 활용해서 배포를 진행했다. 배포 이전에 ECS 개념에 대해서 정리도 했었고 많이 찾아봤었다. 하지만 단순히 개념을 공부하는 것과 실제로 연결해 운영하는 것은 별개인 것 같다.
ECS 도입이나 ECS와 Github Actions를 활용한 글은 많지만, 그 외 다른 AWS 서비스들까지 함께 활용해 전체 배포 자동화 구축에 대해 다룬 글은 드물었다. 혼자 여러 번 헤매다 보니, 이번 기회에 AWS ECS를 통한 배포를 기반으로 RDS, ALB을 함께 활용한 전체적인 배포 자동화 구축 과정을 꼼꼼히 정리해보려 한다.
1. AWS ERC 생성

Amazon ECR(Elastic Container Registry)은 AWS가 제공하는 완전 관리형 컨테이너 이미지 저장소이다. 쉽게 말하면, AWS에서 제공하는 Docker Hub와 같다. ECR은 프라이빗 저장소로, Docker 이미지를 안전하게 저장, 관리, 배포할 수 있도록 해주며 AWS의 다양한 서비스와 IAM과 원활하게 통합되어 이미지 보안과 접근 제어를 쉽게 할 수 있다.
ECS를 사용해 컨테이너 기반 애플리케이션을 실행하려면 먼저 컨테이너 이미지를 준비해야 한다. 이 이미지를 ECR에 업로드하고, 태스크 정의에서 해당 ECR 이미지 URL을 지정하면 ECS는 이 이미지를 기반으로 컨테이너를 실행한다. 즉, ECR과 ECS는 연동되어 최신 이미지를 쉽게 배포하고 관리할 수 있도록 해준다.
따라서 ERC를 먼저 생성해야한다. 간단하게 리포지토리 이름만 정해주면 된다. namespace/repo-name으로 작성하라 되어있지만 나는 간단하게 service name-server로 지정했다. 이미지 태그 지정은 도커 이미지에 버전을 부여하는 방법이다. 예를 들어 "image:latest"에서 "latest"가 태그이다. mutable 태그는 동일 태그에 새로운 이미지를 덮어쓸 수 있도록 허용하는 반면, immutable 태그는 한 번 지정된 태그를 변경할 수 없게 하여 특정 이미지 버전을 보존한다. 나는 따로 태그 지정을 해주지 않고 latest로 계속 사용할 예정이라 덮어쓸 수 있도록 허용했으며, 암호화도 표준으로 설정했다.

Repository가 생성됐다면, 생성된 리포지토리에 이미지를 하나 푸시해놓는다. 물론, 우리는 GitHub Actions를 통해 이미지 푸시를 자동화할 예정이지만, 자동화 이전에 ECS가 잘돌아가는지 확인해보기 위해서 미리 하나 올려놓는 것이다.
만들어진 리포지토리에 푸시 명령 보기 버튼을 클릭하면 각 OS에 맞는 푸시 명령어가 나와있다. 해당 프로젝트에 Dockerfile이 있는 루트에서 실행해서 이미지가 잘 올라가는지 미리 확인해보면 된다. 이미지가 잘 올라갔다면 해당 리포지토리에 image가 하나 생길 것이다. (올리는 방법은 매우 친절하게 써있기 때문에 생략하겠다. 대략적으로 Dockerfile을 읽어서 이미지를 빌드하고, 태그를 변경해서 AWS 리포지토리로 푸시하는 순서이다.)
2. AWS ECS 클러스터 생성

클러스터는 ECS에서 컨테이너 태스크를 논리적으로 그룹화하는 역할이다. 클러스터 생성 시 이름만 입력하면 생성된다. 클러스터는 논리적 그룹화 개념이므로, 태스크 정의 등록 순서와 큰 관계가 없다.
3. AWS ECS 테스크 정의 생성

태스크 정의는 컨테이너를 어떻게 실행할지에 대한 세부 설정을 포함하는 JSON 파일이다. 태스크 정의 생성 화면에서 태스크 정의 이름과 인프라 옵션(Fargate, EC2)을 선택한다.
나는 인프라 운영의 간편함을 위해 Fargate를 선택했다. Fargate는 서버리스 환경으로, 인프라 관리를 AWS에 전적으로 위임할 수 있어 운영이 단순해진다. EC2 인스턴스는 사용자가 직접 관리해야 하므로, 세밀한 제어는 가능하지만 관리 부담이 있다.
테스크 크기(CPU 단위와 메모리 크기를 지정)할 수 있으며, 각자 프로젝트 환경에 맞게 설정해주면 된다. 추가로 태스크 역할을 ecsTaskExecutionRole로 설정해줬다.

해당 화면은 ECS 태스크 정의에서 컨테이너를 어떻게 실행할지 세부적으로 설정하는 부분이다.
1. 컨테이너 이름 및 이미지 URL
컨테이너 이름을 정하고, 사용할 도커 이미지를 지정한다. 여기서 이미지 URI엔 위에서 생성한 ECR의 URI를 작성해주면 된다. repository/image:tag 형식으로 입력한다. 나는 tag를 :latest로 지정해주었다.
2. 포트매핑
포트 매핑을 추가하여 컨테이너가 호스트의 포트에 액세스하여 트래픽을 보내거나 받을 수 있도록 설정하는 것이다. 나는 SpringBoot의 포트인 8080을 열어주었고, MySQL(AWS RDS)을 사용하기 때문에 MySQL포트인 3306포트도 열여주었다.
3. 리소스 설정
컨테이너가 사용할 CPU 단위와 메모리 크기를 지정한다. (GPU가 필요한 경우 GPU 리소스를 설정할 수도 있지만, 이는 EC2 기반 GPU 인스턴스에서만 가능하다.)
4. 환경 변수, 시크릿
애플리케이션이 필요로 하는 환경 변수를 직접 입력하거나, 외부 파일(환경 파일) 또는 Secrets Manager를 통해 주입할 수 있다. DB 접속 정보나 API 키 같은 민감 정보는 보통 AWS Secrets Manager 또는 SSM Parameter Store를 사용한다. 하지만 나는 Github의 Secret 키를 통해 관리하기 때문에 따로 설정해두진 않았다.
더 많은 옵션이 있지만 이 정도 설정을 하고 생성을 해줬다. 태스크 정의를 생성하게 되면, 이 태스크 정의를 사용해서 서비스를 배포하거나 태스크 실행을 할 수 있다.
4. AWS ECS 서비스 생성

이전에 만들어진 태스크 정의에서 배포 -> 서비스 생성을 진행하면 해당 태스크로 서비스를 생성할 수 있다. 환경(기존 클러스터) 선택 란에서 위에 생성한 클러스터를 선택해 주면 해당 클러스터(그룹) 안에 서비스가 생성된다. 원하는 태스크 개수부터 배포 옵션까지 자세히 설정할 수 있지만 나는 따로 변경하진 않았다.
네트워킹 설정 란에서는 각 서비스가 안전하고 효율적으로 통신할 수 있도록 VPC를 별도로 설정해 줬다. VPC를 사용하면 서브넷, 보안 그룹, 라우팅 테이블 등을 통해 네트워크 트래픽을 세밀하게 제어할 수 있다. 또한, 외부와 내부 리소스 간의 접근을 분리하여 보안을 강화할 수 있다. VPC와 같은 AWS 네트워크에 관한 자세한 내용은 해당 포스팅을 참고하길 바란다!
4-1. VPC 생성


Classless Inter-Domain Routing(CIDR)은 인터넷상의 데이터 라우팅 효율성을 향상시키는 IP 주소 할당 방법이다. CIDR을 사용하여 네트워크에 유연하고 효율적으로 IP 주소를 할당한다. 관한 자세한 내용은 https://aws.amazon.com/ko/what-is/cidr/ 공식 문서를 확인하면 된다.
Virtual Private Cloud(VPC)의 IP 주소는 Classless Inter-Domain Routing(CIDR) 표기법을 사용하여 표시된다. VPC에는 연결된 IPv4 CIDR 블록이 있어하며, 선택에 따라 추가 IPv4 CIDR 블록과 하나 이상의 IPv6 CIDR 블록을 연결할 수 있다. (공식 문서: https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-cidr-blocks.html)
VPC만 생성하고 서브넷이나 라우팅 테이블을 각각 생성해도 되지만, VPC 등 을 선택하면 나머지 요소들도 알아서 자동으로 생성해 준다. 가용영역 2, 퍼블릭 서브넷과 프라이빗 서브넷도 각각 두 개씩 생성했으며, CIDR 블록은 임의로 해두긴 했는데 이 주소 또한 계산을 해서 설정해 주면 된다. (CIDR 계산기이다. https://cidr.xyz/)
4-2. 보안 그룹 생성

보안그룹의 VPC를 위에서 생성한 VPC로 설정해 둔 다음, 인바운드 규칙을 설정해 준다. 나는 http, https, mysql, ssh, 8080(Spring) 포트를 열어줬다.
4-3. 로드 밸런서 생성


이 실습에서 LB까지 다루긴 너무 긴 관계로 난 서비스 서버와 admin 서버의 트래픽을 분산하기 위함과 https 보안 인증과 dns 적용을 쉽게 하기 위해서 적용했다. 나중에 로드밸런서에 대해서 자세히 다뤄보겠다..!! 아마도..ㅎㅎ
이름과 생성해 준 VPC를 선택 해주고 가용 영역(ALB가 여러 가용 영역에 걸쳐 트래픽을 고르게 분산시켜 준다.)으로 Public 서브넷 두 개를 선택해 줬다. 보안그룹까지 위에서 생성한 보안 그룹을 선택해줬다.
리스너 및 라우팅을 연결하기 위해선 대상그룹을 생성해줘야 하는데 IP주소 유형으로 선택해 줬으며, 위에서 만든 VPC를 설정해 줬다.

결국 이 과정을 중간에 넣었던 건 서비스를 생성하면서 로드 밸런싱 설정을 같이 해줘야 하기 때문에 미리 도입했다. 로드 밸런서까지 서비스에 연결을 해줬다면 서비스를 생성하면 된다.
사실 서비스가 생성되면 서비스에서 설정된 방식으로 태스크가 실행이 될 것이다. 서비스에 설정된 ECR에서 태그에 맞는 이미지를 가져와서 지정한 서버(Fargate)로 실행을 시켜준다.
5. AWS RDS 생성
DB 생성에서는 각자 필요한 Database를 선택해서 생성할 때, VPC와 보안그룹만 위에 생성해 준 것들로 지정하면 된다. 생략하겠다 ㅎㅎ
6. Github Actions
name: CICD Deploy to Amazon ECS - dev
on:
push:
branches: [ "dev" ]
env:
AWS_REGION: ap-northeast-2
ECR_REPOSITORY: ${{ secrets.DEV_ECR_REPO }}
ECS_SERVICE: ${{ secrets.DEV_ECR_SV }}
ECS_CLUSTER: ${{ secrets.DEV_ECR_CS }}
ECS_TASK_DEFINITION: task-definition.json
CONTAINER_NAME: ${{ secrets.DEV_ECR_CONTAINER }}
permissions:
contents: read
jobs:
ci:
name: Push Docker Image to DockerHub
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 23
uses: actions/setup-java@v4
with:
java-version: '23'
distribution: 'corretto'
- name: Gradle Cache
uses: actions/cache@v3
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', '**/build.gradle') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Create application.yml
run: |
mkdir -p src/main/resources
echo "${{ secrets.CD_DEV_APPLICATION }}" > src/main/resources/application.yml
cat src/main/resources/application.yml
shell: bash
- name: Build with Gradle
run: |
chmod +x gradlew
./gradlew clean build -x test --parallel --build-cache --daemon
shell: bash
- name: Docker Login
uses: docker/login-action@v2.2.0
with:
username: ${{ secrets.DOCKER_LOGIN_USERNAME }}
password: ${{ secrets.DOCKER_LOGIN_ACCESSTOKEN }}
- name: Docker Image Build and Push
run: |
DOCKER_BUILDKIT=1 docker build --cache-from dockerhub/repo --platform linux/amd64 -t dockerhub/repo .
docker push dockerhub/repo
cd:
needs: ci
name: ECS for Deploy
runs-on: ubuntu-latest
environment: production
steps:
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Checkout
uses: actions/checkout@v4
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Pull Docker image from Docker Hub
run: |
echo "Pulling image from Docker Hub..."
docker pull dockerhub/repo:latest
- name: Tag Docker image for ECR
run: |
echo "Tagging image for Amazon ECR..."
docker tag dockerhub/repo:latest ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
- name: Push Docker image to Amazon ECR
run: |
echo "Pushing image to Amazon ECR..."
docker push ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
- name: Remove enableFaultInjection from task-definition.json
shell: bash
run: |
jq 'del(.enableFaultInjection)' task-definition.json > task-definition-fixed.json
mv task-definition-fixed.json task-definition.json
- name: Fill in the new image ID in the Amazon ECS task definition
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: ${{ env.ECS_TASK_DEFINITION }}
container-name: ${{ env.CONTAINER_NAME }}
image: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: ${{ env.ECS_SERVICE }}
cluster: ${{ env.ECS_CLUSTER }}
wait-for-service-stability: true
FROM amazoncorretto:23
COPY build/libs/main-0.0.1-SNAPSHOT.jar favy-main.jar
ENTRYPOINT ["java", "-Duser.timezone=Asia/Seoul", "-jar", "-Dspring.profiles.active=dev", "favy-main.jar"]
다음은 이 워크플로우가 dev 브랜치에 코드가 push 될 때 Amazon ECS에 최신 이미지를 배포하는 과정을 설명한 내용이다. (나는 dev 브랜치를 개발서버로, main 브랜치를 운영서버로 두고 연결해 놨다.)
CI 작업
- JDK 23을 설정하고 Gradle 캐시를 구성한다.
- 애플리케이션 설정 파일(application.yml)을 생성하고, 빌드를 통해 JAR 파일을 생성한다.
- 도커 로그인을 하고, Dockerfile을 기반으로 이미지를 빌드한 후 Docker Hub에 푸시한다.
CD 작업
- AWS 자격 증명을 한다.
- 소스 코드를 다시 체크아웃한 후 Amazon ECR에 로그인한다.
- Docker Hub에서 이미지를 pull 하고, 이를 Amazon ECR 형식으로 태그 한 후 ECR에 푸시한다.
- 태스크 정의 파일(task-definition.json)에서 enableFaultInjection 키를 제거하여 수정한다.
- 수정된 태스크 정의 파일의 containerDefinitions 섹션에서, 새 이미지 URL을 반영한다.
- 업데이트된 태스크 정의를 기반으로, 지정된 ECS 서비스와 클러스터에 배포하여 서비스가 새 이미지를 사용하도록 한다.
간단히 말하면, CI 단계에서는 소스 코드 변경 시 빌드를 진행하고 생성된 JAR 파일을 기반으로 Dockerfile로 이미지를 생성한다. 이 이미지를 Docker Hub에 푸시하는 작업이 이루어진다. 이후 CD 단계에서는 Docker Hub에서 이미지를 가져와 Amazon ECR에 올리고, 태스크 정의를 수정하여 업데이트된 이미지로 ECS 서비스와 클러스터에 배포한다.
여기서 Docker Hub에 푸시하는 것은 반드시 필요한 과정은 아니다. 바로 빌드한 이미지를 ECR에 올려도 되지만, 추가 기록과 안전장치 개념에서 Docker Hub에 이미지를 저장하도록 구성하였다.
또한, "Remove enableFaultInjection from task-definition.json" 단계는 일반적으로 필요하지 않으나, 내 경우 enableFaultInjection 관련 에러가 발생하여 해당 키를 제거하도록 설정했다.
이렇게 구성함으로써, 코드 변경 시 자동으로 빌드, 이미지 생성, 태스크 정의 업데이트, 그리고 ECS 배포가 이루어진다.
추가로, 나는 application.yml 파일을 GitHub Secret으로 관리한다. RDS를 사용할 경우에는, application.yml 파일 내의 database URL에 AWS RDS 생성 시 제공된 엔드포인트와 사용자 이름, 비밀번호를 동일하게 설정하면 된다.
7. 까진 아니지만 난 ALB에 Route 53을 활용해서 https와 DNS까지 적용했다.
AWS ECS를 도입하면서..
ECS를 처음 도입하면서 여러 에러와 설정 이슈를 겪었다. 맨날 ec2 인스턴스만 생성해서 서버를 돌리기만 했던 나기에 어쩌면 당연할지도..? 생각해 보면 ECS만 알고 진행할 수 있는 게 아니라 기존에 같이 설정해 놓았던 다양한 AWS 서비스들까지 같이 도입해야 하는 상황에서 각각의 AWS 서비스에 대한 이해가 잘 없는 상태였기 때문에 힘들었던 거 같다. 그래도 한 번 고생하고 나니 Admin 서버를 배포하는 것이 쉬워졌다 럭키비키..!
배포하면서 생각보다 시간 지체를 많이 했던 큰 이슈들 중 하나는 DB 연결 오류였다. DB 연결이 안 된다고 나오는 경우, 동일한 VPC 내에 있지 않거나 보안그룹의 인바운드/아웃바운드 규칙을 제대로 설정하지 않아서 발생하는 문제였다. 또한, 데이터베이스 서버에 create database 명령어로 DB를 생성하지 않아서 오류가 발생한 경우도 있었다. 만약 수정 후 빠르게 확인하고 싶다면, 서비스 강제 재배포를 통해 최신 설정을 반영할 수 있다(다만, 이미 운영 중인 서버에서는 주의해야 한다) 혹시라도 DB에러가 난다면 참고하길 바란다!!!!!!

그리고 각 태스크에서 실행되면 실패든 성공이든 로그를 통해 문제 상황을 확인할 수 있다는 점이 좋았다. 로그를 뜯어보면 대략적으로 어느 문제인지 알기 쉬웠다.
결국, ECS를 사용해 보니 결국 편리함이 가장 큰 장점인 것 같다. 배포나 관리를 편하게 하고 싶다면 ECS 도입을 한 번 고려해 보는 것도 좋을 것 같다.
'Infra' 카테고리의 다른 글
[Infra] AWS ECS란? (1) | 2025.03.25 |
---|---|
[Infra] AWS 인프라 입문 (VPC, Subnet, Route Table, NAT Gateway, AZ..) (0) | 2025.03.05 |
[Infra] Nginx란 무엇인가? 그리고 왜 사용하는가? (feat. Apache) (2) | 2024.11.08 |