HTTPS 리스너 없이 ALB 구축 후 ACM 등록으로 HTTPS 전환하기
안녕하세요 클래스메소드 김재욱(Kim Jaewook) 입니다. 이번 블로그에서는 HTTPS 리스너 없이 ALB 구축 후 ACM 등록으로 HTTPS로 전환하는 방법에 대해서 정리해 봤습니다.
ACM에 대해서
ACM(AWS Certificate Manager)에서 인증서를 요청한 후 72시간 이내에 도메인 검증을 완료하지 않으면 인증서 발급 요청이 만료됩니다.
CloudFormation(CFN)이나 AWS 콘솔을 통해 인증서를 생성한 경우에도 동일하며, 검증이 완료되지 않으면 인증서 상태가 검증 시간 초과(Timed out validation) 로 변경됩니다.
아래 이미지와 같이 검증 시간 초과 상태가 표시되며, 인증서 검증이 72시간 이내에 수행되지 않았으며 요청 시간이 초과되었습니다. 라는 메시지를 확인할 수 있습니다.

ACM 없이 ALB를 먼저 생성하고 싶을 때
검증이 완료되지 않은 ACM 인증서(검증 대기 중 또는 검증 실패 상태)는 ALB의 HTTPS 리스너에 연결할 수 없습니다. AWS에서는 HTTPS 리스너에 대해 발급됨(Issued) 상태의 인증서만 사용할 수 있도록 제한하고 있습니다.
따라서 도메인 검증이 완료되지 않은 시점에는 HTTPS 리스너를 생성할 수 없으며, 해당 인증서를 ALB에 연결하는 것도 불가능합니다.
다만 프로젝트 일정에 따라 ACM 인증서 발급보다 ALB 생성이 먼저 필요한 경우가 있습니다. 이 경우에는 HTTPS 리스너와 인증서 관련 설정을 제외한 상태로 ALB를 먼저 생성한 뒤, ACM 인증서가 발급된 이후 HTTPS 리스너를 추가하는 방식으로 진행할 수 있습니다.
즉, CloudFormation 템플릿에서는 리스너 관련 리소스를 제거한 상태로 ALB를 배포하고, 이후 인증서 발급이 완료되면 HTTPS 리스너를 추가하여 HTTPS를 활성화하면 됩니다.
ALB 생성
아래는 리스너를 제외한 상태로 ALB를 생성하는 CloudFormation 코드입니다.
AWSTemplateFormatVersion: "2010-09-09"
Description:
Create ALB or S3 Bucket
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SystemName:
Description: "System name of each resource names."
Type: String
Default: "{{SystemName}}"
EnvName:
Description: "Environment name of each resource names."
Type: String
Default: "{{EnvName}}"
VpcId:
Description: "VPC ID"
Type: AWS::EC2::VPC::Id
{{SystemName}}{{EnvName}}ALBPublicSubnet1aId:
Description: "Public Subnet ID for ALB({{SystemName}}-{{EnvName}}-public-subnet-1a)"
Type: AWS::EC2::Subnet::Id
{{SystemName}}{{EnvName}}ALBPublicSubnet1cId:
Description: "Public Subnet ID for ALB({{SystemName}}-{{EnvName}}-public-subnet-1c)"
Type: AWS::EC2::Subnet::Id
Resources:
# ------------------------------------------------------------#
# Create S3 Bucket
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBLogsBucket:
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub ${SystemName}-${EnvName}-alb-{{AccountID}}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: !Sub ${SystemName}-${EnvName}-aws-alb-logs-lifecycle
Status: Enabled
ExpirationInDays: 365
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvName}-alb-{{AccountID}}
- Key: Env
Value: !Sub ${EnvName}
# ------------------------------------------------------------#
# Create S3 Bucket Policy
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBLogsBucketPolicy:
Type: "AWS::S3::BucketPolicy"
Properties:
Bucket: !Ref {{SystemName}}{{EnvName}}ALBLogsBucket
PolicyDocument:
Id: "AWSCFn-AccessLogs-Policy-20180920"
Version: "2012-10-17"
Statement:
- Sid: "AlbLogs"
Effect: "Allow"
Action:
- "s3:PutObject"
Resource: !Sub arn:aws:s3:::${{{SystemName}}{{EnvName}}ALBLogsBucket}/AWSLogs/${AWS::AccountId}/*
Principal:
AWS:
- "{{ELBAccountID}}"
# ------------------------------------------------------------#
# Target Group
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBTargetGroup:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
VpcId: !Ref VpcId
Name: !Sub ${SystemName}-${EnvName}-alb-tg
Protocol: HTTP
ProtocolVersion: HTTP1
Port: 80
HealthCheckProtocol: HTTP
HealthCheckPath: "/healthcheck.html"
HealthCheckPort: "traffic-port"
HealthyThresholdCount: 5
UnhealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 5
HealthCheckIntervalSeconds: 30
Matcher:
HttpCode: 200
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvName}-alb-tg
- Key: Env
Value: !Sub ${EnvName}
TargetGroupAttributes:
- Key: "stickiness.enabled"
Value: false
Targets:
- Id:
Fn::ImportValue: !Sub ${SystemName}-${EnvName}-web
Port: 80
# ------------------------------------------------------------#
# internet-facing ALB
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}InternetFacingALB:
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
Properties:
Name: !Sub ${SystemName}-${EnvName}-alb
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvName}-alb
- Key: Env
Value: !Sub ${EnvName}
Scheme: "internet-facing"
LoadBalancerAttributes:
- Key: "deletion_protection.enabled"
Value: true
- Key: access_logs.s3.enabled
Value: true
- Key: access_logs.s3.bucket
Value: !Ref {{SystemName}}{{EnvName}}ALBLogsBucket
SecurityGroups:
- Fn::ImportValue: !Sub ${SystemName}-${EnvName}-alb-sg
Subnets:
- !Ref {{SystemName}}{{EnvName}}ALBPublicSubnet1aId
- !Ref {{SystemName}}{{EnvName}}ALBPublicSubnet1cId
Type: application
스택 생성이 완료된 후 ALB 콘솔에서 확인해 보면 아래와 같이 리스너가 없는 상태로 생성된 것을 확인할 수 있습니다.

타겟 그룹은 생성되어 있지만 아직 어떤 ALB 리스너의 기본 동작(Default Action)이나 규칙(Rule)에도 연결되어 있지 않습니다.
따라서 타겟 그룹 상태는 Unused 로 표시되며, ALB를 통한 트래픽 전달이나 헬스 체크가 수행되지 않습니다.

이 상태에서는 ALB 또는 Target Group 관련 CloudWatch 알람을 구성해 두었더라도 대부분의 메트릭이 생성되지 않거나 트래픽이 발생하지 않기 때문에 알람이 동작하지 않습니다.
| 알람 | 발생 여부 | 이유 |
|---|---|---|
| UnHealthyHostCount | ❌ | Target Group이 Unused 상태이므로 헬스 체크가 수행되지 않음 |
| HTTPCode_ELB_503_Count | ❌ | ALB로 유입되는 요청이 없음 |
| HTTPCode_ELB_502_Count | ❌ | ALB로 유입되는 요청이 없음 |
| HTTPCode_ELB_4XX_Count | ❌ | ALB로 유입되는 요청이 없음 |
| HTTPCode_Target_4XX_Count | ❌ | 타겟으로 전달되는 요청이 없음 |
| HTTPCode_Target_5XX_Count | ❌ | 타겟으로 전달되는 요청이 없음 |
즉, ALB와 Target Group은 생성되어 있지만 실제 서비스 트래픽을 처리할 수 있는 상태는 아닙니다. ACM 인증서 발급이 완료된 이후 HTTPS 리스너를 추가하고 Target Group을 연결해야 정상적으로 요청 수신, 트래픽 전달, 헬스 체크 및 모니터링이 시작됩니다.
ALB에 리스너 추가
프로젝트 일정이 확정되어 ACM 인증서를 발급받고 도메인 검증까지 완료했다면, 이제 ALB에 HTTPS 리스너를 추가할 수 있습니다.
아래는 기존 CloudFormation 템플릿에 HTTPS 리스너를 추가한 예제입니다.
AWSTemplateFormatVersion: "2010-09-09"
Description:
Create ALB or S3 Bucket
# ------------------------------------------------------------#
# Input Parameters
# ------------------------------------------------------------#
Parameters:
SystemName:
Description: "System name of each resource names."
Type: String
Default: "{{SystemName}}"
EnvName:
Description: "Environment name of each resource names."
Type: String
Default: "{{EnvName}}"
VpcId:
Description: "VPC ID"
Type: AWS::EC2::VPC::Id
{{SystemName}}{{EnvName}}ALBPublicSubnet1aId:
Description: "Public Subnet ID for ALB({{SystemName}}-{{EnvName}}-public-subnet-1a)"
Type: AWS::EC2::Subnet::Id
{{SystemName}}{{EnvName}}ALBPublicSubnet1cId:
Description: "Public Subnet ID for ALB({{SystemName}}-{{EnvName}}-public-subnet-1c)"
Type: AWS::EC2::Subnet::Id
LoadBalancerCertificateARN:
Type: String
Default: #Arn修正
Description: Enter certificate ARN; Use ACM to create a certificate before creating this stack
Resources:
# ------------------------------------------------------------#
# Create S3 Bucket
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBLogsBucket:
Type: "AWS::S3::Bucket"
Properties:
BucketName: !Sub ${SystemName}-${EnvName}-alb-{{AccountID}}
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: !Sub ${SystemName}-${EnvName}-aws-alb-logs-lifecycle
Status: Enabled
ExpirationInDays: 365
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvName}-alb-{{AccountID}}
- Key: Env
Value: !Sub ${EnvName}
# ------------------------------------------------------------#
# Create S3 Bucket Policy
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBLogsBucketPolicy:
Type: "AWS::S3::BucketPolicy"
Properties:
Bucket: !Ref {{SystemName}}{{EnvName}}ALBLogsBucket
PolicyDocument:
Id: "AWSCFn-AccessLogs-Policy-20180920"
Version: "2012-10-17"
Statement:
- Sid: "AlbLogs"
Effect: "Allow"
Action:
- "s3:PutObject"
Resource: !Sub arn:aws:s3:::${{{SystemName}}{{EnvName}}ALBLogsBucket}/AWSLogs/${AWS::AccountId}/*
Principal:
AWS:
- "{{ELBAccountID}}"
# ------------------------------------------------------------#
# Target Group
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBTargetGroup:
Type: "AWS::ElasticLoadBalancingV2::TargetGroup"
Properties:
VpcId: !Ref VpcId
Name: !Sub ${SystemName}-${EnvName}-alb-tg
Protocol: HTTP
ProtocolVersion: HTTP1
Port: 80
HealthCheckProtocol: HTTP
HealthCheckPath: "/healthcheck.html"
HealthCheckPort: "traffic-port"
HealthyThresholdCount: 5
UnhealthyThresholdCount: 2
HealthCheckTimeoutSeconds: 5
HealthCheckIntervalSeconds: 30
Matcher:
HttpCode: 200
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvName}-alb-tg
- Key: Env
Value: !Sub ${EnvName}
TargetGroupAttributes:
- Key: "stickiness.enabled"
Value: false
Targets:
- Id:
Fn::ImportValue: !Sub ${SystemName}-${EnvName}-web
Port: 80
# ------------------------------------------------------------#
# internet-facing ALB
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}InternetFacingALB:
Type: "AWS::ElasticLoadBalancingV2::LoadBalancer"
Properties:
Name: !Sub ${SystemName}-${EnvName}-alb
Tags:
- Key: Name
Value: !Sub ${SystemName}-${EnvName}-alb
- Key: Env
Value: !Sub ${EnvName}
Scheme: "internet-facing"
LoadBalancerAttributes:
- Key: "deletion_protection.enabled"
Value: true
- Key: access_logs.s3.enabled
Value: true
- Key: access_logs.s3.bucket
Value: !Ref {{SystemName}}{{EnvName}}ALBLogsBucket
SecurityGroups:
- Fn::ImportValue: !Sub ${SystemName}-${EnvName}-alb-sg
Subnets:
- !Ref {{SystemName}}{{EnvName}}ALBPublicSubnet1aId
- !Ref {{SystemName}}{{EnvName}}ALBPublicSubnet1cId
Type: application
# ------------------------------------------------------------#
# HTTPS Listener
# ------------------------------------------------------------#
{{SystemName}}{{EnvName}}ALBListenerHTTPS:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Port: 443
Protocol: HTTPS
Certificates:
- CertificateArn: !Ref LoadBalancerCertificateARN
DefaultActions:
- TargetGroupArn: !Ref {{SystemName}}{{EnvName}}ALBTargetGroup
Type: forward
LoadBalancerArn: !Ref {{SystemName}}{{EnvName}}InternetFacingALB
SslPolicy: "ELBSecurityPolicy-TLS13-1-2-2021-06"
인증서 ARN을 파라미터로 전달한 뒤, 기존 스택에 대해 업데이트(Update Stack) 를 수행합니다.
CloudFormation에서는 변경 세트(Change Set)를 생성하여 어떤 리소스가 추가 또는 변경되는지 미리 확인할 수 있습니다.

변경 세트를 실행한 후 ALB 콘솔을 확인해 보면 HTTPS(443) 리스너가 정상적으로 생성된 것을 확인할 수 있습니다.

HTTPS 리스너가 생성되면서 기본 동작(Default Action)으로 Target Group이 연결되고, ALB는 등록된 대상에 대한 헬스 체크를 시작합니다.
이후 Target Group 상태를 확인해 보면 기존의 Unused 상태가 해제되고, 헬스 체크가 정상적으로 수행되어 Healthy 상태로 전환된 것을 확인할 수 있습니다.

또한 이 시점부터는 ALB와 Target Group 관련 CloudWatch 메트릭이 생성되기 시작합니다. 따라서 기존에 구성해 두었던 CloudWatch 알람 역시 정상적으로 동작하게 됩니다.
예를 들어 대상 서버에 장애가 발생하면 UnHealthyHostCount 메트릭이 증가할 수 있으며, 실제 트래픽이 유입되기 시작하면 HTTPCode_ELB_*, HTTPCode_Target_* 계열 메트릭도 수집되어 알람 조건을 평가할 수 있게 됩니다.
즉, 리스너를 추가하는 순간 단순히 HTTPS가 활성화되는 것뿐만 아니라 ALB와 Target Group 간의 연결이 완성되며, 헬스 체크와 모니터링 체계 또한 정상적으로 동작하기 시작합니다.
| 항목 | 리스너 추가 후 상태 |
|---|---|
| Target Group 상태 | Healthy 또는 Initial |
| 헬스 체크 | 수행됨 |
| UnHealthyHostCount | 수집 가능 |
| HTTPCode_ELB_* | 트래픽 발생 시 수집 |
| HTTPCode_Target_* | 트래픽 발생 시 수집 |
| CloudWatch 알람 | 정상 동작 |
마지막으로 DNS가 ALB를 가리키도록 설정되어 있다면, 지정된 도메인으로 접속하여 HTTPS 통신이 정상적으로 이루어지는지 확인합니다.
아래와 같이 웹 사이트가 정상적으로 표시된다면 ACM 인증서 적용과 ALB HTTPS 리스너 구성이 모두 완료된 것입니다.

마무리
일반적으로 HTTPS를 사용하는 ALB를 구축할 때는 ACM 인증서를 먼저 발급받고, 해당 인증서를 HTTPS 리스너에 연결하여 ALB를 생성합니다. 하지만 실제 프로젝트에서는 항상 이상적인 순서로 작업할 수 있는 것은 아닙니다.
특히 신규 서비스 구축 단계에서는 도메인이 아직 준비되지 않았거나, DNS 관리 권한이 외부 업체에 있어 인증서 검증을 바로 진행할 수 없는 경우가 있습니다. 또한 개발 환경이나 테스트 환경을 먼저 구성해야 하는 상황에서는 인증서 발급 일정을 기다리기보다 ALB와 Target Group 등의 인프라를 먼저 구축해야 할 때도 있습니다.
문제는 ACM 인증서가 검증 완료 상태가 아니면 ALB의 HTTPS 리스너에 연결할 수 없다는 점입니다. ACM 인증서 요청 후 72시간 이내에 도메인 검증이 완료되지 않으면 인증서 요청이 만료되며, 검증 대기 중이거나 검증에 실패한 인증서는 HTTPS 리스너에 사용할 수 없습니다.
이번 글에서는 이러한 제약 사항을 우회하기 위해 먼저 ALB를 생성하고, 이후 ACM 인증서 발급이 완료된 시점에 CloudFormation 스택 업데이트를 통해 HTTPS 리스너를 추가하는 방법을 살펴보았습니다.
정리하면 전체 작업 순서는 다음과 같습니다.
- ALB와 Target Group을 생성한다.
- HTTPS 리스너는 생성하지 않는다.
- ACM 인증서를 요청한다.
- DNS 검증 또는 이메일 검증을 완료한다.
- 인증서 상태가
Issued가 되면 HTTPS 리스너를 추가한다. - CloudFormation 스택을 업데이트한다.
- ALB와 Target Group이 연결되면서 헬스 체크 및 모니터링이 활성화된다.
- HTTPS 접속이 정상 동작하는지 확인한다.
이 방식의 가장 큰 장점은 인프라 구축 일정과 인증서 발급 일정을 분리할 수 있다는 점입니다. ALB, Target Group, 보안 그룹, 로그 버킷, 모니터링 설정 등 대부분의 인프라 작업을 먼저 완료한 후 인증서 발급이 끝나는 시점에 최소한의 변경만으로 HTTPS를 활성화할 수 있습니다.
또한 CloudFormation을 사용하고 있다면 별도의 리소스 재생성 없이 스택 업데이트만으로 HTTPS 리스너를 추가할 수 있으므로 운영 환경에서도 비교적 안전하게 적용할 수 있습니다. 실제로 이번 예제에서도 기존 ALB를 삭제하거나 새로 생성하지 않고 HTTPS 리스너 리소스만 추가하여 서비스 구성을 완성할 수 있었습니다.
다만 몇 가지 주의해야 할 점도 있습니다.
첫째, 리스너가 없는 상태의 Target Group은 Unused 상태가 되며 헬스 체크가 수행되지 않습니다. 따라서 UnHealthyHostCount와 같은 일부 CloudWatch 메트릭은 생성되지 않거나 정상적으로 수집되지 않을 수 있습니다.
둘째, 실제 트래픽이 발생하지 않는 환경에서는 HTTP 상태 코드 관련 메트릭(HTTPCode_ELB_*, HTTPCode_Target_*) 역시 수집되지 않습니다. 따라서 모니터링 및 알람 정책을 설계할 때 이러한 초기 상태를 고려해야 합니다.
셋째, ACM 인증서는 반드시 ALB와 동일한 리전에 생성되어 있어야 하며, 인증서 상태가 Issued인 경우에만 HTTPS 리스너에 연결할 수 있습니다.
결론적으로 ACM 인증서 발급이 지연되거나 도메인 검증 일정을 별도로 관리해야 하는 환경에서는 "ALB 먼저 생성 → ACM 발급 → HTTPS 리스너 추가" 방식이 충분히 활용 가능한 운영 방법입니다. 특히 CloudFormation 기반으로 인프라를 관리하는 환경이라면 리소스 재생성 없이 스택 업데이트만으로 HTTPS를 활성화할 수 있으므로 구축 일정의 유연성을 높일 수 있습니다.
비록 일반적인 구축 순서는 아니지만, 실제 프로젝트에서는 자주 마주치는 상황이며 알아두면 인프라 구축 일정과 인증서 발급 일정을 효율적으로 분리하여 관리할 수 있습니다.












