프로젝트에서 서버를 AWS에서 관리되고 있는데, 현재는 개발 서버만 세팅되어 있었다. 이제 오픈을 위해 운영서버로 분리해야 했는데 이 작업을 맡게 되었다.
AWS를 처음 다뤄보다보니 개념이 너무 어려웠고, (사실은 아직도 잘 모르겠다 😭) 많은 시행착오를 겪으면서 결국 성공했는데 이것을 한번 잘 정리해보겠다..😅
1. EC2설정 : VPC,SUBNET,보안
기존에는 redis와 Bastion-Host 2개가 실행 중이었다.

기능에 대해서 설명하면 Bastion-Host 는 DB와 같이 private 한 네트워크에 위치해 있는 인스턴스에 접근하기 위한 터널링 역할로 사용한다. redis는 주로 데이터 캐싱이나 세션 저장 같은 작업을 처리하는 역할로 사용한다. 그래서 현재 redis에는 redis, kafka(알림), grafana(로그 시각화) 서버가 redis 도커 컨테이너에 올라가 있다.
※ AWS에서는 무엇을 보고 redis 기능과 Bastion 기능을 구분짓는지 궁금하여 아래에 자세히 설명했두었음. ※
Bastion-Host 설정 방식
- 퍼블릭 서브넷에 위치시킴 (인터넷 게이트웨이 연결)

※ 인터넷게이트웨이 연결 여부는 VPC를 확인
VPC → 인터넷게이트웨이 → 상태가 **"연결됨(Attached)"**인지 확인
💡 만약 인터넷 게이트웨이에 연결되지 않았다면
VPC → 인터넷 게이트웨이 생성 후, 인터넷 게이트웨이 연결 → 라우팅 테이블에서 0.0.0.0/0 대상 추가
하여 인터넷 연결
![]() Bastion-Host 의 서브넷 : DEV_PUB_SUBNET DEV_PUB_SUBNET 의 라우팅 테이블 : PUB_RTB |
![]() PUB_RTB의 라우팅 0.0.0.0 / 0 ▶ igw-XXXXX (게이트웨이에 연결됨) |
- 보안 그룹 설정에서 SSH(포트 22) 허용 → 외부에서 Bastion-Host로 접근 가능
▶ 프라이빗 인스턴스로의 터널링을 위해 프라이빗 인스턴스 보안 그룹에서 Bastion-Host의 IP를 허용

Redis 설정 방식
- 프라이빗 서브넷에 위치시킴 (인터넷 게이트웨이 연결 X)
![]() 서브넷 : DEV_PRV_SUBNET |
![]() DEV_PRV_SUBNET 의 라우팅 테이블 : DEV_PRV_RTB |
![]() DEV_PRV_RTB의 라우팅 0.0.0.0 / 0 ▶ nat-XXXXX igw-xxxxx 로 되어 있으면 인터넷게이트에 연결된 상태이지만 nat-xxxx로 되어 있으므로 프라이빗 서브넷에 위치된 상태임을 짐작 할 수 있다. ![]() |
- 보안 그룹에서 외부 접근 차단하고 Bastion-Host 또는 특정 애플리케이션 서버에서만 접근 허용 (예: 포트 6379)

필요하다면 VPC 피어링이나 보안 그룹 규칙으로 Bastion-Host를 통해 접속 가능하게 설정
2. RDS 추가
개발과 운영을 나눌 때 가장 먼저 한 작업은 RDS 분리였다.
기존에 dev-db로 추가 되어 있었는데 이것과 설정을 비슷하게 해서 prod-dev로 추가했다.
서브넷에는 PRD_PRV_SUBNET_B, PRD_PRV_SUBNET_C 를 추가해주었다. 보안은 기존에 추가되어 있었던 개발 DB 보안을 일단 그대로 사용하였다.

2-1. 로컬에서 prod db 연결 확인하기
※ tool : dbeaver
db : postgresql
SSH에서 Bation-Host 의 IP 를 통해 AWS 운영 db 에 접근 할 수 있다.

Host/IP : Bastion-Host IP주소 (AWS EC2 에서 확인)
User Name : ec2-user (default 설정)
Authentication Method : Public Key
Private Key : prod db를 생성할 때 받은 .pem 파일을 업로드

