티스토리 뷰

들어가며 (다이어그램 출처)

세 환경의 네트워크 다이어그램을 나란히 두고 ALB 노드만 눈여겨보면 라벨이 하나만 다릅니다. dev와 beta는 ":80 HTTP"라고만 적혀 있고, prod에는 ":443 HTTPS (:80→:443 redirect)"라는 조금 더 긴 라벨이 붙습니다. 처음에는 "prod만 인증서를 발급받았나 보다" 정도로 넘어갔는데, Terraform 코드를 열어 보니 이 차이를 만드는 조건문이 생각보다 단순했습니다. local.alb_https_enabled = var.alb_certificate_arn != "". 인증서 ARN 변수가 비어 있는지 아닌지, 그 한 줄이 리스너 구성 전체를 갈라놓고 있었습니다.

 

이 글에서는 이 토글이 어떻게 짜여 있는지, 그리고 이 토글이 "아직 인증서가 없어서"가 아니라 "아직 켤 이유가 없어서"에 가까운 결정이라는 점을 코드와 AWS 공식 문서를 근거로 짚어 보려 합니다. HTTPS를 켜고 끄는 일이 이 프로젝트에서는 리스너 두 벌을 따로 관리하는 문제가 아니라, 변수 하나짜리 가역적 결정으로 다뤄지고 있다는 것이 이 글의 핵심입니다.


if/else 두 벌이 아니라 변수 하나

ALB에 HTTPS 리스너를 추가하는 일 자체는 AWS 문서에 정형화된 절차로 나와 있습니다. HTTPS 리스너를 만들려면 로드밸런서에 SSL 서버 인증서를 최소 하나 배포해야 하고, 클라이언트와 로드밸런서 사이의 보안 연결을 협상할 보안 정책도 함께 지정해야 합니다. 로드밸런서는 이 인증서로 클라이언트와의 연결을 종단하고, 요청을 복호화한 뒤 타깃으로 전달합니다. 이 절차 자체는 dev든 prod든 다르지 않습니다.

 

차이가 생기는 지점은 "이 절차를 실행할 것인가"를 결정하는 방식입니다. 이 프로젝트는 HTTP 리스너와 HTTPS 리스너를 위한 코드를 각각 따로 두고 환경별로 어느 쪽을 배포할지 고르는 대신, var.alb_certificate_arn이라는 문자열 변수 하나로 분기합니다. 이 변수가 빈 문자열이면 local.alb_https_enabled가 거짓이 되어 HTTP 리스너만 만들어지고, ACM 인증서 ARN이 채워지면 참이 되어 HTTPS 리스너가 추가로 만들어지면서 HTTP 리스너의 역할도 바뀝니다. prod의 terraform.tfvars에는 이 값이 채워져 있고, dev와 beta의 tfvars에는 비어 있습니다.

 

이 방식이 리스너 두 벌을 따로 관리하는 방식보다 나은 이유는 분기가 코드 구조가 아니라 값 하나로 표현된다는 데 있습니다. 새로운 환경을 추가하거나 기존 환경의 정책을 바꿀 때, .tf 파일을 고칠 필요 없이 terraform.tfvars의 값 한 줄만 채우면 됩니다. 리스너를 만드는 로직 자체는 환경에 상관없이 하나로 유지되고, "이 환경에 HTTPS가 필요한가"라는 질문에 대한 답만 환경별로 달라집니다.

 

