CloudFront SaaS Manager · AWS CLI 가이드

B2B 고객사의 커스텀 도메인 연결과 SSL 인증서 자동 발급·갱신을 CloudFront SaaS Manager로 구현할 때 실제로 호출하는 명령을 순서대로 정리했습니다. 시연용 시뮬레이터(saas-demo.feelz.online)는 아래 명령과 같은 API를 SDK로 호출합니다.

0. 구성 요소와 동작 원리

Cloudflare for SaaS의 "Custom Hostname"에 해당하는 것이 Distribution Tenant입니다. 공통 설정은 멀티테넌트 배포 하나에 두고, 고객 도메인마다 테넌트를 추가합니다.

구성 요소역할Cloudflare 대응
멀티테넌트 배포
ConnectionMode: tenant-only
모든 고객 도메인이 공유하는 템플릿. 오리진, 캐시 정책, HTTP→HTTPS 리다이렉트, WAF를 한 번만 설정Zone 공통 설정 / Fallback origin
연결 그룹 (Connection Group)고객이 CNAME으로 가리키는 단일 주소(RoutingEndpoint, 예: d10qyq5uv8bdn4.cloudfront.net). 이 주소는 CNAME 대상일 뿐 직접 여는 주소가 아닙니다CNAME target
Distribution Tenant고객 도메인 1개(이상). Host 헤더로 식별되며 부모 배포 설정을 상속. 인증서·WAF·지역 제한만 개별 override 가능Custom hostname
관리형 인증서 (ManagedCertificateRequest)CloudFront가 ACM 퍼블릭 인증서를 대신 요청·검증·자동 갱신. 고객은 CNAME 하나만 등록SSL for SaaS 인증서
브라우저 https://shop.customer.com/ │ DNS: shop.customer.com CNAME d10qyq5uv8bdn4.cloudfront.net ← 고객이 등록하는 유일한 레코드 ▼ 연결 그룹 RoutingEndpoint ── Host 헤더로 테넌트 식별, 그 테넌트의 인증서로 TLS ▼ Distribution Tenant (shop.customer.com, 관리형 ACM 인증서) ▼ 멀티테넌트 배포 (템플릿: redirect-to-https, 캐시 정책, 오리진) ▼ 오리진 (ALB / Lambda URL / S3 …) ── Host 값으로 어느 고객인지 구분
온보딩 순서가 중요합니다. ① 고객 DNS에 CNAME 등록 → ② 테넌트 생성(관리형 인증서 요청) → ③ 인증서 발급 확인 → ④ 인증서 연결 → ⑤ 도메인 active. CNAME보다 먼저 테넌트를 만들면 소유권 검증 실패로 거부됩니다(3절 참고).

1. 사전 준비 (SaaS 운영자, 1회) 멀티테넌트 배포 · 연결 그룹

1멀티테넌트 배포 생성

일반 배포와 같은 create-distribution이지만 ConnectionMode를 tenant-only로 둡니다. 배포 자체의 도메인은 없고(DomainName: -), 테넌트 도메인으로만 서비스됩니다. ViewerProtocolPolicy: redirect-to-https가 "HTTP 접속 시 HTTPS 자동 전환"을 담당합니다.

