SpringBoot Application + ECS 배포

728x90

ECS를 선택한 이유

최근에 물리치료 스터디를 진행하고있다. 이 스터디를 원활하게 도와줄 수 있는 서비스를 만들고 싶어져서 개인적인 토이프로젝트를 진행하고있다. 엄밀한 의미의 CI / CD는 아니지만 코드가 main 브랜치에 푸쉬됐을 때, 자동으로 배포되는 환경은 만들어두고 싶어 서버 인프라를 구성하게 되었다. 

고전적인 방식으로 EC2 인스턴스를 띄워두고  배포 쉘 스크립트를 작성하여 깃 웹훅을 통해서 메인 브랜치에 푸쉬가 발생했을 때, 쉘 스크립트가 실행되도록 트리거를 방식도 있고 또 Jenkins나 CodeDeploy 툴을 이용한 방식도 있지만 전자처럼 직접 EC2 인스턴스로 접속하여 리눅스환경에서의 작업(리눅스도 공부를 하기는 해야할거 같은데 언제하지... 🫠)을 하는 것이 싫었고 후자는 배포를 위한 별도의 인스턴스를 띄어야하기 때문에 결국 별도의 비용💸이 들어가기 때문에 이 또한 꺼려졌다. 

먼저 Jenkins, CodeDeploy와 같은 툴은 github에서 제공하는 Github Action을 통해서 충분히 대체할 수 있기에 큰 고민없이 Github Action을 사용하기로 결정했다. 그리고 전자의 문제점도 문제지만 한번 인프라 구성을 한 이후에는 별도로 인프라 관리를 할 필요가 없도록 구성하고 싶은 마음이 컸기에 이런 니즈를 충족해줄 서비스를 찾게되었고 AWS의 BeanstalkECS 서비스 이 두 가지로 범위가 좁혀지게되었다. 나는 조금 더 친숙하기도했고, 또한 오케스트레이션을 해주는 ECS가 마음에 들어서 ECS를 선택하게 되었다. 

 

1. SpringBoot Application 준비

예제 코드 저장소: https://github.com/Rok93/physical-therapy

 

GitHub - Rok93/physical-therapy: 물리치료 스터디 관리 API

물리치료 스터디 관리 API. Contribute to Rok93/physical-therapy development by creating an account on GitHub.

github.com

스프링부트 프로젝트에서는 미리 준비해두면 좋을만한 포인트가 있는데, 

1. 배포할 서비스 모듈의 jar 파일 이름을 직접 지정하기

tasks.bootJar {
    enabled = true
    archiveFileName = "app-api.jar"
    mainClass.set("physicaltherapy.AppApiSpringApplicationKt")
}

2. health check API 만들기

@RestController
class HealthCheckController }{

@GetMapping("/health")
fun checkHealth(): String = "OK"

}

 

이정도면 SpringBoot Application은 준비가 끝난 듯하다.
(당연히 로컬에서 실행했을 때, 정상적으로 서버가 뜨는지 정도는 확인해야한다)

2. Github Action 배포 스크립트 작성

배포할 코드 저장소에 들어가면 상단에 Actions 탭이 있고 이 탭을 누르게되면, (아직 만든 workflow가 없다면) 지금과 같은 창이 나온다!
나는 ECS에 배포할 것이기 때문에 ECS라고 검색하여 나오는 confiure를 클릭해준다.

클릭해보면 기본적으로 ECS 배포를 할 때, 필요한 스크립트 틀을 제공해준다. 
나는 (Kotlin) SpringBoot 애플리케이션을 빌드해야하니 Java 설치가 필요하니 JDK 설치 잡을 추가해준다.

템플릿의 상단에 보면 아래와 같은 환경변수가 존재하는데, 이 값들을 추후 생성하는 ECR, ECS, ECS Service, ECS Task 등을 생성하여 이 값을 채워주면 된다. (주석으로 순서를 매겨놨는데 나중에 AWS에서 서비스를 각각 만들고나서 한번에 매칭해줘도 되고, 아니면 지금 미리 이름만 정해두는 것도 괜찮을 것 같다)

env:
  AWS_REGION: ap-northeast-2                  # 서울 REGION 
  ECR_REPOSITORY: MY_ECR_REPOSITORY           # 1.AWS ECR 이름 
  ECS_SERVICE: MY_ECS_SERVICE                 # 5.AWS의 ECS 클러스터의 SERVICE 이름 
  ECS_CLUSTER: MY_ECS_CLUSTER                 # 4.AWS ECS 클러스터 이름
  ECS_TASK_DEFINITION: MY_ECS_TASK_DEFINITION # 3.정의한 ECS TASK 이름
                                               # file, e.g. .aws/task-definition.json
  CONTAINER_NAME: MY_CONTAINER_NAME           # 2.ECR의 컨테이너 이름