Host, Database, Username AWS RDS 에서 확인 가능
![]() |
![]() |
connection 이 success 나오면 RDS 설정이 잘 된것!
3. ECS 배포(with. git action)
우선, AWS로 배포를 위해 각 프로젝트별로 task-definition.json 과 git action 의 workflow.yml 파일을 개발과 운영으로 분리 해줘야 한다.
그래서 기존 파일 이름을 task-definition-dev.json, dev-workflow.yml 로 수정하고,
task-definition-prod.json, prod-workflow.yml 을 추가해주었다.
![]() |
name: DEV-Cluster CI CD
# 운영인 경우에는 PROD-Cluster CI CD 로 명칭했음
on:
workflow_dispatch:
env:
DB_RESOURCE_PATH: ./src/main/resources/application-database.yml
REGION: ap-northeast-2
ECR_REPOSITORY: [aws 에서 확인]
IMAGE_TAG: [프로젝트명칭]
ECR_CLUSTER_NAME: [AWS ECS에 추가할 클러스터 명]
ECR_SERVICE_NAME: [AWS ECS 클러스터에 추가 될 서비스명칭]
CONTAINER_NAME: [AWS ECS 클러스터에 추가 될 컨테이너명칭]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Set database info
uses: microsoft/variable-substitution@v1
with:
files: ${{ env.DB_RESOURCE_PATH }}
env:
spring.datasource.url: ${{ secrets.DEV_RDS_HOST }}
spring.datasource.username: ${{ secrets.DEV_RDS_USERNAME }}
spring.datasource.password: ${{ secrets.DEV_RDS_PASSWORD }}
spring.data.redis.host: ${{ secrets.DEV_REDIS_HOST }}
shell: bash
- name: Grant Gradle Wrapper
run: chmod +x ./gradlew
- name: Build with Gradle
uses: gradle/gradle-build-action@bd5760595778326ba7f1441bcf7e88b49de61a25 # v2.6.0
with:
arguments: clean build -x test
- name: Setting AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-region: ${{ env.REGION }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v1
- name: Build, tag, and push image to Amazon ECR
id: build-image
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
IMAGE_TAG: ${{ env.IMAGE_TAG }}
run: |
docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
- 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: task-definition-dev.json✨
# 운영인 경우에는 task-definition-prod.json 으로 경로 수정해 주어야 함.
container-name: ${{env.CONTAINER_NAME}}
image: ${{ steps.build-image.outputs.image }}
- name: Deploy Amazon ECS task definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
cluster: ${{env.ECR_CLUSTER_NAME}}
service: ${{env.ECR_SERVICE_NAME}}
wait-for-service-stability: true
SPRING_PROFILES_ACTIVE 설정으로 dev와 prod 를 구분짓는다.
그래서 dev 라고 설정 되어 있으면 task-definition-dev.json 파일을 읽게 될 것이다.
{
"containerDefinitions": [
{
"name": "[프로젝트 컨테이너 명칭]",
"image": "[AWS 이미지경로]",
"cpu": 0,
"portMappings": [
{
"name": "[컨테이너명칭]-8080-tcp",
"containerPort": 8080,
"hostPort": 8080,
"protocol": "tcp",
"appProtocol": "http"
}
],
"essential": true,
"environment": [
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
{
"name": "SPRING_PROFILES_ACTIVE",
"value": "dev"
},
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
{
"name": "LOGGING_LEVEL_COM_MOMMYDNDN_API",
"value": "DEBUG"
},
{
"name": "SPRING_KAFKA_BOOTSTRAP_SERVERS",
"value": "[redis IP:포트번호]"
},
{
"name": "PUSH_GATEWAY_SERVERS",
"value": "[redis IP:포트번호]"
}
],
"environmentFiles": [],
"mountPoints": [],
"volumesFrom": [],
"ulimits": [],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-create-group": "true",
"awslogs-group": "/ecs/[태스크명칭]",
"awslogs-region": "ap-northeast-2",
"awslogs-stream-prefix": "ecs"
},
"secretOptions": []
}
}
],
"family": "[태스크명칭]",
"executionRoleArn": "arn:aws:iam::[iam번호]:role/ecsTaskExecutionRole",
"networkMode": "awsvpc",
"volumes": [],
"placementConstraints": [],
"requiresCompatibilities": [
"FARGATE"
],
"cpu": "256",
"memory": "512",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"tags": []
}
3-1.배포
배포를 하게 되면 TASK 만 생성되고 ECS 에 서비스가 생성되지 않는 오류가 발생하였다. 이유는 서비스가 없을때 생성하는 로직을 넣는것 자체가 위험하다보니 깃 워크플로우는 ECS에서 서비스를 생성하진 않는 것이었다.

그래서 생성된 TASK로 서비스를 생성하면 원래라면 ECS 배포가 정상적으로 성공했어야 했는데 또 다른 오류가 발생하였다.

📗 해결방법1.
unable to pull secretes or registry auth 라고 검색하니 권한 문제인거 같아서 아래처럼 권한을 추가해보았다. ❌ 실패!
기존 개발 ECS 는 정상적으로 배포 되었으니, 권한 문제는 아니었다.

📗 해결방법 2.
TASK 쪽 로그를 자세히 확인 해보니, 런타임 에러가 있었고 이것을 토대로 검색해 보니 운영으로 추가한 SUBNET 에 대해서 NAT 게이트웨이 설정이 덜 되어 있었다! 😭 이 부분은 초기에 개발 해 놓은 분이 설정을 일부만 해놓아서 찾기 어려웠던 거 같다.
위에서 추가했던 PRD_PUB_SUBNET_B, PRD_PUB_SUBNET_C 각각에 대해서 아래처럼 NAT 게이트웨이가 연결되어 있었어야 했는데 B에 대해서만 연결되어 있고 C에 대해서 연결되어 있지 않아 발생했던 오류였다. ⭕해결!