이 분기가 실제로 리소스 생성 여부까지 좌우한다는 점도 짚어 둘 만합니다. Terraform에서 리소스를 조건부로 만들 때 흔히 쓰는 방식은 count 인자에 불리언을 정수로 바꿔 넣는 것입니다. count = local.alb_https_enabled ? 1 : 0처럼 써 두면, 조건이 참일 때만 그 리소스 블록이 실제로 하나 만들어지고 거짓이면 아예 존재하지 않는 것으로 취급됩니다. HTTPS 리스너 자체를 이렇게 조건부로 선언해 두면, dev와 beta의 상태 파일에는 HTTPS 리스너라는 개념 자체가 등장하지 않습니다. "비활성화된 리스너"가 숨어 있는 게 아니라, 그 리소스가 애초에 만들어지지 않는 것입니다. 이 차이는 사소해 보이지만, terraform plan 결과를 읽을 때 실제로 무엇이 존재하고 무엇이 존재하지 않는지를 정확히 보여 준다는 점에서 의미가 있습니다.

 

이 토글은 리스너 하나에서 끝나지 않고 보안 그룹까지 이어집니다. HTTPS가 켜지면 ALB의 보안 그룹에도 443 포트 인바운드 규칙이 함께 열립니다. 리스너와 보안 그룹 규칙이 같은 조건 변수를 참조하고 있다는 것은, "포트는 열려 있는데 리스너가 없다"거나 "리스너는 있는데 포트가 막혀 있다"는 어긋난 상태가 애초에 발생하지 않도록 두 리소스의 생성 여부를 같은 스위치에 묶어 뒀다는 뜻입니다.


HTTP 리스너는 사라지지 않는다

여기서 놓치기 쉬운 부분이 있습니다. HTTPS가 켜진다고 해서 HTTP 리스너가 없어지는 게 아니라, 역할이 바뀝니다. dev와 beta에서는 80번 포트 리스너가 기본 액션으로 404 고정 응답을 돌려주는 평범한 HTTP 엔드포인트입니다. prod에서는 같은 80번 포트가 443으로 향하는 리다이렉트 전용 통로가 됩니다.

 

AWS 문서를 보면 리스너 규칙의 라우팅 액션 중 하나로 "URL로 리다이렉트"가 있고, 이때 상태 코드를 임시 리다이렉트(HTTP 302)와 영구 리다이렉트(HTTP 301) 중에서 선택하도록 안내하고 있습니다. 이 프로젝트의 prod 리스너는 영구 리다이렉트, 즉 301을 사용합니다. 브라우저나 중간의 캐시 서버가 "이 주소는 이제 이쪽으로 영구히 옮겨졌다"고 기억해 두고 다음부터는 아예 HTTPS로 먼저 요청하게 만드는 상태 코드입니다. 임시 리다이렉트(302)를 쓰면 매 요청마다 HTTP로 먼저 들어왔다가 리다이렉트를 다시 타야 하지만, 301은 이 왕복을 줄여 줍니다. 사용자를 영구적으로 HTTPS 쪽으로 유도하겠다는 의도가 상태 코드 선택에 그대로 드러나는 셈입니다.

 

dev와 beta의 80번 리스너가 404를 돌려주는 이유도 같은 맥락에서 이해할 수 있습니다. 리다이렉트할 HTTPS 리스너 자체가 없는 환경에서는 80번 포트가 그냥 유일한 진입점이고, 규칙에 매칭되지 않는 요청은 리다이렉트가 아니라 고정 응답으로 처리하는 편이 자연스럽습니다. 같은 80번 포트라는 이름 아래, 환경에 따라 완전히 다른 두 가지 역할이 부여돼 있는 것입니다.


왜 dev·beta는 아직 HTTP인가

이 질문에 대한 가장 손쉬운 답은 "인증서가 없어서"입니다. 틀린 말은 아니지만, 왜 인증서를 안 받아 뒀는지까지 들어가 보면 이야기가 조금 더 흥미로워집니다.

 

ACM에서 공인 인증서를 발급받으려면 도메인 소유권을 증명하는 절차를 거쳐야 합니다. AWS 공식 문서는 DNS 검증 방식을 안내하면서, 검증용 CNAME 레코드를 DNS 설정에 추가하는 절차를 설명하고 있고, ACM이 인증서 검증을 최대 72시간 동안 반복 시도한 뒤 시간이 초과되면 요청이 실패한다고 명시하고 있습니다. 검증 자체가 순식간에 끝나는 일이 아니라, DNS 레코드를 걸어 두고 전파를 기다려야 하는 절차라는 뜻입니다.

 

