본문 바로가기

개발노트/LINUX

AWS ECS 로 blue/green 배포 시 겪은 troubleshooting 기록

배경

그동안 EC2 ASG 로 서버 애플리케이션을 실행하는 환경에서 ECS 로 이전을 해야할 필요가 생겼다.

그동안 처음부터 ECS 로 구축하거나 기존에 EC2 ASG 로 운영하던 서비스를 ECS 로 이전해본 경험이 많이 있었는데 유독 이번에 이전 작업을 할 때 문제를 겪은 순간이 많이 있어서 기억을 되짚어가며 기록으로 남겨보려고 한다.

 

들어가기 전에

  • ECS Fargate 가 아닌 ECS on EC2 를 사용하고 있다.
  • ECS 의 capacity provider 는 두 종류의 architecture(amd, arm)를 지원하기 위해 두 개가 구성돼있다.
  • ECS cluster 용 EC2 instance 에는 ecs agent 의 설정에 `ECS_CONTAINER_STOP_TIMEOUT=35m` 가 있는 상황이었다.
  • 그동안 java + jib 를 이용하여 서버 앱을 containerization 을 했다보니 Dockerfile 자체에 대해 깊이 있는 지식이 없는 상태였음
    • 이번 ECS 이전 대상은 ruby on rails 애플리케이션이어서 별도로 Dockerfile 을 작성해야 했음
      • gradle + jib 최고!
  • 이번에 container 에서 띄우는 pid 1 인 foreground process 는 cluster mode 로 동작하는 puma 웹 서버인데, fork 방식으로 여러개의 자식 process 를 생성한다.

 

사용한 AWS 의 서비스

AWS CodePipeline, AWS CodeBuild, AWS CodeDeploy, AWS ECS

 


 

