GoodBye, AWS
이사가자 이사, Azure 이사기
지난 글에서 Zuzoo 운영 서버를 AWS에서 Azure로 옮긴 과정을 정리했다.
Zuzoo가 안정적으로 돌기 시작하니, 이번엔 이 블로그도 같이 옮기기로 했다.
이유는 단순하다. AWS 인스턴스 한 대를 굳이 살려둘 이유가 없어졌고, Azure 5,000달러 크레딧 안에서 한 곳에 모아둬야 운영도 비용 추적도 편하다.
근데 막상 시작하니 Zuzoo와는 결정적으로 다른 두 가지 제약이 있었다.
- 이 사이트는 100% 정적 사이트 — DB도, 백엔드도, KV도 없다. 그냥 nginx가 정적 파일 3종(React 빌드 + Astro 블로그 + Astro 서브노트)을 path로 분기해서 서빙할 뿐.
- slowflowsoft.com 도메인은 Route 53에서 내려놓을 수가 없다 —
api.zuzoo.slowflowsoft.com이 같은 zone에 묶여 있어서, NS를 옮기면 Zuzoo API가 같이 영향을 받는다.
1번은 인프라를 단순화할 수 있는 호재였지만, 2번이 의외로 큰 제약이 됐다. 결국 AWS ALB 하나만 redirect 용도로 살려두는 하이브리드 구조로 정리됐고, 이번 글은 그 셋업을 처음부터 끝까지 정리한다.
들어가기 전에 — 이번 인프라 구조
이번에 잡은 구조는 이렇게 생겼다.
사용자
│
├─── slowflowsoft.com (apex)
│ │
│ ▼
│ Route 53 (Alias)
│ │
│ ▼
│ AWS ALB (살려둠, redirect 전용)
│ │
│ ▼
│ 301 → https://www.slowflowsoft.com
│
└─── www.slowflowsoft.com
│
▼
Route 53 (CNAME)
│
▼
Azure Front Door Premium (zuzoo-prod-fd, Zuzoo와 공유)
│ (endpoint: slowflowsoft-web)
▼
Container App (slowflowsoft-web, Container Apps Env 분리)
└── ACR (zuzooprodacr, Zuzoo와 공유)
선택 기준은 세 가지였다.
- 정적 사이트라 lift-and-shift — 기존 nginx Dockerfile을 그대로 옮긴다. Storage Static Website나 SWA 같은 옵션도 검토했지만, 셋업 변경이 너무 컸다.
- Zuzoo 인프라 재활용 — ACR / Front Door는 공유. 단 Container Apps Environment와 Log Analytics는 분리해서 blast radius 격리.
- 도메인은 손대지 않는다 — slowflowsoft.com은 Route 53 그대로. apex만 redirect 트릭으로 처리.
이제 셋업을 단계별로 정리한다.
아직 Azure와 친하지가 않으니… 각 명령어의 옵션이 무엇을 의미하는지, 왜 그 값을 골랐는지까지도 함께 정리해보자.
1. 왜 Container Apps + nginx lift-and-shift였나
처음엔 Storage Account의 Static Website 기능이 끌렸다. 정적 사이트 호스팅용으로 설계된 거고, 비용도 거의 0에 가깝다.
근데 이 사이트는 한 도메인 아래 세 개의 별개 빌드(React + Astro + Astro) 를 path로 분기해서 서빙해야 하고, nginx의 try_files $uri $uri/ /index.html 같은 SPA fallback이 path별로 다르게 들어가 있다.
이걸 Storage Static Website로 옮기려면:
- 빌드 시점에 dist 3개를 한 디렉토리로 합치고
- SPA fallback을 Front Door rewrite rule로 path별로 다시 짜고
- 각 path의 access policy를 설정하고
— 결국 검증된 nginx.conf를 버리고 새 mental model에서 다시 시작해야 한다. 정적이라는 이점은 얻지만 변경 비용이 너무 컸다.
그리고, 무엇보다… 100% 정적인 사이트라 블로그에 글 하나 쓰려면 다시 컨테이너를 빌드하는데, 배포가 너무 귀찮았다.
반면 Container Apps + 기존 Dockerfile 은:
- 기존 Dockerfile 의 multi-stage build (react-builder → blog-builder → subnotes-builder → nginx) 그대로
- nginx.conf의 path routing / SPA fallback 그대로 동작
az acr build .한 줄이면 빌드/푸시 끝- Zuzoo와 같은 ACR / Front Door / 학습한 az 명령 다 재사용
비용 차이도 사실상 무의미했다.
Container App을 0.25 vCPU / 0.5 GiB로 잡고 min-replicas 1로 두면 월 $10 내외.
Front Door Premium $235가 메인 비용이라 컨테이너 비용 차이는 노이즈 수준이고, 어차피 5,000달러 크레딧 안에서 흡수된다.
결정은 빠르게 났다. lift-and-shift.
2. Container App 생성 — 옵션 하나하나
먼저 이미지를 ACR에 빌드한다. ACR Cloud Build를 쓰면 로컬 Docker가 필요 없고, M1 맥에서도 amd64 image가 자동으로 빌드된다.
az acr build --registry zuzooprodacr --image slowflowsoft-web:v1 --file Dockerfile .
| 옵션 | 의미 |
|---|---|
--registry zuzooprodacr | Zuzoo와 공유하는 ACR. Basic SKU($5/월) 한 개에 여러 image repository를 둘 수 있다. |
--image slowflowsoft-web:v1 | repository 이름과 태그. 같은 태그를 덮어쓰면 Container App이 새 이미지를 못 당기는 케이스가 있어 항상 새 태그로. |
--file Dockerfile | build context의 root에 있는 Dockerfile 명시. multi-stage build의 모든 stage가 이 한 파일에 들어 있다. |
. | build context. ACR이 .dockerignore 따라 이 디렉토리를 통째로 클라우드에 업로드한 뒤 클라우드에서 빌드한다. |
빌드된 이미지를 Container App에 띄운다.
az containerapp create `
--name slowflowsoft-web --resource-group Slowflowsoft-Web-RG `
--environment slowflowsoft-web-env `
--image zuzooprodacr.azurecr.io/slowflowsoft-web:v1 `
--target-port 80 --ingress external `
--min-replicas 1 --max-replicas 3 `
--cpu 0.25 --memory 0.5Gi `
--registry-server zuzooprodacr.azurecr.io --registry-identity system `
--system-assigned `
--revisions-mode single `
--scale-rule-name http-concurrency `
--scale-rule-type http --scale-rule-http-concurrency 100
옵션 하나씩 풀어보면,
| 옵션 | 의미 / 선택 이유 |
|---|---|
--target-port 80 | 컨테이너 안의 nginx가 listen하는 포트. Container Apps의 ingress가 이 포트로 traffic을 보낸다. |
--ingress external | 외부 traffic을 받을지 여부. internal 이면 같은 Container Apps Environment 안에서만 호출 가능. 우리는 Front Door를 통해 외부 노출이 필요하니 external. |
--min-replicas 1 | 최소 유지 replica 수. 0도 가능(cold start 감수, 비용 0)하지만 검색 봇이 늘 들어오는 사이트라 1로. |
--max-replicas 3 | scale-out 한계. 정적 사이트라 트래픽이 폭증해도 nginx 1대가 꽤 버틴다. 3이면 충분. |
--cpu 0.25 --memory 0.5Gi | replica당 자원. 정적 파일 서빙은 CPU 부하가 거의 없어서 최소 단위로 설정. |
--registry-server | 이미지를 당겨올 레지스트리. ACR FQDN. |
--registry-identity system | ACR pull 시 password 대신 system managed identity를 쓴다. Container App을 만들 때 자동 발급되는 identity가 ACR에 AcrPull 권한을 받아서 안전하게 image를 당긴다. |
--system-assigned | Container App 자체에 system managed identity 부여. 위 registry-identity system이 동작하려면 이게 켜져 있어야 한다. |
--revisions-mode single | 새 revision이 만들어지면 옛 revision을 자동 deactivate. 기본값은 multiple 인데 옛 revision이 누적되어 디버깅이 헷갈린다. 운영은 single 권장. |
--scale-rule-name http-concurrency | scale rule 식별자. 임의 이름. |
--scale-rule-type http | KEDA scale rule type. HTTP concurrency 기반으로 scale-out. |
--scale-rule-http-concurrency 100 | replica당 동시 처리 100요청을 넘으면 새 replica를 spawn. 정적 사이트는 응답이 빨라서 100을 잡아도 여유롭다. |
PowerShell에서 줄을 backtick(`)으로 잇고 있는데, 한 줄로 다 써도 된다. 길어서 가독성 위해 분리.
ACR pull 권한 — Portal 흐름이 더 안전
위 명령으로 Container App이 만들어졌어도, 이 시점엔 system identity가 ACR에 권한이 없어서 image pull이 실패한다. 권한을 부여해야 한다.
CLI로 시도하면 이렇게 된다.
$PRINCIPAL_ID = az containerapp show -n slowflowsoft-web -g Slowflowsoft-Web-RG --query identity.principalId -o tsv
$ACR_ID = az acr show -n zuzooprodacr --query id -o tsv
az role assignment create --assignee $PRINCIPAL_ID --role AcrPull --scope $ACR_ID
작동하면 가장 빠른데, fresh subscription이나 사용자 권한이 약한 케이스에서 MissingSubscription 또는 Cannot find user or service principal in graph database 에러로 silently 실패하는 일이 잦다. 그땐 Portal로:
- portal.azure.com → 검색
zuzooprodacr→ 클릭 - 좌측 Access control (IAM) → + Add → Add role assignment
- Role: AcrPull → Next
- Assign access to: Managed identity → + Select members → Subscription 선택 → Managed identity 종류: Container apps →
slowflowsoft-web선택 - Review + assign
권한 전파에 1~2분 걸리고, 그 사이에 Container App이 image pull 재시도를 하지 않으면 revision suffix를 달아 강제 재시도한다.
az containerapp update -n slowflowsoft-web -g Slowflowsoft-Web-RG --revision-suffix retry1
3. nginx.conf 의 server_name 함정
Container App이 떠도 한 번 막혔다. Container App의 기본 FQDN(slowflowsoft-web.salmonground-...koreacontainerapps.io) 으로 접속하니 404가 떨어졌다.
로그를 보니:
"/etc/nginx/html/index.html" is not found (2: No such file or directory)
이상하다. /etc/nginx/html 은 nginx의 default root지, 우리가 nginx.conf에 명시한 /usr/share/nginx/html 이 아니다.
원인은 nginx.conf의 server_name 이었다. 두 server 블록이 있었는데:
- 첫 번째:
server_name _;(catch-all, Let’s Encrypt 검증용 흔적, root 미명시) - 두 번째:
server_name slowflowsoft.com www.slowflowsoft.com;(root 명시)
Container App FQDN으로 들어온 요청은 두 번째 server 블록의 server_name 에 매칭되지 않아 첫 번째 catch-all로 떨어졌고, 거기엔 root 도 location / 도 없어서 nginx의 default 경로(/etc/nginx/html) 를 찾다가 404를 뱉은 거다.
해결은 단순했다. 첫 번째 catch-all 블록(이제 Front Door가 TLS를 다 해주니 Let’s Encrypt 흔적은 불필요) 을 통째로 지우고, 두 번째 블록의 server_name 을 _ 로 바꿔서 모든 host를 받게 했다.
server {
listen 80 default_server;
server_name _;
root /usr/share/nginx/html;
index index.html;
# ... blog / subnotes / SPA fallback 그대로
}
이게 들어간 뒤로 Container App FQDN, Front Door endpoint, custom domain 어느 host로 들어와도 같은 server 블록이 정상 응답한다.
4. Front Door — endpoint / origin / route 옵션
Container App의 기본 FQDN으로 직접 접속해서 모든 path가 정상이면, Front Door 앞에 끼운다.
이 사이트는 Zuzoo가 이미 쓰는 Front Door Premium profile 을 그대로 공유한다.
profile은 한 개만 있어도 그 안에 여러 endpoint를 두고 도메인별로 분리할 수 있다. WAF 비용이 SKU에 포함이라, 도메인이 늘어도 추가 청구가 없다.
4-1. Endpoint 생성
az afd endpoint create `
--resource-group Zuzoo-RG `
--profile-name zuzoo-prod-fd `
--endpoint-name slowflowsoft-web `
--enabled-state Enabled
| 옵션 | 의미 |
|---|---|
--profile-name zuzoo-prod-fd | 공유하는 profile. |
--endpoint-name slowflowsoft-web | 이 endpoint의 식별자. *.azurefd.net hostname의 prefix가 된다. |
--enabled-state Enabled | 처음부터 traffic 받게. |
4-2. Origin Group + health probe
az afd origin-group create `
--resource-group Zuzoo-RG --profile-name zuzoo-prod-fd `
--origin-group-name slowflowsoft-origins `
--probe-path / --probe-protocol Https --probe-request-type GET `
--probe-interval-in-seconds 30 `
--sample-size 4 --successful-samples-required 3 `
--additional-latency-in-milliseconds 50
| 옵션 | 의미 |
|---|---|
--probe-path / | health check 경로. 정적 사이트라 root가 항상 200이면 healthy. |
--probe-protocol Https | backend가 HTTPS로 응답하니 HTTPS로 probe. |
--probe-request-type GET | HEAD가 아니라 GET. 일부 origin이 HEAD 응답을 다르게 처리해서 안전하게 GET. |
--probe-interval-in-seconds 30 | 30초마다 probe. |
--sample-size 4 | 최근 4번의 probe 결과를 sliding window로 본다. |
--successful-samples-required 3 | 4번 중 3번이 성공이면 healthy. 일시적인 1번 실패는 무시. |
--additional-latency-in-milliseconds 50 | latency 비교 시 origin 간 50ms까지는 동일하게 본다. multi-origin 부하 분산용. |
4-3. Origin (실제 backend) 등록
$APP_FQDN = az containerapp show -n slowflowsoft-web -g Slowflowsoft-Web-RG --query properties.configuration.ingress.fqdn -o tsv
az afd origin create `
--resource-group Zuzoo-RG --profile-name zuzoo-prod-fd `
--origin-group-name slowflowsoft-origins `
--origin-name slowflowsoft-web-origin `
--host-name $APP_FQDN --origin-host-header $APP_FQDN `
--priority 1 --weight 1000 `
--enabled-state Enabled `
--https-port 443 --http-port 80
| 옵션 | 의미 |
|---|---|
--host-name $APP_FQDN | Front Door가 connect할 backend 호스트네임. |
--origin-host-header $APP_FQDN | backend로 보낼 Host 헤더 값. nginx가 catch-all (server_name _;) 이라 어떤 값이든 OK지만, 명시해두면 디버깅에 유리. |
--priority 1 | 우선순위. 동일 priority origin들 사이에선 weight로 분배, 다른 priority면 fallback. multi-origin이 아니라 1만. |
--weight 1000 | 가중치. 우리는 origin 1개라 의미 없지만 default 값. |
4-4. Route 추가
az afd route create `
--resource-group Zuzoo-RG --profile-name zuzoo-prod-fd `
--endpoint-name slowflowsoft-web `
--route-name default `
--origin-group slowflowsoft-origins `
--supported-protocols Http Https `
--patterns-to-match "/*" `
--forwarding-protocol HttpsOnly `
--link-to-default-domain Enabled `
--https-redirect Enabled
| 옵션 | 의미 |
|---|---|
--supported-protocols Http Https | edge에서 HTTP/HTTPS 둘 다 받는다. HTTP로 들어오면 아래 --https-redirect 로 자동 redirect. |
--patterns-to-match "/*" | 모든 path를 이 origin group으로. blog/subnotes 분기는 nginx가 처리하니 Front Door는 통과만. |
--forwarding-protocol HttpsOnly | edge → backend는 항상 HTTPS. Container Apps의 ingress가 자동 HTTPS라 OK. |
--link-to-default-domain Enabled | *.azurefd.net default domain으로도 트래픽을 받는다. custom domain 셋업 전 검증 단계에 필수. |
--https-redirect Enabled | HTTP 요청을 HTTPS로 자동 301 redirect. |
5. Front Door 의 deployment trigger 함정
CLI로 위 객체들을 다 만들어도, *.azurefd.net hostname으로 접속하면 266 KB 짜리 깔끔한 404 HTML 이 떨어진다.
응답 헤더를 보면 단서가 있다.
HTTP/1.1 404 Not Found
Content-Length: 266478
x-cache: CONFIG_NOCACHE
CONFIG_NOCACHE 가 단서다. Front Door가 endpoint의 라우팅 설정을 아직 모른다는 뜻.
deployment 상태를 보면:
az afd route show ... --query "{deploy:deploymentStatus, provision:provisioningState}" -o tsv
# NotStarted Succeeded
provisioningState: Succeeded (객체는 만들어짐) 인데 deploymentStatus: NotStarted (글로벌 POP에 배포 안 됨) 으로 stuck.
CLI의 az afd ... update 어떤 형태로도 이 trigger를 못 푼다. patterns-to-match를 잠깐 다른 값으로 했다 되돌려도, endpoint를 disable → enable 토글해도, deploymentStatus 는 그대로 NotStarted.
해결은 Portal 에서 한 번 누르는 것뿐이었다.
- portal.azure.com → 검색창
zuzoo-prod-fd→ 클릭 - 좌측 Front Door manager
- 트리에서 endpoint(
slowflowsoft-web) 클릭 - 우측 패널의 Edit endpoint(또는 행의
⋯→ Edit) → 아무것도 변경 없이 Update
5~10분 후 *.azurefd.net 으로 접속하면 200이 떨어진다.
이게 Azure CLI의 알려진 bug라고 생각한다 — Portal의 update API가 CLI와 다른 path로 deployment cycle을 trigger 하는 것 같다. Custom domain을 새로 추가하거나 route에 link 할 때마다 같은 stuck이 재발해서, 매번 Portal을 한 번씩 들러야 했다.
6. 도메인 — 왜 Route 53를 못 떼는가
Azure 셋업이 끝나고 도메인을 옮길 차례. 여기서 가장 큰 제약을 마주쳤다.
slowflowsoft.com 의 hosted zone이 Route 53에 있고, 그 zone 안에 Zuzoo 운영 서비스가 같이 들어있다.
slowflowsoft.com 의 Route 53 hosted zone:
- slowflowsoft.com A → AWS ALB (이번에 옮김)
- www.slowflowsoft.com (없음, 새로 만들 예정)
- api.zuzoo.slowflowsoft.com CNAME → Front Door (Zuzoo, 운영 중)
- zuzoo.slowflowsoft.com CNAME → ... (Zuzoo 관련 다른 endpoint)
- mail.slowflowsoft.com MX → SES (메일)
- 그 외 SPF / DKIM / DMARC TXT
zone 통째로 Azure DNS나 Cloudflare로 옮기려면 NS 레코드를 Registrar에서 변경해야 하는데, 그러면 위에 있는 모든 레코드(특히 Zuzoo 운영 endpoint) 가 동시에 영향을 받는다. NS 전파 중간에 일시적으로 일부 resolver는 옛 NS를, 일부는 새 NS를 쓰면서 Zuzoo가 산발적으로 끊길 수 있다.
운영 중인 서비스가 같은 zone에 묶여있는 이상 NS 이전은 보류해야 했다.
apex 도메인의 문제
이 제약이 까다로운 이유는 apex 도메인(slowflowsoft.com) 은 RFC상 CNAME을 박을 수 없기 때문이다.
다른 host(www.slowflowsoft.com)야 CNAME으로 Front Door를 가리키면 끝인데, apex는 별도 처리가 필요하다.
선택지를 비교했다.
| 옵션 | 방법 | 비용 | 난이도 | 평가 |
|---|---|---|---|---|
| A | Route 53 ALIAS → Front Door | $0 | 0 | ❌ Route 53 ALIAS는 AWS 내부 리소스에만 쓸 수 있다. *.azurefd.net 같은 외부 호스트네임은 안 됨. |
| B | A record로 Front Door anycast IP 직접 박기 | $0 | 낮음 | ❌ 공식 비추. IP가 변경되면 사이트 다운. |
| C | S3 (redirect-only) + CloudFront + ACM cert + Route 53 ALIAS | ~$1/월 | 중간 | OK. 비용 거의 0이지만 객체 4개를 새로 셋업해야 함. |
| D | ALB만 살리고 EC2/Target Group 정리, listener rule을 apex → www 301 redirect로 변경 | ~$22/월 | 낮음 | ✅ 셋업 3분, AWS 의존 최소, 5,000달러 크레딧 안에서 흡수. |
| E | apex 포기, www만 운영 | $0 | 0 | ❌ 사용자 UX 손해. |
D를 골랐다. 객체 늘리지 않고 listener rule 두 개만 바꾸면 끝이고, 1인 운영에서 가장 신경 안 써도 되는 선택이었다.
ALB는 살리되, 그 뒤의 EC2와 Target Group은 정리해도 된다. Listener rule이 redirect 액션으로 동작하면 origin이 필요 없기 때문이다.
7. www custom domain + Route 53 CNAME
먼저 www를 Front Door에 등록한다.
az afd custom-domain create `
--resource-group Zuzoo-RG --profile-name zuzoo-prod-fd `
--custom-domain-name slowflowsoft-com-www `
--host-name www.slowflowsoft.com `
--certificate-type ManagedCertificate `
--minimum-tls-version TLS12
| 옵션 | 의미 |
|---|---|
--custom-domain-name slowflowsoft-com-www | Azure 리소스 식별자(점 안 됨, 하이픈만). |
--host-name www.slowflowsoft.com | 실제 도메인. |
--certificate-type ManagedCertificate | Azure가 cert를 자동 발급/갱신. ACM 같은 외부 cert 불필요. |
--minimum-tls-version TLS12 | TLS 1.0/1.1 차단. |
도메인 소유권 검증 — TXT
az afd custom-domain show ... --query validationProperties
validationToken 값을 받아서 Route 53에 TXT 레코드 추가:
| 필드 | 값 |
|---|---|
| Record name | _dnsauth.www (Route 53는 호스트만 입력) |
| Type | TXT |
| Value | <validationToken> (따옴표 없이) |
| TTL | 60 |
한 번 따옴표로 감싸서 등록했다가 검증이 영영 통과 안 되어 한참 헤맸다. AWS Console의 입력 필드는 따옴표를 자동으로 처리하지 않는다 — 토큰 그 자체만 박을 것.
Route에 custom domain 연결
$WWW_DOMAIN_ID = az afd custom-domain show ... --query id -o tsv
az afd route update ... --custom-domains slowflowsoft-com-www
여기서도 작은 함정 — --custom-domains 에 풀 ID를 줬더니 silently 실패하고 customDomains: [] 로 빈 배열이 돼버렸다. name만 줘야 정상 적용됐다. Azure CLI 문서엔 ID를 받는다고 적혀있는데 실제로는 name 기반 lookup이 우선이었다.
연결 직후엔 또 deployment stuck 함정이 발동했다 — § 5의 Portal Update를 한 번 더 해줘야 cert가 endpoint에 binding 되고 TLS 핸드셰이크에 정확한 도메인이 응답한다. 이걸 안 하면 CN=*.azureedge.net 디폴트 cert가 나와서 브라우저가 차단한다.
Route 53에 www CNAME 추가
검증 + cert binding이 모두 끝났으면 마지막으로 실제 트래픽을 보낸다.
| 필드 | 값 |
|---|---|
| Record name | www |
| Type | CNAME |
| Value | <endpoint hostname> (예: slowflowsoft-web-fvf0hfg6b7cvakb4.z01.azurefd.net) |
| TTL | 60 |
TTL 60초로 둔 이유는 컷오버 직후 문제 발견 시 빠른 롤백을 위해서다. 안정화 후엔 3600으로 올려도 된다.
8. apex → www 301 redirect (ALB listener rule)
이제 apex(slowflowsoft.com) 차례. EC2와 Target Group은 정리하고, ALB는 listener rule만 두 개 redirect로 바꾼다.
AWS Console 작업
- EC2 → Load Balancers → 그 ALB 선택 → Listeners 탭
- HTTP:80 listener의
Manage rules(또는Edit listener):- 기본 rule을 Redirect to 액션으로 변경:
- Protocol:
HTTPS - Host:
www.slowflowsoft.com - Port:
443 - Path:
/#{path}(들어온 path 보존) - Query:
#{query} - Status code: HTTP_301
- Protocol:
- 기본 rule을 Redirect to 액션으로 변경:
- HTTPS:443 listener도 같은 방식으로 변경:
- 같은 redirect 설정. Host만
www.slowflowsoft.com, Status codeHTTP_301.
- 같은 redirect 설정. Host만
- Target Group: 더 이상 사용 안 함. 등록된 EC2 deregister, target group은 한 달 정도 보존 후 삭제.
- EC2 인스턴스: terminate.
Route 53의 apex slowflowsoft.com A 레코드는 이미 ALB Alias로 박혀 있으니 건드리지 않는다. Alias가 가리키는 ALB가 redirect만 해줄 뿐.
검증
# apex로 들어가면 301 → www로 이동
curl -IL https://slowflowsoft.com/blog/
# 1차 응답: HTTP/1.1 301 Moved Permanently
# Location: https://www.slowflowsoft.com/blog/
# 2차 응답: HTTP/2 200 (Front Door)
브라우저로도 https://slowflowsoft.com 들어가면 자동으로 https://www.slowflowsoft.com 으로 튀어가야 한다.
아오.. 좀 많이… 돌아갔다. 아 몰라 일단 나중에 생각하자. (언젠가는 새벽 시간을 틈타 NS 자체를 Azure 생태계 안으로 옮겨야 할 거 같긴하다.)
마치며
이번 셋업의 핵심은 단순히 “AWS에서 Azure로” 가 아니라, 이미 운영 중인 서비스의 의존성 때문에 어디까지가 옮겨도 안전하고 어디부터가 위험한 영역인지를 가르는 작업이었다.
도메인이라는 게 한 번 잘못 건드리면 사용자 입장에서는 “사이트가 안 뜸” 으로 즉시 체감된다. NS 이전이라는 깔끔한 정공법을 알면서도, Zuzoo가 끼어 있다는 이유 하나로 ALB 한 대를 살려두는 선택이 더 합리적이었다.
Azure 셋업 자체는 어렵지 않았다. Container App lift-and-shift는 명령 한 줄, Front Door는 객체 몇 개를 순서대로 만들면 된다. 진짜 어려운 부분은:
- Front Door deployment trigger 함정 — CLI가 객체는 만들지만 글로벌 배포를 trigger 못 함. Portal에서 한 번 더 눌러줘야 하는 패턴.
az afd route update --custom-domains의 ID vs name 동작 차이 — 풀 ID를 주면 silently 실패, name만 줘야 적용.- Route 53 TXT 레코드의 따옴표 — 토큰 그 자체만 박아야 검증 통과.
- nginx.conf의 server_name — 특정 도메인만 매칭하면 Container App FQDN으로 들어온 요청은 default
/etc/nginx/html을 찾다가 404.
이 함정들은 모두 한 번씩 부딪히고 나서야 풀렸고, 셋업 시간의 대부분이 거기에 들어갔다.
마침.