Docker를 타고 EC2에서 ECS로

 배경

인프라의 변천사

위 사진은 초기 인프라의 모습입니다.
3가지 서비스가 각각 하나의 private한 EC2 Instance로 Bastion을 경유하여 접근하는 단순한 모습이었습니다.
이 구조의 문제점은 live 서버와 dev 서버가 같은 EC2 안에서 실행되고 있고 RDS 또한 하나의 RDS가 모든 서버의 요청을 받고 있다는 것이었습니다.
위 모습은 Github Labels와 함께하는 CI/CD 리팩토링 을 진행하면서
우선적으로 live서버와 dev서버의 Instance만이라도 분리하기 위해 분리한 모습으로 Blue/Green 배포를 위해 각 Instance를 AutoScalingGroup으로 지정하였습니다.
하지만 백엔드, 인프라, DB를 모두 저 혼자서 관리하고 있었던 상황에서 늘어난 EC2를 관리하기가 부담스러웠고
네트워크에 관한 지식도 많지 않았기 때문에 서버를 자체적으로 관리하는 것에 불안감을 느꼈습니다.
또한 micro와 small 타입의 EC2를 사용하였지만 트래픽이 많지 않았기 때문에 낭비되는 컴퓨팅 리소스가 많다고 생각했고
Web-3-Tier 구조를 가져가기 위해 nginx 같은 third party module들을 고려하다보니 Docker와 같은 컨테이너 기술에도 관심을 가지기 시작했습니다.

 왜 ECS인가?

우선 ECS를 사용하는 이유를 유연성, 확장성, 비용 효율성, 보안성 4가지 측면에서 찾았습니다.
유연성 : 기본 인프라를 관리할 필요 없이 작업 정의서에 의해 적합한 배포를 할 수 있습니다.
확장성 : 필요에 따라 컨테이너를 신속하게 추가하거나 제거할 수 있으며 컨테이너화된 애플리케이션을 쉽게 확장할 수 있습니다.
비용 효율성 : 작업 정의서에 의해 인스턴스의 리소스를 빠르게 확장 또는 축소할 수 있고 사용한 리소스에 대해서만 비용을 지불하면 됩니다.
보안성 : AWS의 VPC를 사용하므로 어느정도 보안적인 측면에서 보장을 받을 수 있습니다.

ECS란?

ECS는 AWS에서 제공하는 관리형 컨테이너 오케스트레이션 서비스로 Docker 컨테이너 및 AWS 인프라를 사용하여 컨테이너화된 애플리케이션을 실행, 관리 및 확장할 수 있습니다.
ECR : 도커 이미지들 repository 개념으로 docker build로 만든 image를 push해서 올림
task-definition : 컨테이너들을 집단적으로 관리할 수 있는 작업정의서 개념
Cluster/Service : Cluster는 논리적 집합 Service는 Instance의 집합
Instance : EC2 또는 Fargate로 AutoScaling을 설정하여 트래픽에 대한 인스턴스 크기 조절 가능하며 하위에 여러 개의 Task가 존재할 수 있음
Task : 하위에 여러 개의 컨테이너가 존재할 수 있고 task-definition에 정의

Docker란?

Docker는 컨테이너에서 애플리케이션을 생성, 배포 및 실행하기 위한 플랫폼입니다.
컨테이너는 모든 종속성, 라이브러리 및 구성 파일과 함께 애플리케이션을 번들로 제공하는 경량의 독립 실행형 휴대용 환경으로, 다양한 환경에서 일관된 동작을 보장합니다.
전반적으로 Docker는 개발자가 애플리케이션을 더 빠르고 효율적으로 빌드, 배송 및 실행하는 동시에 공동 작업, 생산성 및 확장성을 개선할 수 있는 강력하고 유연한 플랫폼입니다.
추가적인 학습 내용은 Docker 페이지로 대체하겠습니다.

 AWS 설정