Troubles 를 겪은 순간들

  1. ARM architecture 로 빌드한 이미지로 container 가 시작될 때 바로 `standard_init_linux.go:228: exec user process caused: exec format error` 라는 에러 메시지가 뜨며 뜨지 않는 이슈
    • CodeBuild 단계에서 appspec.yml 파일을 생성할 때 capacity provider 관련 항목이 비어있어서 ECS 가 architecture 는 보지 않고 cpu, mem 과 task placement policy 만을 criteria 로 삼고 amd architecture 기반의 EC2 에 배포를 시도하는 문제가 있었다.
    • CodeBuild 단계에서 appspec.yml 파일을 생성할 때 capacity provider strategy 를 arm 기반의 capacity provider 를 사용하도록 지정하여 해결하였음
  2. 사내의 ECS 로 배포가 돼있는 다른 서비스들 참고하여 이번 작업 대상도 처음에는 쉽게 ECS 로 옮겼으나, 연속 배포가 시도될 때 배포가 너무 오랫동안 되지 않는 이슈
    • 결과적으로는 표면적인 이유로는 stopTimeout 이었고, 근본적인 이유로는 container 의 pid 1 프로세스의 명령어가 shell form 으로 감싸진 command 형태였기 때문에 TERM signal 이 제대로 전달되지 않았기 때문인 것으로 밝혀졌다.
    • 처음에는 stopTimeout 설정을 의심하여 이와 관련된 여러 시나리오를 떠올려 보았다.
      • EC2 의 자원(cpu, mem)이 부족해서? 새 task 가 시작되지 못하고 한참 pending 상태를 유지하는 건가?
      • 35분(ECS_CONTAINER_STOP_TIMEOUT) 이내에 ECS 에 연속적인 배포가 생성되면, 기존에 stop 시도 중인 ECS task 때문에 ECS 가 유휴 자원 계산을 잘못 하고 있는 건가?
    • ECS Service 의 min running tasks 와 max running tasks 가 default 100%, 200% 로 돼있는데 이걸 조정하면 되려나?
      • max running tasks 을 300%, 400% 로 올리면 새 task 가 정상적으로 시작되지 않을까란 생각... ㅋㅋㅋ
        • max running tasks 의 최대값이 200% 이어서 불가능
    • blue/green 배포를 하도록 하면 새로 뜰 task 가 기존의 ECS task 가 종료될 때까지 기다리지 않게 되니까 해결되지 않을까?
      • 결론을 알고 있는 글을 쓰는 지금 시점 기준으로는 이 가설도 틀린 가설이었음
      • blue/green 배포를 하도록 한 이후에도 연속 배포가 시도될 때 기존 task 가 약 35분 이후에 죽는 것과 관련하여 배포가 제때 되지 않았음
  3. container 에서 signal TERM 에 대한 handler 가 제대로 작동하지 않는 현상을 발견함
    • Dockerfile 에서 ENTRYPOINT 와 CMD 의 차이점과 작성 스타일에 따른 차이를 간과했음
    • dumb-init 사용하여 해결함
      • 이때까지는 pid 1 인 프로세스가 foreground 프로세스로서 계속 떠있어야 한다는 것 정도의 지식만 갖고 있었고, container 운영 시 dumb-init, tini, runit, monit, supervisord 같은 게 필요하다는 이론적인 내용이 존재함 수준으로만 알고 있었는데 이번 기회에 이게 정말 왜 필요한지를 알게 됐다.
      • https://github.com/Yelp/dumb-init
  4. CodeBuild 에서 imageDetail.json 파일 생성 후, CodeDeploy 에서 imageDetail.json 을 parse 할 때 오류가 발생
    • CodeDeploy 단계에서 `exception while trying to parse the image uri file from the artifact` 라는 에러 메시지가 뜸 
    • imagedefinitions.json 형식은 json array 형식으로 돼있어서 imageDetail.json 도 형식은 똑같겠거니라고 생각했었던 게 패착이었음
  5. Blue/Green CodeDeploy 방식으로 배포하니 순간적으로 다량의 502 Bad Gateway 가 발생하였음
    • 리서치 해보니 LB 가 새 TG(green) 으로 트래픽을 라우팅하도록 변경한 이후에도 새 TG(green)의 targets 이 healthy 되기 전까지는 기존 TG(blue)에 트래픽을 routing 하고 있는 것으로 보였고, 이 상황에서 기존 TG(blue)에 등록된 targets 를 `termination_wait_time_in_minutes` 설정값(내가 설정한 1분)에 따라 빠르게 종료되면서 새 TG(green)과 기존 TG(blue) 모두 응답할 수 있는 target 이 없는 상태가 발생했으므로 이 때 502 Bad Gateway 가 발생했던 것으로 파악하였음
    • `termination_wait_time_in_minutes` 값을 기존의 1분에서 5분으로 늘려서 해결하였음
    • 링크: https://www.yuryoparin.com/2022/10/aws-bluegreen-codedeploy-fixing-http.html  
    • 더보기
      After our recent migration of landing pages from EC2 to AWS ECS Fargate, we started to experience 502 Bad Gateway errors that lasted for 1–2 minutes each time a CodePipeline for that ECS landing service finished running its final stage, the Blue/Green CodeDeploy.

      To fix the issue, I decided to track all the changes CodeDeploy performed with the service load balancer and its blue/green target groups in AWS Console.

      It turned out that after launching a healthy Fargate instance, CodeDeploy registered a new IP address of that Fargate instance in the next blue/green target group using round-robin scheduling algorithm and assigned that target group to the load balancer immediately (default behaviour).

      Since registering an IP address in target groups takes approx. 2 minutes (= 3 consecutive health check successes running with 30 seconds intervals, i.e. 90 seconds), the load balancer stayed unreachable for that duration, hence our HTTP 502 error.

      An apparent solution was to prolong the assignment of the old target group to the load balancer. However with the immediate rerouting of traffic it wasn't feasible. Then I luckily remembered that CodeDeploy had an option of setting a waiting time for "Original revision termination" and so I set it to the next possible value after zero, 5 minutes, and started the CodePipeline.

      What was surprising to find out was that this setting fixed the problem! The tricky and not well documented behaviour of AWS load balancer was that even though it had already a new target group assigned visible in AWS Console, the load balancer still tried to reach an old target group, which was inconsistent with AWS Console. This I detected by comparing response ETag headers of the index page in the landing service.

      So the actual cause of the error was not that the new target group was initializing and therefore not responding but that the old instance was terminated immediately after CodeDeploy finished. Hence setting a waiting time did the trick of allowing the landing service to continue responding without a break as if CodeDeploy hadn't run yet.

      Another pleasant surprise was that once a new target group got to a healthy state in about 1.5 minutes, the load balancer switched traffic there (finally consistent with AWS Console), without waiting for the old instance to be terminated in the remaining 3.5 minutes (=5 min - 1.5 min).

      This essentially created for us and our users a smooth and very fast transition from the old Fargate instance to a new one.

      The recipe: Set a waiting time for "Original revision termination" in CodeDeploy to 5 minutes.

