- 서론
-
- 사전 준비
-
- GCP 프로젝트 및 결제
-
- Next.js를 Cloud Run에 배포
-
- 자체 도메인 연결하기 (가장 막히는 부분)
-
- 비용 안전 장치
- 막혔던 포인트 정리
- DNS에서 문제가 생겼을 때의 분리 작업
- 끝맺음
お名前.com에서 구매한 자체 도메인을 사용하여 GCP의 Cloud Run에서 자신의 웹페이지(Next.js로 만든 자기소개 사이트)를 공개하는 과정까지의 절차를 정리합니다.
실제로 진행해 보니, 네임서버 설정, 소유권 확인, DNS 반영 대기 단계에서 상당히 막혔습니다. 절차뿐만 아니라 막혔던 지점과 원인도 남깁니다.
※AI로 작성된 글이므로 부정확한 정보가 포함되어 있을 수 있습니다.
본 기사의 도메인, 프로젝트 ID, 계정 ID 등은 모두 가상의 값(example.com, YOUR_PROJECT_ID 등)으로 대체했습니다.
브라우저 → example.com (お名前.com에서 구매 및 DNS도 같은 회사)
→ Google의 프런트엔드 (HTTPS 인증서는 자동 발급)
→ Cloud Run (도쿄) → Next.js 컨테이너
- 로컬 환경과
gcloud의 준비 - GCP 프로젝트/결제/예산 알림 - Next.js를 Cloud Run에 배포
자체 도메인을 연결(네임서버 → 소유권 확인 → 도메인 매핑 → A/AAAA 레코드 → 인증서 대기) - 비용 안전 장치
정적인 페이지만이라면 Firebase Hosting이 더 저렴하고 쉽습니다. 이번에는 GCP를 학습하는 목적이라 Cloud Run을 선택했습니다.
Next.js 16은 Node 20.9 이상이 필요합니다. 오래된 경우 nvm으로 업데이트합니다.
nvm install node
nvm alias default node
node -v
libatomic.so.1: cannot open shared object file (WSL / Ubuntu)
최신 Node는 libatomic1에 의존합니다. 이것이 없으면 node 자체가 실행되지 않습니다. sudo의 비밀번호 입력이 필요하므로, 일반 터미널에서 실행합니다.
sudo apt-get update && sudo apt-get install -y libatomic1
홈 디렉토리에 전개하는 방법입니다.
curl -fsSL -o /tmp/gcloud.tar.gz \
tar -xzf /tmp/gcloud.tar.gz -C ~
...
gcloud beta ...는 컴포넌트가 미설치된 경우 대화 확인으로 멈춥니다. --quiet을 붙여도 통하지 않으므로, 먼저 위의 명령어로 설치해 둡니다.
gcloud billing accounts list # OPEN이 True인지
gcloud billing projects describe YOUR_PROJECT_ID # billingEnabled: true인지
저의 경우, 청구 계정이 OPEN: False (무료 체험 종료)여서 Cloud Run을 사용할 수 없었습니다. 콘솔에서 유료 계정으로 재등록했습니다. Cloud Run / Cloud Build / Artifact Registry는 결제가 활성화되어 있지 않으면 사용할 수 없습니다.
- 표시명은 나중에 변경할 수 있지만, 프로젝트 ID는 생성 후에 변경할 수 없습니다. - 생성 직후에는
gcloud projects list에 나타나지 않을 때가 있습니다. ID를 알고 있다면 gcloud projects describe YOUR_PROJECT_ID로 직접 확인할 수 있습니다.
gcloud config set project YOUR_PROJECT_ID
gcloud config set run/region asia-northeast1
gcloud services enable \
...```
예산 알림은 '알림'만 할 뿐, 초과해도 서비스는 중단되지 않습니다. 비용을 절감하고 싶다면, 뒤에서 설명할 Cloud Run의 스케일 상한도 병행해야 합니다.
알림 메일 수신자는 콘솔의 '결제 → 예산 및 알림 → 편집 → 조치'에서 확인할 수 있습니다.
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone", // 운영에 필요한 최소한의 파일만 출력합니다
...
Cloud Run은 포트 `8080`에서 대기합니다.
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
...
`COPY --from=builder /app/public ./public`이 있으므로, ** public/ 디렉토리가 없으면 빌드가 실패합니다**. 내용물을 전부 지우면 빈 디렉터리는 Git에 포함되지 않고 사라지기 때문에,
`public/.gitkeep`
을 넣어 남겨둡니다.
gcloud run deploy my-homepage
--source .
--region asia-northeast1
...
-
`--source .`로, Cloud Build가 Dockerfile로부터 빌드해 줍니다. -
`--allow-unauthenticated`로, 누구나 접근할 수 있는 공개 사이트가 됩니다. -
`--max-instances 2`는 비용 안전장치입니다(뒤에서 설명).
완료되면, `https://my-homepage-PROJECT_NUMBER.asia-northeast1.run.app`와 같은 URL이 표시됩니다. 먼저 여기서 표시를 확인합니다.
ERROR: ... [email protected] does not have
storage.objects.get access to the Google Cloud Storage object ...
**조직 하위 프로젝트**에서는 Cloud Build가 사용하는 기본 서비스 계정에 빌드 권한이 없을 수 있습니다. 이 프로젝트에 한해서, 빌드용 역할을 부여합니다.
gcloud projects add-iam-policy-binding YOUR_PROJECT_ID
--member="serviceAccount:[email protected]"
--role="roles/cloudbuild.builds.builder"
...
콘솔의 IAM에서 'Cloud Build 서비스 계정' 역할을 추가해도 같습니다. 부여한 후, 다시 배포합니다.
흐름은 다음과 같습니다.
(1) 네임 서버를 DNS 레코드 설정용으로 변경 … お名前.com
(2) TXT 레코드로 소유권을 증명 … お名前.com + Search Console
(3) 도메인 매핑을 생성 … gcloud
...
お名前.com의 '**DNS 레코드 설정**'은, 도메인의 네임 서버가 다음 4개일 때만 유효합니다.
01.dnsv.jp
02.dnsv.jp
03.dnsv.jp
...
'네임 서버 설정' → '다른 네임 서버 이용'에서 설정하고 저장합니다.
**여기서 처음 막혔습니다.** DNS 레코드 설정 화면에 TXT를 등록하여 '유효'로 표시되어 있음에도 불구하고, 인터넷상의 DNS에는 나타나지 않았습니다.
원인은, 네임 서버가 초기값(`dns1.onamae.com` / `dns2.onamae.com`) 그대로였기 때문입니다.
DNS 레코드 설정 확인 화면에 나오는 '네임 서버 변경: **안 함**'은 '도메인의 네임 서버를 바꾸지 않겠다'는 의미입니다. **네임 서버의 변경은 다른 화면에서 진행해야 합니다.**
현재 네임 서버는 Google의 DNS over HTTPS로 확인할 수 있습니다(`dig`가 없는 환경에서도 사용 가능합니다).
curl -s "https://dns.google/resolve?name=example.com&type=NS" | tr ',' '\n' | grep ""data""
`01.dnsv.jp.`
~ `04.dnsv.jp.`
이 나오면 전환이 완료된 것입니다.
**확인 화면의 요금에 주의하세요.** 제 경우, 설정 확인 화면에 '도메인 프로텍션(1,078엔) + 서비스 유지 조정비(275엔)'가 **자동으로 추가**되어 있었습니다. DNS 설정에는 불필요한 옵션입니다. 확인 화면에서 금액란이 0엔인지 확인하고, 체크를 해제한 후 '설정하기'를 누르세요.
도메인 매핑을 만들기 전에, **Google에게 도메인의 소유자임을 증명**해야 합니다. 이것을 확인하지 않으면 다음 오류가 발생합니다.
The provided domain does not appear to be verified for the current account.
gcloud domains verify example.com # Search Console 화면이 열림
- Search Console에서 '도메인' 속성에 `example.com`을 입력합니다. - 표시된 TXT 레코드 값(`google-site-verification=xxxxxxxx...`)을 복사합니다. - お名前.com의 DNS 레코드 설정에 다음 TXT 레코드를 추가합니다.
| 호스트명 | TYPE | TTL | VALUE |
|---|---|---|---|
| (공란) | TXT | 3600 | `google-site-verification=xxxxxxxx...` |
- **DNS에 표시된 것을 확인한 후에** Search Console에서 '확인'을 누릅니다.
curl -s "https://dns.google/resolve?name=example.com&type=TXT" | tr ',' '\n' | grep ""data""
- 완료되면 다음 명령어로 도메인이 표시됩니다.
gcloud domains list-user-verified
- **확인 버튼을 연타하지 마세요.** DNS에 나타나기 전에 눌러도 실패할 뿐이며, 화면이 사라져서 알아보기 어려워졌습니다. 사라지면 `gcloud domains verify`를 다시 실행하거나, Search Console에서 같은 도메인을 다시 입력하면 TXT 값이 재표시됩니다(값은 동일합니다). - 인증은, 에서 진행합니다. 다른 계정의 경우 `gcloud`에 로그인한 것과 같은 Google 계정인 `gcloud`에서는 보이지 않습니다. - TXT 레코드는 **지우지 않고 남겨둡니다**.
gcloud beta run domain-mappings create
--service my-homepage
--domain example.com
...
성공하면 등록해야 할 **A 레코드 4개, AAAA 레코드 4개**가 표시됩니다.
NAME RECORD TYPE CONTENTS
my-homepage A 216.239.32.21
my-homepage A 216.239.34.21
...
(값은 환경에 따라 달라질 수 있으므로, 반드시 자신의 출력을 사용하세요.)
お名前.com의 DNS 레코드 설정에서, **호스트명은 공란**으로, TTL은 3600 그대로, 표시된 8개를 등록합니다.
- **TXT**는 '이 도메인의 소유자는 나'라는 소유권 증명입니다. - **A / AAAA**는 '이 도메인에 대한 접근을 이 IP 주소로 보낸다'라는 연결지 지정입니다. A가 IPv4, AAAA가 IPv6입니다. - HTTPS 인증서는 도메인이 Cloud Run을 가리키고 있음을 Google이 확인하여 발급합니다. A / AAAA가 없으면 발급되지 않습니다.
gcloud beta run domain-mappings describe
--domain example.com --region asia-northeast1
--format='yaml(status.conditions)'
| 상태 | 의미 |
|---|---|
`CertificatePending` | A / AAAA가 전파되는 것을 기다리는 중 |
`Ready: True`와 `CertificateProvisioned: True` | 인증서 발급 완료 |
마지막으로, 실제로 접속하여 확인합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
curl -sI https://example.com
**기다릴 수밖에 없는 상황이 2가지 있습니다.**
-
**A / AAAA를 등록했는데 연결되지 않는 경우**: 이전에 문의했을 때의 '레코드 없음' 답변이 DNS에 **부정 캐시(negative cache)**되어 있기 때문입니다. 응답에 포함된 SOA 레코드의 마지막 숫자(예: `300`)
가 유지 시간(TTL)입니다. 몇 분~1시간 내로 해소됩니다. -
:`CertificateProvisioned: True`
인데 HTTPS 연결이 안 되는 경우, `SSL_ERROR_SYSCALL`로 TLS 중간에 끊어질 수 있습니다. IP 주소를 직접 지정해도 동일하다면, 사용자의 DNS가 아닌 Google 측의 반영을 기다려야 합니다. 설정을 수정할 필요는 없으며, 몇 분~1시간 정도 지나면 개통되었습니다.
예산 알림은 통지 기능일 뿐이므로, 다음 대책도 마련했습니다.
앞서 언급한 `--max-instances 2`입니다. 트래픽 급증이나 공격이 발생해도 인스턴스가 계속 늘어나 청구 금액이 커지는 것을 방지할 수 있습니다. `min-instances 0`으로 설정했기 때문에, 접속이 없을 때는 비용이 발생하지 않습니다.
`gcloud run deploy --source`는 배포할 때마다 이미지를 Artifact Registry에 쌓습니다. 클린업 정책(cleanup policy)을 통해 자동으로 삭제됩니다.
[
{
"name": "delete-old-images",
...
gcloud artifacts repositories set-cleanup-policies cloud-run-source-deploy
--project YOUR_PROJECT_ID --location asia-northeast1
--policy=cleanup-policy.json --no-dry-run
14일이 지난 이미지를 삭제하고, 최신 3개와 `latest` 태그는 남깁니다. 다만, 삭제된 이미지의 리비전으로는 롤백할 수 없다는 점에 주의가 필요합니다.
⚠️ `[IMG:N]` 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
| # | 증상 | 원인 | 대처 |
|---|---|---|---|
| 1 | `node`가 실행되지 않음 | `libatomic1`이 없음 (WSL) | `sudo apt-get install -y libatomic1` |
| 2 | Cloud Run을 사용할 수 없음 | 결제가 비활성화됨 | 청구 계정을 활성화하고 프로젝트에 연결|
| 3 | 배포 시 403 (`storage.objects.get`) | 조직 하위에 기본 서비스 계정에 권한이 없음 | `roles/cloudbuild.builds.builder`를 부여|
| 4 | `public/`을 삭제하니 빌드 실패 | Dockerfile이 `public/`을 복사함. 빈 디렉터리는 Git에 들어가지 않음 | `public/.gitkeep`을 배치|
| 5 | `domain does not appear to be verified` | 소유권 확인이 완료되지 않음 | TXT 레코드로 확인하기|
| 6 | TXT가 DNS에 표시되지 않음 | 네임서버가 초기값인 상태로 남아 있음 | `01〜04.dnsv.jp`으로 변경하기|
| 7 | 확인 화면에서 요금이 발생함 | 도메인 보호 기능이 자동으로 선택되었음 | 체크를 해제하고 0엔인지 확인한 후 설정하기|
| 8 | Search Console 확인 실패/화면 사라짐 | DNS에 표시되기 전에 연타했음 | TXT가 전파되는 것을 확인하고, 확인 버튼은 한 번만 누르기|
| 9 | A / AAAA 레코드가 표시되지 않음 | 부정 캐시 (Negative cache) | 몇 분~1시간 기다리기 |
| ... | `dig` / `nslookup`이 없음 | WSL에 미설치됨 | `curl "https://dns.google/resolve?name=…&type=…"`로 대체하기
먼저 **'어떤 네임서버가 응답하고 있는지'**를 확인합니다. お名前.com 화면에 레코드가 표시되어 있어도, 네임서버가 다르면 무효입니다.
| 확인할 항목 | `type=` | 판단 |
|---|---|---|
| 네임서버 | `NS` | `01〜04.dnsv.jp`인지 |
| 소유권의 TXT | `TXT` | `google-site-verification=…`이 표시되는지 |
| 연결 대상 | `A` / `AAAA` | 8건이 표시되는지 (SOA만이라면 부정 캐시) |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
curl -s "https://dns.google/resolve?name=example.com&type=NS" | tr ',' '\n' | grep ""data""
작업의 대부분은 '도메인 설정(네임서버・소유권・DNS)'에 할애되며, 앱 배포 자체는 `gcloud run deploy` 1개 명령어로 끝났습니다. 막히는 지점은,
- 네임서버를 변경해야 DNS 레코드 설정이 적용됨
- DNS나 HTTPS의 반영은, 설정이 정확하다면 **기다릴** 수밖에 없는 상황이 있음 - 등록기관 확인 화면에서 유료 옵션이 자동으로 선택되는 경우가 있음
세 가지로 요약된다고 생각합니다. 같은 구성을 시도하는 분들의 참고가 되기를 바랍니다.