(3. ECS_TASK_DEFINITION이 조금 헷갈리는데, 이 부분은 주석 내용을 참고해보면, TASK 이름을 정의하는 것이 아니라 Task를 정의한 json 파일명을 넣어야하는 것으로  보인다. 🤔  이 부분에서 조금 삽질을 해서, 나의 경우에는 ECS_TASK 이름으로 TASK의 task-definition.json 파일을 먼저 다운로드한 뒤, 가져와서 직접 넣어주는 방식으로 우회했다. 

대략적인 배포 흐름을 살펴보면(deploy job의 step 순서) 아래와 같다.

1) github action에 체크아웃 -> 2) aws 접근권한 확인 -> 3) ECR 로그인 -> 4) docker image build & ECR에 push
 ->  5) ECS Task에 push한 image로 채우는 작업 -> 6) 새로 정의한 ECS Task 배포 

아마 단일 모듈(모노 모듈?)인 일반적인 애플리케이션이라면 제공해준 템플릿에서 JDK 설치 부분만 추가하면 배포 스크립트가 다 작성된게 아닐까 싶다.  하지만 나의 경우에는 멀티 모듈 프로젝트이다보니 4번 부분에서 일부 내용을 수정했었다.

멀티모듈 프로젝트의 배포스크립트 예제가 궁금하다면 👉  멀티모듈 프로젝트 배포 스크립트 예제

이 정도면 배포 스크립트 작성도 끝이난 듯하다.

3. AWS ECR 생성

AWS 검색창에서 ECR을 검색해서 private 리포지토리로 이동하고, 우측 상단에 리포지토리 생성 버튼을 누른다.

설정을 해준다.
(태그 변경 불가능 기능은 초기 배포설정할 때는 활성화하지 말자... 첫 배포환경 구축할 때는 동일한 태그로 배포할 일이 생각보다 많았었는데, 괜히 이 설정을 ON 해둬서, 다시 풀어줘야했던 귀찮은 상황이있었다)

4. ECS Task 생성

AWS 검색 창에서 ECS Task 로 검색한다.
작업 정의 목록으로 들어간다.

새 태스크 정의 생성을 한다.

설정을 추가해준다. 
Task의 이름을 입력해주고, 태스크의 역할(= 없음) 및 태스크 실행 역할(ecaTaskExecutionRole)을 지정해준다.

ECS Service가 최초 배포시 배포한 태스크가 정상적으로 실행되지 않는다면, 몇번 더 배포를 시도하다가 자연스럽게 Service가 중지된다. (여러번의 삽질끝에 알게됐다. 🫠)

우리는 아직 우리가 만든 스프링 애플리케이션을 도커 이미지로 만들어서 ECR에 푸쉬하지 않았다 그래서 일단 돌아가는 어떤 도커 이미지든지 간에 태스크에 등록해서 ECS Service 생성 시, 이 태스크를 오케스트레이션 할 때, 정상적으로 배포가 완료되도록 해야만 한다. (그래야 ECS Service가 계속해서 유지된다)

ECR에 수동으로 도커 이미지를 빌드&푸시하는 방법은 아래와 같다.
(ECR의 특정 리포지토리로 들어가면 우측 상단에 푸시 명령 보기 버튼을 누르면, 본인 리포지토리에 맞는 명령어를 아래와 같이 알려준다!   하나하나 타이핑하지말고 복붙하자 😆)

5. ECS Cluster 생성

AWS 검색 창에서 ECS를 검색하여 Elastic Container Service 서비스로 접근한다.

클러스터 생성 버튼을 누른다.

설정을 추가해준다. 
클러스터 이름(배포스크립트 환경변수 4번)과 VPC(VPC는 첫번째것만 남겨놨다) 설정을 해준다.
(인프라는 fargate로 설정한다  ⚠️ 요금이 부과된다)

6. ECS Service 생성

주의할점: ECS Service가 계속 유지될 수 있도록 꼭 살려야한다!

위의 내용을 몰라서 삽질을 많이했었다. 
제대로 배포가 되지 않으면 서비스가 자동으로 중단된다. 
꼭 pull 받았을 때, 정상적으로 실행되는 도커 이미지를 태스크에 지정해줘야한다. 

5에서 생성한 ECS Cluster에 들어가면 하단에 서비스 생성을 할 수 있는 버튼이 있다. 눌러주자!

ECS Service 설정을 해준다.

보안 그룹 설정

