CodeDeploy 운용 시 주의해야 할 점들

안녕하세요. StyleShare에서 커머스 개발을 맡고 있는 강성용입니다.

본문은 스타일쉐어에서 적극적으로 사용하고 있는 배포 도구인 CodeDeploy의 실전 운용 노하우 및 팁을 공유하기 위해 작성되었습니다.

CodeDeploy는 2014년 경에 등장했습니다. 당시에도 이미 ECS는 있었습니다. 그러나 ECS는 특정한 기술(Docker)을 강요한다는 단점 아닌 단점이 있었습니다. 그렇기 때문에 CodeDeploy는 Docker를 적용할 수 없는 특정 환경에서 작업을 해야 하거나, 여러 이유로 ECS를 선호하지 않는 엔지니어들에게는 하나의 대안이었습니다.

특히나 배포에 있어서 당시는 환경 배포(Environmental deployment)와 애플리케이션 배포(Source code deployment)가 크게 분리되어 있지 않아, 이를 구분하려는 각종 노력이 존재했습니다. CodeDeploy는 그러한 아이디어에 기반해, 순수한 AWS의 환경 위에서 애플리케이션 배포와 환경 배포를 내부적으로 확실히 구분하면서도 심리스하게 동작시킬 수 있다는 믿음, 즉 ECS처럼 어플리케이션 배포와 소스코드 배포를 퉁쳐서 넘어가지 않고도 ECS만큼의 기능성을 확보할 수 있다는 확신을 가지고 시작한 프로젝트로 보입니다.

CodeDeploy의 이러한 시도는 일부 성공을 거두었습니다. 그러나 몇몇 이유들로 인해 기존의 배포 도구 시장을 뒤엎지는 못했습니다(만약 그랬다면 Kubernetes와 EKS의 출현은 조금 후가 되었을지도 모르겠습니다).

개인적인 의견이지만 아직 배포 도구 시장에는 완전한 정답은 없는 것 같습니다. 그런 의미에서 이 글은 2019년에 CodeDeploy를 적용하려는 회사들을 위해 남기는 메모입니다. 이를 공유함으로써 추후 다른 개발자들의 기술 선택에 도움이 되길 바랍니다.


PART 0. CodePipeline 소개

CodePipeline은 AWS의 각종 인프라스트럭쳐들을 Lambda로 체이닝함으로써 궁극적으로는 지속적 배포(Continuous Deployement)라는 목적을 달성하기 위한 도구입니다. 보통 하나의 Pipeline 은 AWS Codecommit/Github – Codebuild/Jenkins – Manual Approval – Codedeploy/EKS/ECS 등의 형태로 구성되곤 합니다.

CodePipeline의 존재가 기존의 Continuous Integration 도구들과 무슨 차이가 있는지 어렴풋이 감이 오는 분도 계실 것이고 왜 있는지 모르겠다는 분도 계실 것 같습니다. 제 생각은 양쪽의 생각이 모두 맞습니다.

우선 왜 있는지 모르겠다는 의견이 타당하냐면 CodePipeline의 완성도가 그리 높지 않기 때문입니다. 예를 들어 컨셉만 보자면 CodePipeline은 같은 리포지토리에 대해 다중 브랜치 배포(Multi branch deployment)같은 걸 지원해야 할 것 같습니다만 안타깝게도 그런 기능을 지원하지는 않습니다(만약 이를 달성하려면 Terraform 등을 병용하여 아예 AWS 인프라를 별도로 관리해야 합니다).

단점을 먼저 언급했으나 CodePipeline가 가지는 장점은 상당히 큽니다. 써보신 분들은 아시다시피 Lambda의 컨셉은 매우 단순합니다. 입력을 받고, 수행한 뒤, 결과를 반환하죠. 이 단순한 과정을 여러번 반복하는 것은 일종의 함수형 프로그래밍과도 같습니다.

