GoodBye, AWS

이사가자 이사, Azure 이사기

Jun Noh

지난 글에서 Zuzoo 운영 서버를 AWS에서 Azure로 옮긴 과정을 정리했다.

Zuzoo가 안정적으로 돌기 시작하니, 이번엔 이 블로그도 같이 옮기기로 했다.

이유는 단순하다. AWS 인스턴스 한 대를 굳이 살려둘 이유가 없어졌고, Azure 5,000달러 크레딧 안에서 한 곳에 모아둬야 운영도 비용 추적도 편하다.

근데 막상 시작하니 Zuzoo와는 결정적으로 다른 두 가지 제약이 있었다.

  1. 이 사이트는 100% 정적 사이트 — DB도, 백엔드도, KV도 없다. 그냥 nginx가 정적 파일 3종(React 빌드 + Astro 블로그 + Astro 서브노트)을 path로 분기해서 서빙할 뿐.
  2. 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와 공유)

선택 기준은 세 가지였다.

  1. 정적 사이트라 lift-and-shift — 기존 nginx Dockerfile을 그대로 옮긴다. Storage Static Website나 SWA 같은 옵션도 검토했지만, 셋업 변경이 너무 컸다.
  2. Zuzoo 인프라 재활용 — ACR / Front Door는 공유. 단 Container Apps Environment와 Log Analytics는 분리해서 blast radius 격리.
  3. 도메인은 손대지 않는다 — 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 zuzooprodacrZuzoo와 공유하는 ACR. Basic SKU($5/월) 한 개에 여러 image repository를 둘 수 있다.
--image slowflowsoft-web:v1repository 이름과 태그. 같은 태그를 덮어쓰면 Container App이 새 이미지를 못 당기는 케이스가 있어 항상 새 태그로.
--file Dockerfilebuild 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 3scale-out 한계. 정적 사이트라 트래픽이 폭증해도 nginx 1대가 꽤 버틴다. 3이면 충분.
--cpu 0.25 --memory 0.5Gireplica당 자원. 정적 파일 서빙은 CPU 부하가 거의 없어서 최소 단위로 설정.
--registry-server이미지를 당겨올 레지스트리. ACR FQDN.
--registry-identity systemACR pull 시 password 대신 system managed identity를 쓴다. Container App을 만들 때 자동 발급되는 identity가 ACR에 AcrPull 권한을 받아서 안전하게 image를 당긴다.
--system-assignedContainer App 자체에 system managed identity 부여. 위 registry-identity system이 동작하려면 이게 켜져 있어야 한다.
--revisions-mode single새 revision이 만들어지면 옛 revision을 자동 deactivate. 기본값은 multiple 인데 옛 revision이 누적되어 디버깅이 헷갈린다. 운영은 single 권장.
--scale-rule-name http-concurrencyscale rule 식별자. 임의 이름.
--scale-rule-type httpKEDA scale rule type. HTTP concurrency 기반으로 scale-out.
--scale-rule-http-concurrency 100replica당 동시 처리 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로:

  1. portal.azure.com → 검색 zuzooprodacr → 클릭
  2. 좌측 Access control (IAM)+ Add → Add role assignment
  3. Role: AcrPull → Next
  4. Assign access to: Managed identity+ Select members → Subscription 선택 → Managed identity 종류: Container appsslowflowsoft-web 선택
  5. 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로 떨어졌고, 거기엔 rootlocation / 도 없어서 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 Httpsbackend가 HTTPS로 응답하니 HTTPS로 probe.