3-2.spring swagger 운영도메인 확인하기
ECS 에 필요한 프로젝트를 배포 성공하였고, spring swagger 로 API를 확인 하려고 하는데
[도메인]/[서비스명]/v3/api-docs 경로로 접근하면 왼쪽 이미지처럼 json이 응답되어야 하는데 운영도메인으로 접근하면 500에러가 발생하였다.
![]() |
![]() |
3-3. 로드밸런스 확인하기
기존 로드밸런서 설정에서 리스너에는 HTTP:80과 HTTPS:443 이 추가 되어 있었다. 그리고 HTTP:80 포트로 접근 하면 HTTPS:443 으로 리다렉션 해주는 규칙이 추가 되어 있다.
※ 80포트를 443으로 리다렉션 해주어야 한다. 그리고 GATEWAY 프로젝트에서 url 경로를 관리하고 권한을 체크하다보니 배포 할 때 GATEWAY 프로젝트가 필요하고 우선적으로 배포되는 것이 좋다.

HTTPS:443 에는 개발대상그룹 (DEV-GATEWAY-SERVICE) 만 연결되어 있는 상태였다.
아래에 간략한 그림과 함께 설명하자면 현재 ECS 에는 개발, 운영 클러스터로 나뉘어져 있으며 각각 프로젝트가 배포되어 있는 상태이다.
로드밸런서에 연결할 GATEWAY_SERVICE 를 생성 할 때는 로드밸런싱 사용을 체크로 하여 로드밸런서에 연결해주어야 한다.

그리고 대상그룹을 새로 추가한다. 추가한 대상그룹은 EC2 → 로드밸런서 → 대상그룹 에서 확인 해 볼 수 있다.

그리고 배포를 했을 때 대상에 IP가 생성되어 있고 상태가 Healthy 이면 정상적으로 배포 된 것을 확인 할 수 있다.

처음 로드밸런서에 연결을 해야 하는 걸 몰라서 배포하면 대상그룹의 타겟 IP의 상태가 Unhealthy 가 뜨고 배포가 실패 했었다. 😥
그래서 개발과 같이 운영 GATEWAY_SERVICE 를 생성 할 때 로드밸런서를 연결하면서 대상을 추가해주니,
PROD-GATEWAY-SERVICE 대상도 로드밸런서에 정상적으로 연결되었다.
그리고 HTTPS:443 에 운영과 개발의 도메인을 분리시키는 규칙을 추가함으로써 타겟을 분리시켰다.

3-4. spring swagger 운영도메인 다시 확인하기
이렇게만 해주면 swagger 도 연결이 잘 될 줄 알았으나... 계속해서 500에러가 났다..

Chat gpt 와 검색을 통해서 확인해 보라는 사항은 다 확인 해 보았다.
확인사항1. API docs 문제 : [도메인주소]/caring-service/v3/api-docs 호출하여 json 응답이 오는지 확인 ❌
springdoc.api-docs.path 설정값이 "/caring-service/v3/api-docs" 인지 확인.

<WebConfig 설정>
@Configuration
public class WebConfig implements WebMvcConfigurer {
private final LoginUserIdArgumentResolver loginUserIdArgumentResolver;
private final ApiLogInterceptor apiLogInterceptor;
public WebConfig(LoginUserIdArgumentResolver loginUserIdArgumentResolver,
ApiLogInterceptor apiLogInterceptor) {
this.loginUserIdArgumentResolver = loginUserIdArgumentResolver;
this.apiLogInterceptor = apiLogInterceptor;
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(loginUserIdArgumentResolver);
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(apiLogInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/swagger-ui/**",
"/v3/api-docs/**",
"/error",
"/favicon.ico"
);
}
}
확인사항2. [도메인주소]/v3/api-docs/swagger-config 호출 했을 때, json 이 응답되는지 확인 ⭕
이것을 봤을 때, caring-service 명을 못 찾는 오류 인 거 같다는 생각이 번뜩💡 나버렸다!!!
그래서 ECS 에서 확인을 해 보니 구성및네트워킹 > 서비스연결 에서 DNS 를 연결해주지 않아서 발생했던 오류 였다 ㅠㅠ
(3일정도를 고생했던거 같다..)

이렇게 해주니 정상적으로 운영 swagger 를 확인 할 수 있었다!

여전히 AWS 각각의 기능에 대해서 정확히 이해하지는 못하고 처음부터 환경을 구축하라고 한다면 할 수 있을지 자신은 없지만 어느정도 환경이 이렇게 구성되어 있구나를 머릿속으로 그림은 그릴 수 있게 된거 같아 뿌듯하다. ㅎㅎ