ECS는 ECR이라는 Docker Image Repository에 존재하는 Image를 task-definition이라는 작업 정의서를 통해 컨테이너화하여 실행합니다.
먼저 SpringBoot의 Docker Image를 만들기 위한 Dockerfile을 프로젝트 내에 작성하겠습니다.
FROM amazoncorretto:17 VOLUME /tmp ENV JAR_FILE="<JAR_FILE>" ENV APM_FILE="agents/elastic-apm-agent-1.35.0.jar" ENV APM_OPTS="<APM_OPTS>" ENV JAVA_OPTS="<JAVA_OPTS>" COPY ${JAR_FILE} gocho-api.jar COPY ${APM_FILE} elastic-apm-agent-1.35.0.jar EXPOSE 8080 ENTRYPOINT java ${APM_OPTS} ${JAVA_OPTS} -jar /gocho-api.jar
Java
복사
docker build하는 환경은 Github Action의 가상 환경입니다.
여러 서비스나 서버에 대한 배포 파이프라인을 모두 포용하기 위해 가변 변수들은 ‘<>’로 표시한 후 Github Action의 배포 스크립트에서 주입할 것입니다.
컨테이너의 포트는 8080으로 열어주고 ENTRYPOINT를 이용해 spring-boot를 실행시켜줍니다.
그 다음엔 Dockerfile을 통해 만들어진 Image를 docker run 하기 위해 task-definition.json을 작성합니다.
AWS의 콘솔을 이용하면 더 쉽게 작성할 수 있습니다.
아래는 json 형태의 task-definition이고 프로젝트 내에 작성해줍니다.
{ "containerDefinitions": [ { "name": "spring-boot", "image": "<IMAGE>", "portMappings": [ { "name": "spring-boot-8080-tcp", "containerPort": 8080, "hostPort": 8080, "protocol": "tcp", "appProtocol": "http" } ], "essential": true, "environment": [], "environmentFiles": [], "mountPoints": [], "volumesFrom": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-create-group": "true", "awslogs-group": "/ecs/task-definition", "awslogs-region": "ap-northeast-2", "awslogs-stream-prefix": "ecs" } } } ], "family": "task-definition", "taskRoleArn": "arn:aws:iam::661851899694:role/ec2-codedeploy-role", "executionRoleArn": "arn:aws:iam::661851899694:role/ecsTaskExecutionRole", "networkMode": "awsvpc", "volumes": [], "placementConstraints": [], "cpu": "<CPU>", "memory": "<MEMORY>", "runtimePlatform": { "cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX" }, "tags": [] }
Java
복사
여기서도 마찬가지로 ECR에 등록된 Image의 URI나 Instance의 CPU나 MEMORY 같이 가변적인 값은 ‘<>’로 표시한 후 Github Action의 배포 스크립트에서 주입할 것입니다.
taskRoleArn은 Instance가 S3와 같은 다른 서비스들과 원활하게 통신할 수 있는 역할을 설정해주면 됩니다.
networkMode는 fargate를 사용할 것이기 때문에 awsvpc만을 지원합니다.
마지막으로 Linux의 아키텍처가 다르면 multi image builder를 통해 빌드를 해주어야 하기 때문에 신경써서 설정해주어야합니다.
이제 AWS ECS 콘솔에서 클러스터를 생성해줍니다.
클러스터는 Instance 들의 논리적인 그룹 단위로 저는 각 서비스의 live서버와 dev서버의 Instance를 묶어 하나의 클러스터로 지정하였습니다.
네트워크는 기존에 사용하던 VPC를 선택해주었고 서브넷은 API 서버가 있는 private 서브넷 2개를 선택하였습니다. (서브넷은 가용성을 위해 여러 개를 선택해주는 것이 좋습니다.)
마지막으로 인프라는 EC2가 아닌 Fargate를 선택해 서버리스로 만들어지게끔 설정하였습니다.
이제 클러스터 안에 서비스를 만들어보겠습니다.
서비스는 live서버와 dev서버 각각 하나씩 만들었습니다.
컴퓨팅 옵션에서 시작 유형을 선택한 후 Fargate를 선택해줍니다.
용량 공급자를 선택하면 태스크가 여러 용량 공급자에 의해 분산되어 실행됩니다.
배포 구성은 위 사진처럼 설정하였습니다.
항상 서버가 실행되어야 하기 때문에 태스크가 아닌 서비스로 선택하였고 아래에선 작업 정의서를 지정해줍니다.
원하는 태스크의 수를 설정해주면 작업 정의서의 작업이 설정한 갯수만큼의 인스턴스로 실행됩니다.
그 후 네트워크 설정은 클러스터에 설정한거처럼 기존의 VPC와 서브넷을 설정해주고 보안 그룹 또한 기존에 사용하던 API 서버의 보안 그룹을 그대로 사용하였습니다.
이 때 퍼블릭 IP를 꺼주어야 ALB만을 통한 접근이 가능하게 할 수 있습니다.
로드 밸런서 또한 기존의 사용하던 것을 설정해주고 해당 로드 밸런서가 바라보고 있을 컨테이너를 설정해줍니다.
대상 그룹을 설정해주면 로드 밸런서는 해당 대상 그룹을 통해 컨테이너에 대한 트래픽을 조정합니다.
SpringBoot는 실행되는데 어느정도의 시간이 걸리기 때문에 상태 검사 유예 기간은 적당히 늘려줍니다.
이렇게 서비스를 생성하게 되면 ECS가 자동으로 태스크들을 서비스 내에 작업 정의서대로 실행하며
ALB는 대상 그룹에 해당 컨테이너를 잡아주고 트래픽을 라우팅해줍니다.

 CI/CD 변경

우선 ECS의 배포 방식은 아래와 같습니다.
1.
Github Action 에서 테스트
2.
자바 빌드
3.
도커 이미지 빌드
4.
ECR로 이미지 푸시
5.
작업정의서 리렌더링
6.
배포 요청
ECS는 롤링 업데이트 방식으로 서비스에 새로운 태스크를 배포한 후
새로운 태스크에 대한 ALB의 HealthCheck가 정상적으로 끝나면 새로운 태스크로 트래픽을 라우팅합니다.
기존의 Github Action 배포 스크립트는 아래와 같습니다.
name: Deploy WorkFlow on: pull_request: types: - closed branches: - 'live' - 'dev' env: SERVICE: SERVER: jobs: Deploy-Job: if: github.event.action == 'closed' && github.event.pull_request.merged == true runs-on: ubuntu-latest steps: # Git Checkout - name: Git Checkout uses: actions/checkout@v3 # Set Environment Variables - name: Set Environment run: | sudo timedatectl set-timezone 'Asia/Seoul' echo "SERVICE=$(echo ${{ github.event.pull_request.labels[0].name }})" >> $GITHUB_ENV echo "SERVER=$(echo ${{ github.base_ref }})" >> $GITHUB_ENV # Set JDK - name: Set JDK uses: actions/setup-java@v3 with: java-version: '17' distribution: 'corretto' # Copy Secrets from Github Secrets - name: Copy Secrets run: | mkdir -p be/module-user-api/src/main/resources && echo "${{ secrets.USER_PROPERTIES }}" > be/module-user-api/src/main/resources/application.yml mkdir -p be/module-business-api/src/main/resources && echo "${{ secrets.BUSINESS_PROPERTIES }}" > be/module-business-api/src/main/resources/application.yml mkdir -p be/module-admin-api/src/main/resources && echo "${{ secrets.ADMIN_PROPERTIES }}" > be/module-admin-api/src/main/resources/application.yml mkdir -p be/module-user-api/src/main/resources/key && echo "${{ secrets.APPLE_PRIVATE_KEY }}" > be/module-user-api/src/main/resources/key/ApplePrivateKey.p8 mkdir -p be/module-business-api/src/main/resources/key && echo "${{ secrets.APPLE_PRIVATE_KEY }}" > be/module-business-api/src/main/resources/key/ApplePrivateKey.p8 mkdir -p be/module-admin-api/src/main/resources/key && echo "${{ secrets.APPLE_PRIVATE_KEY }}" > be/module-admin-api/src/main/resources/key/ApplePrivateKey.p8 # Grant execute permission to Gradle - name: Grant execute permission for Gradle working-directory: be run: chmod +x ./gradlew # Build with Gradle - name: Run build with Gradle working-directory: be run: ./gradlew clean build -x test #AWS 인증 - 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: ap-northeast-2 # deploy 폴더에 배포에 필요한 파일 복사 후 S3 업로드 - name: Zip And Upload to S3 if: ${{ env.SERVICE != 'all' }} run: | mkdir be/${{ env.SERVICE }}-deploy cp be/module-${{ env.SERVICE }}-api/build/libs/*.jar be/${{ env.SERVICE }}-deploy cp be/appspec.yml be/${{ env.SERVICE }}-deploy cp be/scripts/${{ env.SERVER }}-deploy.sh be/${{ env.SERVICE }}-deploy/deploy.sh cp be/agents/*.jar be/${{ env.SERVICE }}-deploy sed -i 's/replace/module-${{ env.SERVICE }}-api-0.0.1-SNAPSHOT.jar/g' be/${{ env.SERVICE }}-deploy/deploy.sh zip -r -qq -j be/${{ env.SERVICE }}-deploy.zip be/${{ env.SERVICE }}-deploy aws s3 cp --region ap-northeast-2 be/${{ env.SERVICE }}-deploy.zip s3://gocho-deploy/${{ env.SERVICE }}-deploy.zip # 모든 모듈 deploy 폴더에 배포에 필요한 파일 복사 후 S3 업로드 - name: All Modules Zip And Upload to S3 if: ${{ env.SERVICE == 'all' }} run: | mkdir be/user-deploy cp be/module-user-api/build/libs/*.jar be/user-deploy/ cp be/appspec.yml be/user-deploy/ cp be/scripts/${{ env.SERVER }}-deploy.sh be/user-deploy/deploy.sh cp be/agents/*.jar be/user-deploy/ sed -i 's/replace/module-user-api-0.0.1-SNAPSHOT.jar/g' be/user-deploy/deploy.sh zip -r -qq -j be/user-deploy.zip be/user-deploy aws s3 cp --region ap-northeast-2 be/user-deploy.zip s3://gocho-deploy/user-deploy.zip mkdir be/business-deploy cp be/module-business-api/build/libs/*.jar be/business-deploy/ cp be/appspec.yml be/business-deploy/ cp be/scripts/${{ env.SERVER }}-deploy.sh be/business-deploy/deploy.sh cp be/agents/*.jar be/business-deploy/ sed -i 's/replace/module-business-api-0.0.1-SNAPSHOT.jar/g' be/business-deploy/deploy.sh zip -r -qq -j be/business-deploy.zip be/business-deploy aws s3 cp --region ap-northeast-2 be/business-deploy.zip s3://gocho-deploy/business-deploy.zip mkdir be/admin-deploy cp be/module-admin-api/build/libs/*.jar be/admin-deploy/ cp be/appspec.yml be/admin-deploy/ cp be/scripts/${{ env.SERVER }}-deploy.sh be/admin-deploy/deploy.sh cp be/agents/*.jar be/admin-deploy/ sed -i 's/replace/module-admin-api-0.0.1-SNAPSHOT.jar/g' be/admin-deploy/deploy.sh zip -r -qq -j be/admin-deploy.zip be/admin-deploy aws s3 cp --region ap-northeast-2 be/admin-deploy.zip s3://gocho-deploy/admin-deploy.zip # Deploy - name: Deploy if: ${{ env.SERVICE != 'all' }} env: aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_KEY }} run: | aws deploy create-deployment \ --application-name Gocho_BE_Spring_Server_CI_CD \ --deployment-group-name Gocho_BE_${{ env.SERVICE }}_${{ env.SERVER }}_Deploy \ --file-exists-behavior OVERWRITE \ --s3-location bucket=gocho-deploy,bundleType=zip,key=${{ env.SERVICE }}-deploy.zip \ --region ap-northeast-2 # Deploy All Module - name: Deploy All Module if: ${{ env.SERVICE == 'all' }} env: aws-access-key-id: ${{ secrets.AWS_S3_ACCESS_KEY }} aws-secret-access-key: ${{ secrets.AWS_S3_SECRET_KEY }} run: | aws deploy create-deployment \ --application-name Gocho_BE_Spring_Server_CI_CD \ --deployment-group-name Gocho_BE_user_${{ env.SERVER }}_Deploy \ --file-exists-behavior OVERWRITE \ --s3-location bucket=gocho-deploy,bundleType=zip,key=user-deploy.zip \ --region ap-northeast-2 aws deploy create-deployment \ --application-name Gocho_BE_Spring_Server_CI_CD \ --deployment-group-name Gocho_BE_business_${{ env.SERVER }}_Deploy \ --file-exists-behavior OVERWRITE \ --s3-location bucket=gocho-deploy,bundleType=zip,key=business-deploy.zip \ --region ap-northeast-2 aws deploy create-deployment \ --application-name Gocho_BE_Spring_Server_CI_CD \ --deployment-group-name Gocho_BE_admin_${{ env.SERVER }}_Deploy \ --file-exists-behavior OVERWRITE \ --s3-location bucket=gocho-deploy,bundleType=zip,key=admin-deploy.zip \ --region ap-northeast-2 # 배포 결과 슬랙 알람 - name: Slack Alarm uses: 8398a7/action-slack@v3 if: always() with: status: ${{ job.status }} author_name: Github Action Deploy fields: repo,message,commit,author,action,eventName,ref,workflow,job,took,pullRequest env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
YAML
복사
아래의 몇가지 작업을 해주어야합니다.
jar 빌드 후 docker image 빌드
S3로 업로드 → ECR에 업로드로 변경
task-definition 렌더링
CodeDeploy에 배포 요청 → ECS에 배포 요청으로 변경
# Set Environment Variables - name: Set Environment run: | sudo timedatectl set-timezone 'Asia/Seoul' echo "SERVICE=$(echo ${{ github.event.pull_request.labels[0].name }})" >> $GITHUB_ENV echo "SERVER=$(echo ${{ github.base_ref }})" >> $GITHUB_ENV if [ ${{ github.base_ref }} == "live" ]; then echo "CPU=1024" >> $GITHUB_ENV echo "MEMORY=2048" >> $GITHUB_ENV else echo "CPU=512" >> $GITHUB_ENV echo "MEMORY=2048" >> $GITHUB_ENV fi
YAML
복사
위 코드는 CPU와 MEMORY에 대한 값을 변수로 등록해주는 부분입니다.
#Inject Variables in DockerFile - name: Inject Variables in DockerFile working-directory: be/deploy run: | sed "s|<JAR_FILE>|$(echo module-${{ env.SERVICE }}-api/build/libs/module-${{ env.SERVICE }}-api-0.0.1-SNAPSHOT.jar)|g" Dockerfile-app > Dockerfile-app-new mv Dockerfile-app-new Dockerfile-app sed "s|<JAVA_OPTS>|$(echo -Duser.timezone=Asia/Seoul -Dspring.profiles.active=${{ env.SERVER }})|g" Dockerfile-app > Dockerfile-app-new mv Dockerfile-app-new Dockerfile-app if [ ${{ env.SERVER }} == "live" ]; then sed "s|<APM_OPTS>|$(echo -javaagent:/elastic-apm-agent-1.35.0.jar -Delastic.apm.service_name=gocho-${{ env.SERVICE }} -Delastic.apm.server_urls=https://apm.gocho-back.com -Delastic.apm.application_packages=com.gocho.be)|g" Dockerfile-app > Dockerfile-app-new mv Dockerfile-app-new Dockerfile-app else sed "s|<APM_OPTS>||g" Dockerfile-app > Dockerfile-app-new mv Dockerfile-app-new Dockerfile-app fi
YAML
복사
위 코드는 Dockerfile에 ‘<>’로 표시했던 가변 변수에 값을 주입해주는 코드입니다.
sed 명령어를 통해 파일 내의 텍스트를 대체해주는 방식을 선택하였습니다.
#Authenticate to Amazon ECR - name: Login to Amazon ECR uses: aws-actions/amazon-ecr-login@v1 id: login-ecr #Build, tag, and push Docker App image to Amazon ECR - name: Build, tag, and push App Image to Amazon ECR id: build-app-image working-directory: be env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: gocho-ecr IMAGE_TAG: gocho-${{ env.SERVICE }}-api-${{ env.SERVER }} run: | docker build --platform linux/arm64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f deploy/Dockerfile-app . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
YAML
복사
위 코드는 ECR 인증을 받은 후 docker image를 빌드하고 ECR에 push하는 코드입니다.
build-app-image 스텝의 output으로는 push한 docker image의 URI가 반환됩니다.
#Inject Variables in the Amazon ECS Task Definition - name: Inject Variables in the Amazon ECS Task Definition working-directory: be/deploy run: | jq '.containerDefinitions[0].image |= "${{ steps.build-nginx-image.outputs.image }}"' task-definition.json > task-definition-new.json mv task-definition-new.json task-definition.json jq '.containerDefinitions[1].image |= "${{ steps.build-app-image.outputs.image }}"' task-definition.json > task-definition-new.json mv task-definition-new.json task-definition.json jq '.cpu |= "${{ env.CPU }}"' task-definition.json > task-definition-new.json mv task-definition-new.json task-definition.json jq '.memory |= "${{ env.MEMORY }}"' task-definition.json > task-definition-new.json mv task-definition-new.json task-definition.json #Deploy to the Amazon ECS - name: Deploy Amazon ECS task definition uses: aws-actions/amazon-ecs-deploy-task-definition@v1 with: task-definition: /home/runner/work/BE_GOCHO_MANAGER_JAVA/BE_GOCHO_MANAGER_JAVA/be/deploy/task-definition.json service: gocho_${{ env.SERVICE }}_service_${{ env.SERVER }} cluster: gocho_${{ env.SERVICE }}_cluster wait-for-service-stability: true
YAML
복사
이제 task-definition에 ‘<>’로 표시했던 부분에 값을 주입해준 후 ECS에 배포 요청을 하게 됩니다.
이번엔 sed가 아닌 jq를 사용해 값을 바꿔줬으며 입맛에 따라 사용하시면 될 것 같습니다.

 추가로 Nginx 붙이기

Nginx는 싱글 스레드로 이벤트 기반으로 비동기 처리를 하기 때문에 WAS(Tomcat) 앞에 붙이게 된다면 리소스를 좀 더 효율적으로 사용할 수 있기 때문에 사용하게 되었습니다.
그 외에도 Reverse Proxy Server의 역할로서 실제 서버에 대한 캡슐화를 할 수 있다는 장점이 있었습니다.
자세한 학습 내용은 Nginx 로 대체하겠습니다.
Nginx도 하나의 컨테이너이기 때문에 Dockerfile을 작성해줍니다.
FROM nginx VOLUME /tmp COPY deploy/nginx.conf /etc/nginx/nginx.conf EXPOSE 80 EXPOSE 443 ENTRYPOINT nginx
YAML
복사
nginx의 설정 파일인 nginx.conf를 COPY하여 옮겨주고 80, 443 포트를 열어줍니다.
아래는 nginx.conf에 대한 설명입니다.
#유저, 권한 설정 user nginx; #worker process의 수 설정 worker_processes auto; #daemon 설정 daemon off; #error log 경로와 master process의 경로 설정 error_log /var/log/nginx/error.log notice; pid /var/run/nginx.pid; #네트워크 작동환경 설정 events { #한 개의 worker process가 동시에 처리할 수 있는 connection의 수 worker_connections 1024; } #http 관련 설정 http { #mime.types 파일 불러오기 include /etc/nginx/mime.types; #기본 타입 설정 default_type application/octet-stream; #엑세스 로그 포맷 설정 log_format main '$remote_addr - $remote_user [$time_local] "$request" ' '$status $body_bytes_sent "$http_referer" ' '"$http_user_agent" "$http_x_forwarded_for"'; #엑세스 로그 경로 설정 access_log /var/log/nginx/access.log main; #sendfile api 설정 sendfile on; #타임아웃 시간 설정 keepalive_timeout 65; #nginx 버전 가리기 server_tokens off; #호스트 설정 server { #http 요청을 받을 포트 설정(default_server는 모든 서버로부터 요청 받음) listen 80 default_server; #IPv6에 대한 설정 listen [::]:80 default_server; #요청을 받을 도메인 이름 설정 server_name _; #URI 매핑 설정 location / { # Reject requests with unsupported HTTP method if ($request_method !~ ^(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)$) { return 405; } proxy_pass http://localhost:8080; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Host $server_name; proxy_set_header X-Forwarded-Proto $scheme; } } }
YAML
복사
worker_processes는 하나의 master process가 관리하는 여러 개의 worker process의 갯수로 auto로 지정하면 nginx가 적절한 값으로 설정해줍니다.
worker_processes * worker_connecitons 로 동시 접속자 수를 정할 수 있습니다.
저는 ECS의 fargate를 사용하여서 networkMode가 강제로 awsvpc로 설정되었기 때문에
컨테이너 간의 연결을 할 때 컨테이너의 이름으로 지정하면 인식하지 못하는 이슈가 있었습니다.
그렇기 때문에 proxy_pass를 localhost로 설정한 후 포트 매핑을 통해 사용하였는데
이렇게 사용하게되면 Instance 하나에 Nginx + SpringBoot 가 세트로 무조건 1개씩 있을거고 비효율적일 것입니다.
이 부분은 추후에 사용하는 Instance가 많아진다면 Nginx만을 따로 태스크로 떼어내서 사용하는 구조로 변경해야 할 것입니다.
이제 컨테이너가 하나 더 늘었기 때문에 task-definition도 수정해줍니다.
{ "containerDefinitions": [ { "name": "nginx", "image": "<IMAGE>", "portMappings": [ { "name": "nginx-80-tcp", "containerPort": 80, "hostPort": 80, "protocol": "tcp", "appProtocol": "http" } ], "essential": true, "environment": [], "environmentFiles": [], "mountPoints": [], "volumesFrom": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-create-group": "true", "awslogs-group": "/ecs/task-definition", "awslogs-region": "ap-northeast-2", "awslogs-stream-prefix": "ecs" } } }, { "name": "spring-boot", "image": "<IMAGE>", "portMappings": [ { "name": "spring-boot-8080-tcp", "containerPort": 8080, "hostPort": 8080, "protocol": "tcp", "appProtocol": "http" } ], "essential": true, "environment": [], "environmentFiles": [], "mountPoints": [], "volumesFrom": [], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-create-group": "true", "awslogs-group": "/ecs/task-definition", "awslogs-region": "ap-northeast-2", "awslogs-stream-prefix": "ecs" } } } ], "family": "task-definition", "taskRoleArn": "arn:aws:iam::661851899694:role/ec2-codedeploy-role", "executionRoleArn": "arn:aws:iam::661851899694:role/ecsTaskExecutionRole", "networkMode": "awsvpc", "volumes": [], "placementConstraints": [], "cpu": "<CPU>", "memory": "<MEMORY>", "runtimePlatform": { "cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX" }, "tags": [] }
YAML
복사
containerDefinitions 이 부분에 nginx 컨테이너에 대한 정보만 추가되었습니다.
또한 Github Action의 배포 스크립트에도 nginx의 이미지를 빌드하고 푸쉬하는 코드를 추가합니다.
#Build, tag, and push Docker Nginx image to Amazon ECR - name: Build, tag, and push Nginx Image to Amazon ECR id: build-nginx-image working-directory: be env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: gocho-ecr IMAGE_TAG: nginx run: | docker build --platform linux/arm64 -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG -f deploy/Dockerfile-nginx . docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
YAML
복사

 결과물

이로써 EC2에서 ECS로의 이사가 완료되고 인프라는 위와 같은 구조가 되었습니다.