Skip to main content Link Menu Expand (external link) Document Search Copy Copied

created at 2024-01-29

Table of contents

  1. 배포 자동화
  2. 서비스 관리 자동화
  3. 자동화 시 발생한 이슈들과 해결책
    1. AWS-ECR token 갱신 스크립트 이슈
    2. AWS-EKS deployment image 버전 이슈
    3. AWS-ECR 관리 이슈
    4. Kubernetes HPA 이슈
    5. Kubernetes 파드 몰림 이슈
    6. Kubernetes 파드 요청 실패 이슈
    7. 모든 노드가 연쇄적으로 다 죽는 이슈
    8. EC2 파드 갯수 제한 이슈
    9. Git Actions 단일 Job 시 원하는 step 을 re-run 할 수 없는 이슈

지금까지 자동화한 부분에 대해 정리가 필요해서 본 포스팅을 작성합니다.

결론부터 말씀드리면, git main 브랜치에 병합되면 자동으로 신버전 태깅되어 여러 워커노드에 deploy 됩니다. 그리고 여러 Job 들의 결과를 slack 채널로 반환받게 됩니다. 이후 각 서비스 pod 들은 새로운 버전으로 롤링 업데이트 됩니다. 이 과정에서 서비스가 중단되지 않습니다. 이 모든 과정은 버튼 하나만 누르면 자동으로 수행됩니다. 업데이트 이후 트래픽 증가시 자동으로 워커노드, 파드가 확장됩니다! 트래픽이 줄어들면 그에 맞춰 파드가 줄어들도록 구현하였습니다.

제가 배포, 서비스 관리 자동화에 사용한 기술들은 아래와 같습니다.

  • Git Actions
  • AWS-EKS(Kubernetes)
  • AWS-ECR
  • AWS-IAM
  • AWS-CloudWatch
  • AWS-AutoScaling-Group
  • Shell Script
  • Gradle Script
  • Docker compose
  • Slack Alert

배포 자동화

자 그러면 먼저 배포 자동화를 그림으로 보시죠! img

이제 각 Job 들을 살펴볼거에요.

  1. Push new tag

    먼저 가장 최근에 푸시된 태그를 가져옵니다. 위의 그림에서는 v5.3.24-5e15f0a 를 가져왔습니다. 그리고 PR 제목에 따라 어떻게 버전을 올려서 태깅할 지 정하는데요. 예를 들어 PR 제목에 [major] 새로운 기능 추가 라고 적혀있다면, v6.0.0-{commit_hash} 버전을 올리고, [patch] 이라고 적혀있다면 v5.3.25-{commit_hash} 버전으로 올립니다. 이후엔 main branch 에 새롭게 업데이트된 버전으로 태깅합니다!

  2. Push image to AWS-ECR

    프로젝트 모듈빌드 시 Gradle script 를 통해 모든 도커파일과 .jar 파일이 자동으로 만들어집니다. 그리고 docker compose 를 통해 7개의 이미지를 빌드합니다. 이 때 이미지 이름은 main_service-{service_name}_{version} 으로 설정합니다. 이렇게 하면 Git tag 와 호환되는 버전별 이미지를 만들 수 있게 되겠죠? 그리고 이 main_service_ prefix 가 붙어있는 이미지들을 AWS-ECR 에 push 합니다.

    현재의 모든 이미지를 ECR 에 푸시하게 되면, Ubuntu 나 여러 기타 이미지들이 같이 푸시되버립니다. Git Actions 내부적으로 사용하는 이미지까지 푸시되버리는거죠. 그래서 저는 앞에 main_service_ 라는 prefix 를 붙여서 해당 이미지들만 푸시하도록 설정했습니다.
    prefix 붙여서 단일 ECR 에 여러 이미지 푸시하는 쉘 스크립트

  3. Deploy to AWS-EKS

    먼저 새로운 버전이 postfix 로 붙어있는 이미지들을 가져옵니다. 그리고 모든 deployment 타입의 configuration 들 내부 image 필드값들을 방금 가져온 이미지이름으로 교체해줍니다. Deployment image 필드 교체 쉘 스크립트 모든 Deployment 의 이미지 필드가 교체되면, 이제 kubectl apply 를 통해 hpa, volume, namespace, service, deployment 등 모든 것들을 업데이트합니다.

  4. Slack alert

    이제 마지막으로 각 Job 들의 메타정보들을 가져옵니다. 그리고 이를 Slack 채널로 보내게 됩니다. 기본지원되는 기능은 Multi Job 이 고려되지 않아서 하나씩 커스터마이징했습니다.
    Job 수행 시간, 버전과 Job status 등 여러 정보들을 PR 링크와 merge 한 사람의 프로필 링크와 함께 Slack 채널로 보내게 됩니다.