--probe-request-type GETHEAD가 아니라 GET. 일부 origin이 HEAD 응답을 다르게 처리해서 안전하게 GET.
--probe-interval-in-seconds 3030초마다 probe.
--sample-size 4최근 4번의 probe 결과를 sliding window로 본다.
--successful-samples-required 34번 중 3번이 성공이면 healthy. 일시적인 1번 실패는 무시.
--additional-latency-in-milliseconds 50latency 비교 시 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_FQDNFront Door가 connect할 backend 호스트네임.
--origin-host-header $APP_FQDNbackend로 보낼 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 Httpsedge에서 HTTP/HTTPS 둘 다 받는다. HTTP로 들어오면 아래 --https-redirect 로 자동 redirect.
--patterns-to-match "/*"모든 path를 이 origin group으로. blog/subnotes 분기는 nginx가 처리하니 Front Door는 통과만.
--forwarding-protocol HttpsOnlyedge → backend는 항상 HTTPS. Container Apps의 ingress가 자동 HTTPS라 OK.
--link-to-default-domain Enabled*.azurefd.net default domain으로도 트래픽을 받는다. custom domain 셋업 전 검증 단계에 필수.
--https-redirect EnabledHTTP 요청을 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 에서 한 번 누르는 것뿐이었다.

  1. portal.azure.com → 검색창 zuzoo-prod-fd → 클릭
  2. 좌측 Front Door manager
  3. 트리에서 endpoint(slowflowsoft-web) 클릭
  4. 우측 패널의 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는 별도 처리가 필요하다.

선택지를 비교했다.

옵션방법비용난이도평가
ARoute 53 ALIAS → Front Door$00❌ Route 53 ALIAS는 AWS 내부 리소스에만 쓸 수 있다. *.azurefd.net 같은 외부 호스트네임은 안 됨.
BA record로 Front Door anycast IP 직접 박기$0낮음❌ 공식 비추. IP가 변경되면 사이트 다운.
CS3 (redirect-only) + CloudFront + ACM cert + Route 53 ALIAS~$1/월중간OK. 비용 거의 0이지만 객체 4개를 새로 셋업해야 함.
DALB만 살리고 EC2/Target Group 정리, listener rule을 apex → www 301 redirect로 변경~$22/월낮음✅ 셋업 3분, AWS 의존 최소, 5,000달러 크레딧 안에서 흡수.
Eapex 포기, www만 운영$00❌ 사용자 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-wwwAzure 리소스 식별자(점 안 됨, 하이픈만).
--host-name www.slowflowsoft.com실제 도메인.
--certificate-type ManagedCertificateAzure가 cert를 자동 발급/갱신. ACM 같은 외부 cert 불필요.
--minimum-tls-version TLS12TLS 1.0/1.1 차단.

도메인 소유권 검증 — TXT

az afd custom-domain show ... --query validationProperties

validationToken 값을 받아서 Route 53에 TXT 레코드 추가:

필드
Record name_dnsauth.www (Route 53는 호스트만 입력)
TypeTXT
Value<validationToken> (따옴표 없이)
TTL60

한 번 따옴표로 감싸서 등록했다가 검증이 영영 통과 안 되어 한참 헤맸다. 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 namewww
TypeCNAME
Value<endpoint hostname> (예: slowflowsoft-web-fvf0hfg6b7cvakb4.z01.azurefd.net)
TTL60

TTL 60초로 둔 이유는 컷오버 직후 문제 발견 시 빠른 롤백을 위해서다. 안정화 후엔 3600으로 올려도 된다.


8. apex → www 301 redirect (ALB listener rule)

이제 apex(slowflowsoft.com) 차례. EC2와 Target Group은 정리하고, ALB는 listener rule만 두 개 redirect로 바꾼다.

AWS Console 작업

  1. EC2 → Load Balancers → 그 ALB 선택 → Listeners 탭
  2. 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
  3. HTTPS:443 listener도 같은 방식으로 변경:
    • 같은 redirect 설정. Host만 www.slowflowsoft.com, Status code HTTP_301.
  4. Target Group: 더 이상 사용 안 함. 등록된 EC2 deregister, target group은 한 달 정도 보존 후 삭제.
  5. 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.

이 함정들은 모두 한 번씩 부딪히고 나서야 풀렸고, 셋업 시간의 대부분이 거기에 들어갔다.

마침.

다른 글 보기