보안 그룹쪽은 나도 블로그 글들을 참고해보면서, 따라한 부분인데 이 부분은 잘 모르고 설정했다. 
보안쪽은 사실 잘 모르겠어서, 혹시나 이 글을 따라서 배포를 하고 계신분이 계시다면 이 부분은 꼭 의심과 함께 더 알아보고 진행하면 좋을 것 같다. 

AWS 공식문서에서 ECS는 Anywhere가 고급 보안 기능을 갖추고 있다고 하는데.... 위처럼 설정하는게 고급 보안이 되고 있는지는 확실치 않다. 

이제 로드밸런서 설정해줄건데, 로드밸런서를 설정하는 이유는 깃헙 액션의 플로우를 통해서 배포가 일어날 때마다 매번 신규 태스크가 뜨게되는데, 그 때마다 인스턴스의 host 주소가 계속 바뀌는 문제가 있었다. 탄련적 IP를 이용해서 배포되는 IP를 고정시키려고했는데, 공식문서에는 아래와 같이 말하고있다.

고정 IP 주소 또는 탄력적 IP 주소는 Fargate 태스크에 직접 추가할 수 없습니다. Fargate 태스크에서 고정 IP 또는 탄력적 IP를 사용하려면 먼저 Network Load Balancer를 사용하여 Fargate 서비스를 생성해야 합니다. 그런 다음, 태스크의 탄력적 IP 주소를 로드 밸런서에 연결할 수 있습니다.

따라서 로드밸런서를 생성해서 설정할 것이다. 
헌데, 나의 경우에는 ECS Service를 생성하는 창에서 새 로드밸런서 등록을 하니까 오류가 발생했다.
에러로그도 매우 불친절해서 원인을 알 수가 없었다.

결국 로드밸런서를 직접 생성해주고, 기존 로드밸런서를 가지고와서 설정하는 방식으로 우회했다.
AWS 검색창에서 EC2 콘솔로 이동하면 좌측 메뉴에 로드 밸런싱 탭이 있다.

먼저 대상 그룹부터 먼저 생성한다.

이 때, 별 다른 설정은 필요없지만 대상 유형 선택에서 IP 주소를 선택하는 것이 중요하다.

그 이유는 아래와 같다.

⚠️ 중요
서비스 태스크의 정의가 awsvpc 네트워크 모드(Fargate 시작 유형에 필수)를 사용할 경우 대상 유형으로 IP 주소(IP addresses)를 선택해야 합니다. awsvpc 네트워크 모드를 사용하는 태스크는 Amazon EC2 인스턴스가 아닌 탄력적 네트워크 인터페이스와 연결되기 때문입니다.

출처: https://docs.aws.amazon.com/ko_kr/AmazonECS/latest/userguide/create-application-load-balancer.html

참고로 fargate를 사용하면 네트워크 모드가 자동으로 awsvpc로 지정되기 때문에 이 설정이 중요하다.
(설정하지 않으면 서비스 생성 오류가 발생한다)

대상그룹 설정은 끝났고 이어서 좌측 메뉴에서 로드밸런서 탭으로 들어가서 로드 밸런서를 생성한다.

그림상으로는 하나만 픽했지만 최소 2개를 지정해야하니, 적당히 나머지 하나도 지정하면 된다.

포트는 80 으로 그대로 두고, 대상 그룹은 방금전 단계에서 생성했던 대상 그룹을 지정한다.

이렇게 설정이 끝나고 나면, 서비스를 생성한다. 

정상적으로 배포된 상태라면 서비스 상태는 위와 같아야한다.

서비스를 클릭해서 들어간 뒤, 태스크 탭을 눌러보면 현재 배포된 태스크를 확인할 수 있다. 
정상적으로 배포된 상태이고 health check 응답도 잘 떨어졌다면, 태스크 상태는 '정상'으로 나와야한다.

ECS 서비스의 네트워크 탭을 누르면 연결된 로드밸런서의 DNS 주소를 알 수 있다.

로드밸런서 DNS로 health check API를 요청해보니, 응답이 잘 떨어지는 것을 확인했다.

마치며...

인프라는 역시 어렵다. 인프라는 나한테 너무 막연하게 느껴지는 분야다보니, 인프라 초기 설정할 때는 겁이 덜컥 나는 편인데, 이번에 삽질하면서 조금은 친숙해진 듯하다. 다음에 또 인프라 작업할 일 있으면, 시간이 좀 걸려서그렇지 어떻게든 할 수는 있겠다는 자신감도 조금 생겼다. 

728x90

'Infra' 카테고리의 다른 글

쿠버네티스 소개  (0) 2022.12.03
리눅스 환경구성 기초  (0) 2020.10.22