cat > mt-distribution.json <<'JSON'
{
  "CallerReference": "saas-mt-2026-09",
  "Comment": "SaaS multi-tenant distribution (template for tenants)",
  "Enabled": true,
  "ConnectionMode": "tenant-only",
  "HttpVersion": "http2and3",
  "ViewerCertificate": {
    "ACMCertificateArn": "arn:aws:acm:us-east-1:<ACCOUNT>:certificate/<TEMPLATE-CERT>",
    "SSLSupportMethod": "sni-only",
    "MinimumProtocolVersion": "TLSv1.2_2021"
  },
  "Origins": { "Quantity": 1, "Items": [ {
    "Id": "saas-origin",
    "DomainName": "<origin-alb-or-lambda-url-host>",
    "CustomOriginConfig": { "HTTPPort": 80, "HTTPSPort": 443, "OriginProtocolPolicy": "https-only",
                            "OriginSslProtocols": { "Quantity": 1, "Items": ["TLSv1.2"] } }
  } ] },
  "DefaultCacheBehavior": {
    "TargetOriginId": "saas-origin",
    "ViewerProtocolPolicy": "redirect-to-https",
    "AllowedMethods": { "Quantity": 3, "Items": ["GET","HEAD","OPTIONS"], "CachedMethods": { "Quantity": 2, "Items": ["GET","HEAD"] } },
    "Compress": true,
    "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad",
    "OriginRequestPolicyId": "b689b0a8-53d0-40ab-baf2-68738e2966ac"
  },
  "TenantConfig": { "ParameterDefinitions": [ { "Name": "tenantName",
    "Definition": { "StringSchema": { "Comment": "tenant id, usable as {{tenantName}} in origin path", "DefaultValue": "root", "Required": false } } } ] }
}
JSON
aws cloudfront create-distribution --distribution-config file://mt-distribution.json \
  --query 'Distribution.[Id,DistributionConfig.ConnectionMode]' --output text
# → E1234567890ABC  tenant-only

캐시 정책 4135ea2d…는 관리형 CachingDisabled, 오리진 요청 정책 b689b0a8…는 관리형 AllViewerExceptHostHeader입니다. 정적 자산이 많으면 경로별 behavior와 캐시 정책을 추가합니다. ViewerCertificate는 tenant-only 배포에도 필요합니다(CloudFormation에서는 생략 시 생성 실패).

2연결 그룹 생성과 RoutingEndpoint 확인

aws cloudfront create-connection-group --name saas-prod-cg --enabled --ipv6-enabled \
  --query 'ConnectionGroup.[Id,RoutingEndpoint]' --output text
# → cg_3JhYw92ZATvxAiJ08zR8sDnkLYx   d10qyq5uv8bdn4.cloudfront.net

# 이미 있는 그룹 조회
aws cloudfront list-connection-groups --query 'ConnectionGroups[].[Id,Name,RoutingEndpoint,IsDefault]' --output table
aws cloudfront get-connection-group --identifier cg_3JhYw92ZATvxAiJ08zR8sDnkLYx --query 'ConnectionGroup.RoutingEndpoint' --output text

RoutingEndpoint가 고객에게 안내할 CNAME 값입니다. 연결 그룹을 지정하지 않으면 계정 기본 그룹이 쓰이지만, 운영에서는 명시적으로 만들어 두는 것이 좋습니다. 고정 IP가 필요한 고객이 있으면 Anycast IP 목록을 연결 그룹에 붙일 수 있습니다.

2. 고객 DNS 설정 (고객사가 수행) CNAME 1개

SaaS 관리 화면에서 고객에게 아래 레코드를 안내합니다. 실제 고객은 자사 DNS(가비아, Cloudflare, Route 53 등)에 등록하고, 시연에서는 Route 53에 대신 등록합니다.

경우타입이름값
서브도메인 (일반)CNAMEshop.customer.comd10qyq5uv8bdn4.cloudfront.net
Apex 도메인 (CNAME 불가)A/AAAA Alias (Route 53)customer.comd10qyq5uv8bdn4.cloudfront.net
TXT_cf-challenge.customer.comd10qyq5uv8bdn4.cloudfront.net
# Route 53 예시 (시뮬레이션): 고객 서브도메인 CNAME
aws route53 change-resource-record-sets --hosted-zone-id <ZONE-ID> --change-batch '{
  "Changes": [ { "Action": "UPSERT", "ResourceRecordSet": {
    "Name": "shop.customer.com", "Type": "CNAME", "TTL": 60,
    "ResourceRecords": [ { "Value": "d10qyq5uv8bdn4.cloudfront.net" } ] } } ] }'