dev와 beta처럼 외부 사용자에게 공개되지 않는 검증용 환경에 이 절차를 매번 밟아 가며 공인 인증서를 유지하는 일은 비용 대비 얻는 게 많지 않습니다. 이 환경들의 목적은 배포 파이프라인이 정상 동작하는지, 서비스 코드가 제대로 뜨는지를 확인하는 것이지, TLS 종단 자체를 검증하는 것이 아닙니다. ALB에서 타깃으로 가는 백엔드 구간은 어느 환경에서든 HTTP로 통일돼 있어서, HTTPS 종단은 오로지 "외부에서 ALB까지"의 구간에만 관여합니다. 이 구간이 외부에 노출되지 않는 환경이라면, 인증서 발급과 갱신을 위한 운영 비용을 들일 유인이 상대적으로 적습니다.

 

중요한 것은 이 결정이 "안 한다"가 아니라 "아직 안 한다"로 설계돼 있다는 점입니다. var.alb_certificate_arn에 값을 채우고 apply를 한 번 더 돌리는 것만으로 dev나 beta도 언제든 같은 절차를 밟을 수 있습니다. 코드를 새로 쓰거나 리소스 구조를 바꿀 필요가 없습니다. 되돌리기 어려운 결정과 언제든 뒤집을 수 있는 결정을 구분해서 다뤄야 한다면, 이 토글은 후자에 가깝습니다.

 

실제로 이 전환을 한 번 켠 뒤에 되돌리는 경우까지 생각해 보면, 가역성의 무게가 완전히 같지는 않습니다. HTTPS를 새로 켜는 쪽은 인증서 검증이라는 대기 시간만 감수하면 되는 추가 작업이라 위험이 크지 않습니다. 반대로 이미 HTTPS와 301 리다이렉트를 켜 둔 환경에서 다시 HTTP로 되돌리는 경우에는, 이미 HTTPS 주소로 북마크되었거나 캐시된 클라이언트가 있다면 그 경험이 끊길 수 있습니다. 같은 변수 하나로 켜고 끌 수 있다는 사실이 "언제 켜도 끄도 비용이 같다"는 뜻은 아니라는 점은 구분해서 봐야 합니다. 이 프로젝트에서는 dev·beta가 아직 한 번도 HTTPS를 켠 적이 없는 상태이므로, 지금 시점에는 켜는 방향의 가역성만 고려하면 됩니다.


TLS 정책까지 변수로 열어 둔다는 것

HTTPS 리스너를 켜는 것만으로 끝나지 않습니다. AWS 문서에 따르면 HTTPS 리스너는 보안 정책을 반드시 하나 지정해야 하고, 지정하지 않으면 생성 방법에 따라 정해진 기본값이 적용됩니다. 보안 정책은 클라이언트와 로드밸런서 사이에서 어떤 TLS 프로토콜 버전과 암호화 방식(cipher)을 쓸 것인지를 정한 조합입니다.

 

이 프로젝트는 보안 정책도 var.alb_ssl_policy라는 변수로 열어 두고, 기본값을 TLS 1.2와 1.3을 함께 지원하는 ELBSecurityPolicy-TLS13-1-2-2021-06으로 잡아 뒀습니다. AWS가 제공하는 보안 정책들은 이름 자체에 지원 범위가 드러나도록 명명돼 있어서, 오래된 프로토콜 버전을 허용하지 않는 최신 정책부터 레거시 클라이언트를 위해 하위 호환을 넉넉히 열어 둔 정책까지 고를 수 있습니다. 이 값도 변수로 열려 있다는 것은, "HTTPS를 켤 것인가"와 "얼마나 엄격한 TLS 정책을 쓸 것인가"가 서로 다른 축의 결정이라는 점을 코드 구조 자체가 인정하고 있다는 뜻입니다. 지금은 모든 환경이 같은 기본 정책을 공유하고 있지만, 특정 환경에서 더 엄격하거나 더 느슨한 정책이 필요해지면 이 변수 하나만 조정하면 됩니다.

 