이거 말고도 다른 자잘한 이슈들(blue/green 배포에 사용된 TargetGroup 의 protocol 이 TCP vs HTTP 로 서로 다르게 구성됐던 게 있어서 CodeDeploy 에서 ECS task 실행은 잘 했지만 traffic routing 에 실패했던 것 등 - 두 target groups 의 protocol 을 같은 것으로 구성하여 해결)이 있었기도 했다.

 


 

Trouble 까지는 아니지만 작업하면서 신경써야 하는 점 또는 장애물이 됐던 점

CodeBuild 단계에서 task definition template 파일과 appspec template 파일을 작성하는 형태로 해야 한다.

이것 관련해서 task definition 을 terraform 으로 만드는 방식을 사용하는 것은 redundant 하므로 terraform 에서 task definitnion 을 생성하거나 참조시킬 필요가 없고, appspec.yml 파일은 굳이 CodeBuild 단계에서 생성하지 않고 source code 수준에서 작성해도 되기는 하다. 난 기존의 CodeDeploy 로 EC2 ASG 에 배포할 때 사용되던 appspec.yml 파일이 이미 소스 코드 내에 존재하고 있어서 혼란을 피하기 위해 CodeBuild 단계에서 파일을 생성하도록 하였다.


ECS 에 배포하는 방법은 크게 3가지 종류가 있음

  • ECS console 에서 직접 관리(새 task 를 수동으로 생성하거나 ECS Service 를 생성하고 Service 를 수정하며 Task Definition 을 바꾸거나 force deployment 옵션을 켜서 업데이트시키는 방식 등)
  • CodePipeline 의 standard ECS deployment stage 를 이용
  • CodePipeline 의 CodeDeployECS deployment stage 를 이용 (provider=CodeDeployToECS)

이 중 blue/green 배포를 하려면 "CodePipeline 의 CodeDeployECS deployment stage 를 이용" 하는 방식을 사용해야만 한다.

CodePipeline 에서 CodeDeploy action 의 구성 내용

이 때, CodeDeploy 에서 "imageDetail.json" 이라는 파일 이름을 찾고, 다른 이름으로 작성한 파일을 참조시킬 수 없으므로 반드시 지켜야 한다.


AWS CodeDeploy for ECS blue/green 에서는 엄격히 지켜야 할 것이 있음

build 의 결과물 artifact(output_artifact)에 반드시 "imageDetail.json" 라는 이름을 가진 파일이 필요하다.

c.f.  standard ECS deployment 에서는 "imagedefinitions.json" 라는 이름이 default name 이지만, 다른 이름으로 했더라도 CodePipeline 의 Deploy action 에서 다른 이름으로 지정할 수가 있어서 '강제'되는 것은 아니다.


CodeBuild 에서 docker image 빌드할 때 external cache 사용하여 속도 개선하기

https://aws.amazon.com/ko/blogs/devops/reducing-docker-image-build-time-on-aws-codebuild-using-an-external-cache/

 


 

Helpful links

 

반응형