# 전파 확인은 권한 있는 네임서버에 직접 (로컬 리졸버의 NXDOMAIN 부정 캐시 회피)
dig +short @ns-716.awsdns-25.net shop.customer.com CNAME
레코드를 만들기 전에 그 도메인을 브라우저나 dig로 조회하지 마십시오. 없는 이름을 조회하면 리졸버가 NXDOMAIN을 SOA TTL(Route 53 기본 900초)만큼 캐시해 이후 확인이 최대 15분 늦어집니다. 확인은 권한 있는 네임서버(@ns-…)로 합니다.

3. 테넌트 생성 + 관리형 인증서 요청 (SaaS 온보딩 API) CreateDistributionTenant

고객이 DNS를 등록했다는 신호를 받으면 SaaS 백엔드가 이 명령 하나를 호출합니다. ManagedCertificateRequest가 있으면 CloudFront가 ACM 인증서를 대신 요청하고 갱신까지 관리합니다.

aws cloudfront create-distribution-tenant \
  --distribution-id E1234567890ABC \
  --connection-group-id cg_3JhYw92ZATvxAiJ08zR8sDnkLYx \
  --name customer-shop \
  --domains Domain=shop.customer.com \
  --enabled \
  --managed-certificate-request ValidationTokenHost=cloudfront,PrimaryDomainName=shop.customer.com,CertificateTransparencyLoggingPreference=enabled \
  --parameters Name=tenantName,Value=customer-123 \
  --tags 'Items=[{Key=customer,Value=customer-123}]' \
  --query 'DistributionTenant.[Id,Status,Domains[0].Status]' --output text
# → dt_3JhcwGuRQdQpkNCUVsEL4UuSam3   InProgress   inactive
옵션의미
ValidationTokenHost=cloudfront고객 CNAME이 연결 그룹을 가리키면 CloudFront가 HTTP 검증 토큰(/.well-known/pki-validation/…)을 직접 응답해 ACM 검증을 통과시킵니다. 새 도메인, 트래픽 없는 도메인에 적합
ValidationTokenHost=self-hosted기존 트래픽이 있어 CNAME을 먼저 바꿀 수 없을 때. 고객의 현재 웹서버가 토큰 경로를 ACM으로 301 리다이렉트해 검증한 뒤 CNAME을 전환(무중단 이관)
--parameters tenantName배포의 TenantConfig에 정의한 파라미터. 오리진 경로 /{{tenantName}} 등에 치환되어 테넌트별 라우팅에 사용
--customizations (선택)테넌트별 WAF ARN, 지역 제한, 기존 ACM 인증서 override
CNAME이 아직 CloudFront를 가리키지 않으면 다음 오류로 거부됩니다.
InvalidArgument: The provided Domain Name is not valid. Could not verify Domain Name ownership. It may not be pointing to a valid CloudFront resource.
온보딩 화면은 DNS 전파를 확인한 뒤 호출하고, 실패하면 잠시 후 재시도하도록 구현합니다.

4. 상태 확인 Get / List

# 테넌트 상태 (Status: InProgress → Deployed, Domains[].Status: inactive → active)
aws cloudfront get-distribution-tenant --identifier dt_3JhcwGuRQdQpkNCUVsEL4UuSam3 \
  --query 'DistributionTenant.{Status:Status,Enabled:Enabled,Domains:Domains,Cert:Customizations.Certificate.Arn}' --output json

# 관리형 인증서 상태 (pending-validation → issued) 와 검증 토큰 경로
aws cloudfront get-managed-certificate-details --identifier dt_3JhcwGuRQdQpkNCUVsEL4UuSam3 --output json

# 배포에 속한 테넌트 전체
aws cloudfront list-distribution-tenants --association-filter DistributionId=E1234567890ABC \
  --query 'DistributionTenantList[].[Name,Domains[0].Domain,Domains[0].Status,Status]' --output table

# 도메인으로 테넌트 찾기 / DNS 설정 검증
aws cloudfront get-distribution-tenant-by-domain --domain shop.customer.com --query 'DistributionTenant.Id' --output text
aws cloudfront verify-dns-configuration --domain shop.customer.com --identifier dt_3JhcwGuRQdQpkNCUVsEL4UuSam3