AWS 문서는 보안 정책을 "프로토콜과 암호화 방식(cipher)의 조합"이라고 정의합니다. 클라이언트와 로드밸런서가 연결을 맺을 때 각자 지원하는 암호화 방식 목록을 우선순위대로 제시하고, 서버 쪽 목록에서 클라이언트 목록과 처음으로 겹치는 항목이 골라집니다. TLS 1.3만 지원하는 정책이거나, TLS 1.3과 1.2를 함께 지원하면서 암호화 방식을 ECDHE 계열로 한정한 정책은 완전 순방향 비밀성(Forward Secrecy)을 제공한다고 문서는 설명합니다. 세션 키가 매번 새로 만들어져서, 장기 비밀키가 나중에 유출되더라도 과거에 오간 트래픽까지 거슬러 복호화되지는 않는다는 뜻입니다. 이 프로젝트가 채택한 정책 계열도 이 조건을 만족하는 쪽에 속합니다. 인증서를 배포하는 것과, 그 인증서로 어떤 암호화 수준을 강제할 것인지는 별개의 결정이라는 사실이 이 변수 하나로 분리돼 있는 셈입니다.


보안 강도를 변수로 관리한다는 것의 의미

dev와 beta가 HTTP만 쓴다는 사실을 처음 봤을 때는 "보안이 느슨하게 설계됐다"는 인상을 받기 쉽습니다. 그런데 코드를 따라가 보면 이건 느슨함이 아니라 명시적인 선택이라는 게 더 정확한 설명입니다. 위협에 노출되는 정도가 다른 환경에 같은 강도의 보안 장치를 일괄 적용하는 대신, 그 장치를 켜고 끄는 스위치를 변수로 노출해 두고 환경마다 다른 값을 준 것입니다. 이 스위치가 없었다면 dev 환경에도 처음부터 인증서 발급과 갱신 자동화까지 갖춰야 했을 것이고, 반대로 prod 환경에도 HTTPS를 강제하는 로직을 하드코딩해야 했을 것입니다. 둘 다 지금보다 더 경직된 구조입니다.

 

이 글을 정리하면서 남은 인상은, 좋은 인프라 코드의 토글은 "켜져 있는가 꺼져 있는가"만 보여 주는 게 아니라 "이 결정을 되돌리는 데 얼마나 드는가"까지 함께 드러낸다는 점이었습니다. alb_certificate_arn이 빈 문자열인 상태는 dev와 beta가 영원히 HTTP로 남는다는 선언이 아니라, 지금은 그럴 필요가 없다는 판단을 코드에 정직하게 남겨 둔 것에 가깝습니다. 다이어그램의 라벨 하나가 짧게 요약해 준 이 차이를, 실제 변수 이름과 조건문까지 따라가 보고 나서야 제대로 이해한 느낌이었습니다.

 

이 프로젝트의 다른 인프라 결정에서도 비슷한 모양의 토글을 다시 만난 적이 있습니다. 이벤트 스트리밍 클러스터를 환경마다 다르게 두는 결정도 결국 불리언 변수 하나가 리소스 생성 여부를 가르는 구조였습니다. 서로 다른 컴포넌트인데 같은 모양의 스위치를 쓰고 있다는 것은, 이 프로젝트가 "환경별로 다른 인프라를 두겠다"는 결정을 매번 새로운 방식으로 표현하는 대신, 검증된 한 가지 패턴을 반복해서 재사용하고 있다는 뜻으로 읽었습니다. 패턴이 반복될수록 그 패턴을 읽는 사람의 학습 비용도 줄어듭니다.


참고한 공식 문서