위 컨셉을 AWS의 각종 서비스와 결부시키면 꽤 쓸 만한 서비스가 됩니다. 수백개의 서비스가 범람하는 마이크로 서비스 아키텍쳐 시대에 코드/빌드/배포를 일괄적으로 하나의 화면에서 관리할 수 있게 되는 것입니다. 심지어 만약 AWS를 적극적으로 활용하고 계시다면, 이미 있는 것들을 단순히 붙이는 것만으로 활용할 수 있습니다.

단, 물론 이것은 여러분이 AWS장애가 나면 자사의 서비스가 중단되는 시대에 태어났기 때문이기도 합니다. 마냥 기뻐할 일은 아닙니다.

아래부터는 CodePipeline과 병용할 경우 주의할 점을 열거해보도록 하겠습니다.


PART 1. CodePipeline과 CodeDeploy 사용시 주의할 점

CodePipeline을 통해 invoke된 프로젝트가 Codebuild에서 넘겨주는 Build Artifacts는 Build step이 끝나고 S3에 업로드될 때 H4VB3Iu 같은 7글자의 Alphanumeric 한 이름을 갖습니다.

그리고 다음 단계로(CodeDeploy로) s3://bucket-name/application/H4VB3Iu.zip 이라는 이름을 넘겨주는 척 하지만 사실 그것은 Display name이 그럴 뿐이고 실제 Location은 s3://bucket-name/application/H4VB3Iu을 넘겨줍니다. 그래야 CodeDeploy 입장에서는 H4VB3Iu라는 파일이 zip으로 압축되어 있다는 사실을 알 수 있으면서도 실제 파일 이름은 H4VB3Iu일 수 있으니까요.

배포하는 척 하는 리비전의 Location

진짜 배포 파일의 Location

이는 CodePipeline으로 호출되는 CodeDeploy가 가진 암묵적인 약속으로 보입니다.

네, 동작이 조금 이상합니다. 물론 동작이 이상한 것과는 별개로 얼핏 보기에 사용하는 데에는 무리가 없어 보입니다. 어차피 머신이 flow를 컨트롤 하는 이상 이런 metadata는 인간에게 참고용일 뿐이니까요. 무슨 일이 일어나지만 않는다면 말입니다.

여기서 무슨 일이란 롤백, 긴급 배포, CodePipeline의 장애 등, 각종 이유로 인해 발생할 수 있는, CodeDeploy 단일 실행을 통한 배포를 의미합니다. (그리고 그런 일은 자주 일어납니다)

만약 여러분이 AWS콘솔에서 CodeDeploy배포를 하게 된다면, CodeDeploy는 이 때 s3://bucket-name/application/H4VB3Iu라는 이름이 아닌s3://bucket-name/application/H4VB3Iu.zip라는 이름의 Build Artifact를 자동완성합니다(심지어, zip이라는 확장자를 제거하면 자동완성이 되지도 않습니다). 왜냐면 이전 배포 이력에 그렇게 남아있기 때문입니다.

그리고 이걸 믿고 배포했다간 어플리케이션이 뜨지 않고 배포가 실패하는 모습을 보실 수 있습니다. 심지어 이 경우 나는 에러는 File not found가 아닌 Access denied 에러이기 때문에 여러분은 절대 원인을 쉽게 발견할 수 없습니다. 아마 S3의 bucket의 policy부터 점검하기 시작하겠지요.

그리고 여러분은 진짜 원인을 찾느라 매우 추상화된 환경 여기저기를 한참을 들쑤신 뒤 원인을 발견하고는 허탈한 표정으로 커피를 들고 하늘만 하염없이 바라볼 것입니다. 하지만 괜찮습니다. 제가 여러분의 2시간을 구했습니다.

 


PART 2. CodeDeploy Blue/Green 옵션 사용 시 주의할 점

Blue/Green이란 일부 인스턴스를 Warming up 시켜놓고 ELB에 연결된 Blue 그룹과 Green 그룹을 Gracefully 교체하는 옵션입니다.