실측 소요 시간: 테넌트 생성 → 인증서 issued 약 5~10분. 온보딩 화면은 5~10초 간격으로 폴링하면 충분합니다.

5. 인증서 연결 (API 직접 호출 시 필요) UpdateDistributionTenant

관리형 인증서가 issued가 되어도 API 경로에서는 테넌트에 자동으로 붙지 않습니다. Customizations.Certificate가 비어 있으면 도메인은 inactive(HTTP 403, TLS 실패)에 머무릅니다. 발급된 인증서 ARN을 연결하면 약 1분 내 active가 됩니다.

TID=dt_3JhcwGuRQdQpkNCUVsEL4UuSam3
CERT=$(aws cloudfront get-managed-certificate-details --identifier $TID --query 'ManagedCertificateDetails.CertificateArn' --output text)
ETAG=$(aws cloudfront get-distribution-tenant --identifier $TID --query ETag --output text)

aws cloudfront update-distribution-tenant --id $TID --if-match $ETAG \
  --distribution-id E1234567890ABC --connection-group-id cg_3JhYw92ZATvxAiJ08zR8sDnkLYx \
  --domains Domain=shop.customer.com --enabled \
  --customizations "Certificate={Arn=$CERT}"
CloudFormation의 AWS::CloudFront::DistributionTenant 리소스를 쓰면 이 단계를 리소스 핸들러가 대신 처리합니다(스택 완료 시 이미 active). 코드로 온보딩할 때만 이 호출을 넣습니다. 시뮬레이터는 발급을 감지해 자동으로 수행합니다.

6. 접속 검증 (고객 시연) HTTPS · 자동 전환 · 인증서

# DNS
dig +short shop.customer.com            # → d10qyq5uv8bdn4.cloudfront.net → CloudFront IP

# HTTP → HTTPS 자동 전환 (배포의 ViewerProtocolPolicy)
curl -sI http://shop.customer.com/ | grep -iE "^HTTP|^location"
# HTTP/1.1 301 Moved Permanently
# Location: https://shop.customer.com/

# 인증서: 발급자 Amazon, SAN = 고객 도메인, 유효기간 (자동 갱신)
echo | openssl s_client -servername shop.customer.com -connect shop.customer.com:443 2>/dev/null \
  | openssl x509 -noout -issuer -subject -dates

# 오리진이 본 테넌트 호스트
curl -s https://shop.customer.com/api/whoami

브라우저 시연 동선: 주소창에 http://shop.customer.com 입력 → 주소가 https://로 바뀌는 것 확인 → 자물쇠 클릭 → 발급자 Amazon과 도메인 확인 → 서비스 화면 정상 노출.

7. 도메인 해제 (오프보딩) Disable → Delete

테넌트는 비활성화가 배포에 반영된 뒤에만 삭제할 수 있습니다. 세 단계로 진행합니다.

TID=dt_3JhcwGuRQdQpkNCUVsEL4UuSam3
ETAG=$(aws cloudfront get-distribution-tenant --identifier $TID --query ETag --output text)

# 1) 비활성화
aws cloudfront update-distribution-tenant --id $TID --if-match $ETAG \
  --distribution-id E1234567890ABC --connection-group-id cg_3JhYw92ZATvxAiJ08zR8sDnkLYx \
  --domains Domain=shop.customer.com --no-enabled

# 2) 배포 반영 대기 (Status: InProgress → Deployed, 보통 30~60초)
until [ "$(aws cloudfront get-distribution-tenant --identifier $TID --query 'DistributionTenant.Status' --output text)" = Deployed ]; do sleep 10; done

# 3) 삭제 (새 ETag 필요)
ETAG=$(aws cloudfront get-distribution-tenant --identifier $TID --query ETag --output text)
aws cloudfront delete-distribution-tenant --id $TID --if-match $ETAG

# 고객 DNS 레코드 정리는 고객사가 수행 (시연에서는 Route 53 DELETE)