그냥 PR 한번 만 하면 자동으로 모든게 된다니! 너무 행복합니다ㅎㅎ 덕분에 배포에 대한 시간을 많이 아낄 수 있었습니다. 지금까지 배포한 횟수가 2024년 2월 7일 기준 약 362회 입니다. 1회 배포에 걸리는 시간을 30분이라고 생각해보면, 181시간을 아낄 수 있었습니다! 이 시간을 다른 공부를 할 수 있게 되었죠. 이전 배포에서는 직접 이것저것 실행하고 계속해서 신경써야되었죠. 그래서 시간도 매우 오래걸릴 뿐 더러 실수도 계속 나왔었습니다. 하지만 지금은 버튼 하나만 클릭하면 모두 자동으로 배포되고 서비스 유지되니 실수가 없을 뿐 더러 매우 편리했습니다. PR 하고 Slack 알림이 올 때까지 다른 일들을 할 수 있으니까요! 이 맛에 자동화를 하게 되나봅니다.

Git Actions 내부 살펴보기

img img img img

서비스 관리 자동화

이제 서비스 관리 자동화를 그림으로 보시죠. 주목할 부분은 HPA 와 AutoScale-Group 입니다.

img

저는 자동으로 파드 1개씩 롤링 업데이트를 진행해주고, AWS-CloudWatch와 AutoScale-Group으로 cpu usage 50% UP/Down 에 따라 EC2 워커노드를 자동 스케일 인 아웃 시키도록 구현하였습니다. 노드 증가시엔 이메일까지 오도록 설정했답니다. 또한 Kubernetes 가 제공하는 HPA 설정과 이를 지원하는 메트릭 서버를 통해 파드 평균 cpu 30% 넘어가면 자동 파드 스케일 아웃/인 을 구현하였습니다. 파드가 죽으면 자동재시작은 물론이며, 트래픽이 줄어들면 자동으로 파드를 줄이도록 구현했습니다. 즉, 트래픽에 따라 자동으로 워커노드, 파드가 확장되고 줄어들게 됩니다. 상당히 편리하죠!

개발하던 와중 몇가지 이슈가 있었는데요. 그것들을 정리해보겠습니다.

자동화 시 발생한 이슈들과 해결책

AWS-ECR token 갱신 스크립트 이슈

쿠버네티스에서 사용할 이미지를 AWS-ECR 에서 받아올 때 Token 필요합니다. 그리고 이 Token 은 12시간마다 갱신되어야하죠. 그래서 이를 자동화하기 위해 스크립드가 별도로 필요했습니다. 이 갱신 스크립트가 생각보다 까다롭습니다. cronjob 으로 계속해서 갱신해줘야하고 token 을 받아와서 저장하는데도 까다로웠습니다. 그래서 복잡한 방식 대신, AWS-EKS 로 이를 해결했습니다. 기존 온프레미스 쿠버네티스와 달리 IAM 과 연동되서 굳이 토큰을 갱신하지 않더라도 자동으로 이미지 Pull 이 인증된답니다!

AWS-EKS deployment image 버전 이슈

쿠버네티스 deployment 보면 image 받아올 때 태그를 설정해주어야합니다. 이 때 해당 버전에 맞는 태그를 설정해주어야하는데요. 직접 버전을 입력해주기에는 너무 귀찮고, 또 실수할 수 있기에 자동화가 필요했습니다. 그래서 sed 를 사용해서 자동으로 버전을 입력해주는 스크립트를 만들었습니다. 해당 스크립트는 write_image_to_deploy.sh 에서 확인하실 수 있습니다!

AWS-ECR 관리 이슈

ECR 을 하나만 쓸 지 여러개 쓸 지 고민이 많았습니다. 1. ECR 을 이미지 별로 따로 파고 새로운 버전이 삽입되면 latest 로 push 하고 pull 할 것인지, 아니면 그냥 2. ECR 하나만 파서 tag 를 커스터마이징해고 ***_{version} 으로 할 것인지 말이죠. 제 경우 이미지가 10개인데, 이것들을 전부 각각의 ECR 로 관리하게 된다면 관리가 힘들어질 것 같아서 2번 방식으로 결정했습니다. 바로 ECR 하나에 태그로 이미지 버전과 이미지 이름을 같이 넣는 방식이죠. 이렇게 관리하기 위해서는 여러 스크립트가 필요한데, 아래에 정리해보겠습니다.

  • push_to_ecr.sh : 이미지 태그를 커스터마이징해서 push 하는 스크립트
  • ecr_image_pull.sh : 새로운 버전이 postfix 붙어있는 이미지 list 받아오는 스크립트
  • remove_dangling_image.sh : dangling 된 이미지 제거 스크립트