이 동작이 실제로 Graceful한지는 둘째 치고서라도, 다들 ELB 혹은 ALB를 운용해 보았다면 이러한 식의 버전 배포가 익숙할 것입니다. 저도 이러한 작업을 Python script 로 만들어 배포 도구를 만든 적이 있습니다.

문제는 CodeDeploy의 책임과 역할이 이 Blue/Green Deployment에 있어서 확장되어버렸다는 사실에 있습니다. 왜냐면 Blue 그룹과 Green 그룹 두 개를 가동시켜야 하기 때문에 CodeDeploy는 그동안 전통적으로 수행했던 애플리케이션 배포 뿐만이 아닌, EC2인스턴스를 직접 띄우는 등의 환경 배포까지 그 책임이 미치기 시작했기 때문입니다.

아시는 분도 계시겠지만 본래의 CodeDeploy는 원래 특정한 인스턴스에 리비전만 다른 애플리케이션을 배포하면, 인스턴스 내부에 실행중인 에이전트가 AWS 서비스와 통신하며 버전을 교체하는 방식이었습니다(이를 in-place deployment라고 합니다). 그렇지만 Blue/Green 방식의 배포를 위해서는 어쩔 수 없이 Instance를 런칭하고, 제거하고 하는 등의 한 단계 위의 작업이 필요합니다. 그리고, CodeDeploy는 아마도 초기 설계 과정에서 앞으로 이러한 부분을 다룰 것이라 예상하지 못한 것으로 보입니다.

왜냐면 Blue/Green에는 이러한 가정을 바탕으로 한 것으로 보이는 수많은 문제들이 있기 때문입니다. 아래는 만약 여러분이 CodeDeploy를 Blue/Green Deploy로 사용한다면 필히 알아두어야 할 점을 정리해봤습니다.

1) Blue/Green을 통해 런칭된 AutoScalingGroup에는 Target Group이 숨어있습니다

평범한 AutoScaling Group 에 연결된 Target Groups

Blue/Green 으로 생성된 ASG의 Target Group

여기엔 아마도 AWS의 내부 API 를 이용한 트릭이 존재하는 것으로 보입니다. Blue/Green을 통해 배포된 ASG의 Target Group은 빈칸이지만, 사실은 실제로 여기에 CodeDeploy에서 지정한 Target Group이 붙어 있습니다. 이유는 잘 모르겠습니다.

그러니까 단적으로 말하자면, “아, 없나보다. 다른 값을 내 마음대로 수정하고 저장해도 되겠지.” 하고 저장하는 순간 Target Group이 분리되며 서비스는 장애를 일으키게 됩니다.

만약 CodeDeploy 배포로 만들어진 ASG를 손으로 수정해야 할 경우, 이 사실을 항상 염두에 두셔야 합니다.

2) CodeDeploy ARN에 Blue/Green을 위한 별도 policy 를 적용해줘야 합니다

코드디플로이의 기본 IAM 권한에는 Blue/Green 에 필요한 권한이 없습니다. 더욱더 안타까운 점은 이에 대해 명시적으로 작성한 문서도 없다는 사실입니다. 다만 검색을 통해 누군가의 블로그 글을 찾을 수 있으며 이는 올해 2월에 밝혀진 버그입니다. 그리고 아직까지 수정되고 있지 않습니다.

어쩔 수 없습니다. 스스로 해결해야 합니다.

(참조 : https://h2ik.co/2019/02/28/aws-codedeploy-blue-green/)

“The IAM role does not give you permission to perform operations in the following AWS service: AmazonAutoScaling. Contact your AWS administrator if you need help. If you are an AWS administrator, you can grant permissions to your users or groups by creating IAM policies.”

만약 여러분이 위와 같은 에러메시지를 보면 아래 Policy를 여러분이 사용하는 코드플레이 롤(Role)에 별도로 추가해주시면 됩니다.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": [
                "iam:PassRole",
                "ec2:CreateTags",
                "ec2:RunInstances"
            ],
            "Resource": "*"
        }
    ]
}