삭제해도 발급된 ACM 인증서는 남습니다. 다른 CloudFront 리소스에 재사용할 수 있고, 불필요하면 aws acm delete-certificate로 정리합니다. 도메인을 다른 배포로 옮길 때는 삭제 대신 update-domain-association을 씁니다.

8. 온보딩 서비스의 최소 IAM 권한

SaaS 온보딩 백엔드(예: ECS 태스크 역할)에 필요한 권한만 부여합니다. 배포 자체를 바꾸는 권한(UpdateDistribution)은 포함하지 않습니다.

{
  "Version": "2012-10-17",
  "Statement": [
    { "Sid": "TenantLifecycle", "Effect": "Allow", "Action": [
        "cloudfront:CreateDistributionTenant", "cloudfront:UpdateDistributionTenant", "cloudfront:DeleteDistributionTenant",
        "cloudfront:GetDistributionTenant", "cloudfront:GetDistributionTenantByDomain", "cloudfront:ListDistributionTenants",
        "cloudfront:GetManagedCertificateDetails", "cloudfront:VerifyDnsConfiguration", "cloudfront:ListDomainConflicts",
        "cloudfront:TagResource", "cloudfront:UntagResource", "cloudfront:ListTagsForResource" ], "Resource": "*" },
    { "Sid": "ReadShared", "Effect": "Allow", "Action": [
        "cloudfront:GetConnectionGroup", "cloudfront:ListConnectionGroups", "cloudfront:GetDistribution", "cloudfront:ListDistributions" ], "Resource": "*" },
    { "Sid": "ManagedCertificates", "Effect": "Allow", "Action": [
        "acm:RequestCertificate", "acm:DescribeCertificate", "acm:ListCertificates", "acm:AddTagsToCertificate", "acm:GetCertificate" ], "Resource": "*" },
    { "Sid": "ServiceLinkedRole", "Effect": "Allow", "Action": "iam:CreateServiceLinkedRole", "Resource": "*",
      "Condition": { "StringLike": { "iam:AWSServiceName": "cloudfront.amazonaws.com" } } }
  ]
}

9. 주의사항 · 한도 · 비용

항목내용
순서DNS CNAME → 테넌트 생성. 반대로 하면 Could not verify Domain Name ownership
인증서 연결API 경로에서는 발급 후 UpdateDistributionTenant로 연결해야 active. CloudFormation은 자동
대기 중 인증서테넌트당 pending 인증서 요청은 1개. 추가 도메인 요청 전 기존 요청을 취소해야 함
도메인 중복같은 도메인은 계정 내 다른 배포·테넌트와 동시에 쓸 수 없음(ListDomainConflicts로 사전 확인)
와일드카드부모 배포의 공유 인증서에 포함되어 있거나, 테넌트에 기존 ACM 인증서를 지정할 때만
Apex 도메인Alias(A/AAAA) + _cf-challenge.<domain> TXT로 소유 증명
부정 캐시레코드 생성 전 조회 금지. 확인은 권한 있는 NS에 직접
비용테넌트 10개 무료 · 11~200개 월 $20 정액 · 201개부터 개당 $0.10. 관리형 인증서 무료. 트래픽은 배포와 동일 요율
비용 비교Cloudflare for SaaS: 100 호스트 무료, 이후 $0.10/호스트/월

10. Cloudflare for SaaS 대응표

CloudflareCloudFront SaaS Manager비고
Custom Hostname 생성 (API)create-distribution-tenantCNAME 선행 필요
Fallback origin / CNAME target연결 그룹 RoutingEndpoint계정당 여러 그룹 가능, Anycast IP 선택
SSL for SaaS 인증서 (자동 발급·갱신)ManagedCertificateRequest + 연결HTTP 토큰 검증, CloudFront가 갱신
Custom hostname 상태 조회get-distribution-tenant, get-managed-certificate-details
Hostname 삭제비활성화 → delete-distribution-tenant2단계
Always Use HTTPS배포 ViewerProtocolPolicy: redirect-to-https테넌트가 상속
Hostname별 WAF / 지역 제한테넌트 Customizations (WebAcl, GeoRestrictions)배포 기본값 override