이러한 스크립트들을 통해 단일 ECR 에서 버전별로 여러 이미지를 관리할 수 있게 되었습니다.

Kubernetes HPA 이슈

저는 서버 스파이크 테스트하면서 HPA 자동 파드 확장을 진행했었는데요. 문제는 HPA Scale-In/Out 이 너무 자주 일어난다는 것이였습니다. 그래서 Initializing 시 잡아먹는 리소스가 수시로 소모되었습니다. 그래서 이를 해결하기 위해 HPA 의 scaleDownStabilizationWindowSeconds 를 300초로 설정했습니다. 이렇게 설정하면 5분동안은 Scale-In 이 일어나지 않습니다. 효율적으로 리소스를 관리할 수 있게 되었죠(Scale-Out 은 높은 cpu 로드 때문에 노드가 죽어버릴 것을 예상해서 그대로 놔두었습니다).

Kubernetes 파드 몰림 이슈

쿠버네티스가 정말 blue/green 배포나 롤아웃/롤백 하기 너무 편하고 좋은데, 파드가 하나의 노드에 몰리는 현상이 까다로웠습니다. 이렇게 되면 자원을 고르게 사용할 수 없게 되죠. 특히 nginx-ingress 가 단일 노드에 몰리면 로드 테스트 시 노드가 죽더라구요. 그래서 안티-어피니티 규칙으로 ingress-controller 파드가 모든 노드에 고르게 분산되도록 설정하였습니다. 추가로 Kubernetes 스케줄러가 효율적으로 파드 분배를 할 수 있도록 Resource limit 을 걸어주기도 하고 어피티니로 특정노드에 배포되도록 설정하였습니다.

Kubernetes 파드 요청 실패 이슈

채팅서비스 파드가 생성되자마자 요청이 전달되면 실패하는 경우가 생겼습니다. 이는 파드가 생성되고 서버가 실행되는데 시간이 걸리기 때문입니다. 그래서 이를 해결하기 위해 readinessProbe 를 설정하였습니다. 이렇게 되면 파드가 생성되고 서버가 실행되기 전까지 요청이 들어오지 않습니다. Spring 의 경우에는 Actuator 로 healthcheck API 를 자동으로 만들었습니다.

모든 노드가 연쇄적으로 다 죽는 이슈

Kubernetes 는 특정 노드가 죽으면 해당 노드에 배치된 파드들을 정상적인 노드들로 다 이동시킵니다. 이 때 저는 초기 Initial 할 때 리소스가 많이 필요하기 떄문에, 이주민을 받은 노드또한 죽게 됩니다. 그러면 또 이주가 발생하고 결국 모든 노드가 연쇄적으로 죽는 현상이 발생했습니다.

그래서 애초에 노드가 죽지 않도록 하기 위해서 노드의 평균 cpu 가 50% 을 넘어가면 자동으로 스케일 아웃 되도록 AWS-AutuScaling Group 와 AWS-CloudWatch 를 연동하였습니다. 이렇게 되면 노드가 죽지 않고, 노드가 죽지 않으면 파드가 죽지 않고, 파드가 죽지 않으면 서비스가 죽지 않습니다. 이렇게 서비스가 안정적으로 운영될 수 있게 되었습니다.

EC2 파드 갯수 제한 이슈

EC2 t3.medium 의 경우 파드 17개만 생성가능합니다(17 = ENI * ( IPv4 per ENI -1 ) + 2). 기본 파드랑 여러 기타 파드들을 합치게 된다면, 실제로 제가 사용할 수 있는 파드는 몇 개 되지 않습니다ㅜㅜ.

그래서 이걸 강제로 늘릴 수 있도록 EC2 AutoScale Group 의 노드 템플릿을 --max-pods=50 수정하였습니다. 이렇게 되면 EC2 가 새로 생성될 때마다 파드 제한을 늘릴 수 있습니다. 다만 파드는 자신의 private IPv4 와 1:1 매칭되진 않겠죠ㅎㅎ.

Git Actions 단일 Job 시 원하는 step 을 re-run 할 수 없는 이슈

원래는 단일 Job 으로 수행했었는데, 내부 Step 을 re-run 할 수 없는 이슈가 있었습니다. 그래서 단일 Job 을 쪼개서 여러 Job 으로 나누고 Job 간 변수 output 공유를 통해 각각의 결과를 수신할 수 있었습니다. 이렇게 되면 각각의 Job 을 다시 수행할 수 있게 되죠!