3) AutoScaling Group의 LifeCycle Hook이 중복되었는지 확인해야 합니다

이 버그는 Blue/Green 배포시 원래는 In-place로 배포했던 ASG를 복사하여 CodeDeploy를 통한 최초 시도 시에 발생합니다. Scheduled action이나 Cloudwatch alarm 등에 걸린 action에 의해 AutoScaling이 실행되면, 두 개의 LifeCycle Hook이(In-place 에서 쓰던 LifeCycle Hook과 Blue/Green 에서 새로 설정한 LifeCycle Hook) 서로 같은 인스턴스에 존재하게 되면서, 하나의 인스턴스에 두 가지 버전이 동시에 배포되는 비극이 일어납니다.

당연히 새 Instance에는 제대로 된 배포가 진행되지 않습니다. 그 인스턴스는 Create 된 직후 PendingWait 상태에서 Health check에 실패하여 Terminate 됩니다. 그러면 또 다시 ASG의 Desired capacity에 의해 다시 Instance가 런칭되고… 이런 상황이 무한하게 반복됩니다.

해결책은 간단합니다. 첫 Blue/Green 배포시 기존의 In-place에 있던 LifeCycle Hook을 삭제해줍시다.

4) Rollback을 하고 나면 AutoScaling Group의 LifeCycle Hook이 훼손됩니다

이 버그는 조금 치명적입니다. 여러분의 배포가 별 일 없이 끝났다면 이 문제를 밟지는 않겠지만, 아시다시피 현대의 웹 애플리케이션은 버그가 있는 버전을 롤백하는 일이 잦습니다.

이 버그는 만약 Blue/Green 배포가 끝난 버전에 대해 Rollback 할 경우 무조건 발생합니다.

상식적으로 Rollback이 실행 된 경우 LifeCycle Hook은 Blue version(롤백된 기존의 버전)에 다시 걸려 있어야 하고, Green version(새로 배포되었다 취소된 버전)에는 걸려있지 않아야 합니다. 그러나 실제로는 롤백된 Blue Version 에는 응당 걸려있어야 할 기존의 LifeCycle Hook 이 없습니다.

이 LifeCycle Hook의 부재로, 현재 배포된 EC2 인스턴스를 제외하고, 새로 런칭되는 인스턴스들에는 CodeDeploy의 자동 배포가 작동하지 않으며, 아예 텅 비어있는 인스턴스가 올라가게 됩니다.

그 결과, 그런 깡통 인스턴스만 계속 떴다 죽었다를 반복하며 전반적인 장애내구성을 약화시키기 시작합니다. 게다가 혹시라도 ASG의 Termination policy가 OldInstance 등으로 걸려있다면 여러분의 ASG는 순식간에 장애를 일으키는 인스턴스들로 가득차게 될 것입니다.

이 버그의 가장 큰 문제는, 이 상태를 문제가 생기기 전에 눈치채기가 쉽지 않다는 점입니다. 왜냐 하면, 이 문제는 평소에 잠재적으로만 존재하다가, Scheduled action 이나 Autoscaling Policy 등에 의해 Instance가 늘어났을 경우에만 가시화되기 때문입니다.

이 문제를 해결하는 법 역시 간단합니다. 만약 버전 롤백을 했을 경우, 꼼꼼히 애플리케이션 상태를 살핍시다.

맺으며

이상으로 CodeDeploy를 실제 환경에 적용할 때 발생할 수 있는 주의점들에 대해 알아보았습니다. 생각보다 허점이 있는 프로덕트에 실망하신 분도 계실 것이고, 이 정도의 배포 시스템을 만드는 데에 드는 비용에 비해서는 생각보다 낮은 트레이드오프라고 생각하신 분도 계실 것입니다.

그러나 가장 좋은 것은 위에서 언급한 주의사항들(이라고 쓰고 버그라고 읽는)을 더 이상 고려하지 않아도 되는 것입니다. AWS의 빠른 패치를 기원합니다.

다음에는 더 좋은 글로 찾아뵙겠습니다.