1. 소개
개요
이 Codelab에서는 소스 코드 변경사항을 GitHub 저장소에 푸시할 때마다 애플리케이션의 새 버전을 자동으로 빌드하고 배포하도록 Cloud Run을 구성합니다.
이 데모 애플리케이션은 사용자 데이터를 Firestore에 저장하지만 데이터의 일부만 제대로 저장됩니다. GitHub 저장소에 버그 수정을 푸시하면 새 버전에서 수정사항이 자동으로 제공되도록 지속적 배포를 구성합니다.
학습할 내용
- Cloud Shell 편집기로 Express 웹 애플리케이션 작성
- 지속적 배포를 위해 GitHub 계정을 Google Cloud에 연결
- Cloud Run에 애플리케이션 자동 배포
- HTMX 및 TailwindCSS 사용 방법 알아보기
2. 설정 및 요구사항
기본 요건
- GitHub 계정이 있고 저장소에 코드를 만들고 푸시하는 데 익숙합니다.
- Cloud 콘솔에 로그인되어 있습니다.
- 이전에 Cloud Run 서비스를 배포한 적이 있습니다. 예를 들어 소스 코드에서 웹 서비스 배포 빠른 시작을 따라 시작할 수 있습니다.
Cloud Shell 활성화
- Cloud Console에서 Cloud Shell 활성화
를 클릭합니다.

Cloud Shell을 처음 시작하는 경우 설명이 포함된 중간 화면이 제공됩니다. 중간 화면이 표시되면 계속을 클릭합니다.

Cloud Shell을 프로비저닝하고 연결하는 작업은 몇 분이면 끝납니다.

이 가상 머신에는 필요한 모든 개발 도구가 로드되어 있습니다. 영구적인 5GB 홈 디렉터리를 제공하고 Google Cloud에서 실행되므로 네트워크 성능과 인증이 크게 개선됩니다. 이 Codelab에서 대부분의 작업은 브라우저로 수행할 수 있습니다.
Cloud Shell에 연결되면 인증이 완료되었고 프로젝트가 해당 프로젝트 ID로 설정된 것을 확인할 수 있습니다.
- Cloud Shell에서 다음 명령어를 실행하여 인증되었는지 확인합니다.
gcloud auth list
명령어 출력
Credentialed Accounts
ACTIVE ACCOUNT
* <my_account>@<my_domain.com>
To set the active account, run:
$ gcloud config set account `ACCOUNT`
- Cloud Shell에서 다음 명령어를 실행하여 gcloud 명령어가 프로젝트를 알고 있는지 확인합니다.
gcloud config list project
명령어 출력
[core] project = <PROJECT_ID>
또는 다음 명령어로 설정할 수 있습니다.
gcloud config set project <PROJECT_ID>
명령어 출력
Updated property [core/project].
3. API 사용 설정 및 환경 변수 설정
API 사용 설정
이 Codelab에서는 다음 API를 사용해야 합니다. 다음 명령어를 실행하여 이러한 API를 사용 설정할 수 있습니다.
gcloud services enable run.googleapis.com \
cloudbuild.googleapis.com \
firestore.googleapis.com \
iamcredentials.googleapis.com
환경 변수 설정
이 Codelab 전체에서 사용할 환경 변수를 설정할 수 있습니다.
REGION=<YOUR-REGION> PROJECT_ID=<YOUR-PROJECT-ID> PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)') SERVICE_ACCOUNT="firestore-accessor" SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
4. 서비스 계정 만들기
이 서비스 계정은 Cloud Run에서 Vertex AI Gemini API를 호출하는 데 사용됩니다. 이 서비스 계정에는 Firestore에 읽고 쓸 수 있는 권한과 Secret Manager에서 보안 비밀을 읽을 수 있는 권한도 있습니다.
먼저 다음 명령어를 실행하여 서비스 계정을 만듭니다.
gcloud iam service-accounts create $SERVICE_ACCOUNT \ --display-name="Cloud Run access to Firestore"
이제 서비스 계정에 Firestore에 대한 읽기 및 쓰기 액세스 권한을 부여합니다.
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member serviceAccount:$SERVICE_ACCOUNT_ADDRESS \ --role=roles/datastore.user
5. Firebase 프로젝트 만들기 및 구성
- Firebase Console에서 프로젝트 추가를 클릭합니다.
- <YOUR_PROJECT_ID>를 입력하여 기존 Google Cloud 프로젝트 중 하나에 Firebase를 추가합니다.
- 메시지가 표시되면 Firebase 약관을 검토하고 이에 동의합니다.
- 계속을 클릭합니다.
- 요금제 확인을 클릭하여 Firebase 요금제를 확인합니다.
- 이 Codelab에서 Google 애널리틱스를 사용 설정하는 것은 선택사항입니다.
- Firebase 추가를 클릭합니다.
- 프로젝트가 생성되면 계속을 클릭합니다.
- 빌드 메뉴에서 Firestore 데이터베이스를 클릭합니다.
- 데이터베이스 만들기를 클릭합니다.
- 위치 드롭다운에서 지역을 선택한 다음 다음을 클릭합니다.
- 기본값인 프로덕션 모드에서 시작을 사용한 다음 만들기를 클릭합니다.
6. 애플리케이션 작성
먼저 소스 코드 디렉터리를 만들고 해당 디렉터리로 cd합니다.
mkdir cloud-run-github-cd-demo && cd $_
그런 다음 다음 콘텐츠로 package.json 파일을 만듭니다.
{
"name": "cloud-run-github-cd-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node app.js",
"nodemon": "nodemon app.js",
"tailwind-dev": "npx tailwindcss -i ./input.css -o ./public/output.css --watch",
"tailwind": "npx tailwindcss -i ./input.css -o ./public/output.css",
"dev": "npm run tailwind && npm run nodemon"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@google-cloud/firestore": "^7.3.1",
"axios": "^1.6.7",
"express": "^4.18.2",
"htmx.org": "^1.9.10"
},
"devDependencies": {
"nodemon": "^3.1.0",
"tailwindcss": "^3.4.1"
}
}
먼저 아래 콘텐츠로 app.js 소스 파일을 만듭니다. 이 파일에는 서비스의 진입점이 포함되어 있으며 앱의 기본 로직이 포함되어 있습니다.
const express = require("express");
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
const path = require("path");
const { get } = require("axios");
const { Firestore } = require("@google-cloud/firestore");
const firestoreDb = new Firestore();
const fs = require("fs");
const util = require("util");
const { spinnerSvg } = require("./spinnerSvg.js");
const service = process.env.K_SERVICE;
const revision = process.env.K_REVISION;
app.use(express.static("public"));
app.get("/edit", async (req, res) => {
res.send(`<form hx-post="/update" hx-target="this" hx-swap="outerHTML">
<div>
<p>
<label>Name</label>
<input class="border-2" type="text" name="name" value="Cloud">
</p><p>
<label>Town</label>
<input class="border-2" type="text" name="town" value="Nibelheim">
</p>
</div>
<div class="flex items-center mr-[10px] mt-[10px]">
<button class="btn bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]">Submit</button>
<button class="btn bg-gray-200 text-gray-800 px-4 py-2 rounded-lg text-center text-sm font-medium mr-[10px]" hx-get="cancel">Cancel</button>
${spinnerSvg}
</div>
</form>`);
});
app.post("/update", async function (req, res) {
let name = req.body.name;
let town = req.body.town;
const doc = firestoreDb.doc(`demo/${name}`);
//TODO: fix this bug
await doc.set({
name: name
/* town: town */
});
res.send(`<div hx-target="this" hx-swap="outerHTML" hx-indicator="spinner">
<p>
<div><label>Name</label>: ${name}</div>
</p><p>
<div><label>Town</label>: ${town}</div>
</p>
<button
hx-get="/edit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
>
Click to update
</button>
</div>`);
});
app.get("/cancel", (req, res) => {
res.send(`<div hx-target="this" hx-swap="outerHTML">
<p>
<div><label>Name</label>: Cloud</div>
</p><p>
<div><label>Town</label>: Nibelheim</div>
</p>
<div>
<button
hx-get="/edit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
>
Click to update
</button>
</div>
</div>`);
});
const port = parseInt(process.env.PORT) || 8080;
app.listen(port, async () => {
console.log(`booth demo: listening on port ${port}`);
//serviceMetadata = helper();
});
app.get("/helper", async (req, res) => {
let region = "";
let projectId = "";
let div = "";
try {
// Fetch the token to make a GCF to GCF call
const response1 = await get(
"http://metadata.google.internal/computeMetadata/v1/project/project-id",
{
headers: {
"Metadata-Flavor": "Google"
}
}
);
// Fetch the token to make a GCF to GCF call
const response2 = await get(
"http://metadata.google.internal/computeMetadata/v1/instance/region",
{
headers: {
"Metadata-Flavor": "Google"
}
}
);
projectId = response1.data;
let regionFull = response2.data;
const index = regionFull.lastIndexOf("/");
region = regionFull.substring(index + 1);
div = `
<div>
This created the revision <code>${revision}</code> of the
Cloud Run service <code>${service}</code> in <code>${region}</code>
for project <code>${projectId}</code>.
</div>`;
} catch (ex) {
// running locally
div = `<div> This is running locally.</div>`;
}
res.send(div);
});
spinnerSvg.js라는 파일을 만듭니다.
module.exports.spinnerSvg = `<svg id="spinner" alt="Loading..."
class="htmx-indicator animate-spin -ml-1 mr-3 h-5 w-5 text-blue-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>`;
TailwindCSS용 input.css 파일 만들기
@tailwind base; @tailwind components; @tailwind utilities;
그리고 tailwindCSS용 tailwind.config.js 파일을 만듭니다.
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.{html,js}"],
theme: {
extend: {}
},
plugins: []
};
.gitignore 파일을 만듭니다.
node_modules/ npm-debug.log coverage/ package-lock.json .DS_Store
이제 새 public 디렉터리를 만듭니다.
mkdir public cd public
이 공개 디렉터리 내에서 htmx를 사용할 프런트엔드의 index.html 파일을 만듭니다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<script
src="https://unpkg.com/htmx.org@1.9.10"
integrity="sha384-D1Kt99CQMDuVetoL1lrYwg5t+9QdHe7NLX/SoJYkXDFfX37iInKRy5xLSi8nO7UC"
crossorigin="anonymous"
></script>
<link href="./output.css" rel="stylesheet" />
<title>Demo 1</title>
</head>
<body
class="font-sans bg-body-image bg-cover bg-center leading-relaxed"
>
<div class="container max-w-[700px] mt-[50px] ml-auto mr-auto">
<div class="hero flex items-center">
<div class="message text-base text-center mb-[24px]">
<h1 class="text-2xl font-bold mb-[10px]">
It's running!
</h1>
<div class="congrats text-base font-normal">
Congratulations, you successfully deployed your
service to Cloud Run.
</div>
</div>
</div>
<div class="details mb-[20px]">
<p>
<div hx-trigger="load" hx-get="/helper" hx-swap="innerHTML" hx-target="this">Hello</div>
</p>
</div>
<p
class="callout text-sm text-blue-700 font-bold pt-4 pr-6 pb-4 pl-10 leading-tight"
>
You can deploy any container to Cloud Run that listens for
HTTP requests on the port defined by the
<code>PORT</code> environment variable. Cloud Run will
scale automatically based on requests and you never have to
worry about infrastructure.
</p>
<h1 class="text-2xl font-bold mt-[40px] mb-[20px]">
Persistent Storage Example using Firestore
</h1>
<div hx-target="this" hx-swap="outerHTML">
<p>
<div><label>Name</label>: Cloud</div>
</p><p>
<div><label>Town</label>: Nibelheim</div>
</p>
<div>
<button
hx-get="/edit"
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-sm font-medium mt-[10px]"
>
Click to update
</button>
</div>
</div>
<h1 class="text-2xl font-bold mt-[40px] mb-[20px]">
What's next
</h1>
<p class="next text-base mt-4 mb-[20px]">
You can build this demo yourself!
</p>
<p class="cta">
<button
class="bg-blue-500 text-white px-4 py-2 rounded-lg text-center text-sm font-medium"
>
VIEW CODELAB
</button>
</p>
</div>
</body>
</html>
7. 애플리케이션을 로컬로 실행
이 섹션에서는 사용자가 데이터를 저장하려고 할 때 애플리케이션에 버그가 있는지 확인하기 위해 애플리케이션을 로컬로 실행합니다.
먼저 Firestore에 액세스하려면 Datastore 사용자 역할이 있어야 합니다 (인증에 ID를 사용하는 경우, 예를 들어 Cloud Shell에서 실행하는 경우). 또는 이전에 만든 사용자 계정을 가장할 수 있습니다.
로컬에서 실행할 때 ADC 사용
Cloud Shell에서 실행 중인 경우 이미 Google Compute Engine 가상 머신에서 실행 중인 것입니다. gcloud auth list를 실행하여 표시된 이 가상 머신과 연결된 사용자 인증 정보는 애플리케이션 기본 사용자 인증 정보(ADC)에서 자동으로 사용되므로 gcloud auth application-default login 명령어를 사용할 필요가 없습니다. 하지만 ID에는 여전히 데이터 스토어 사용자 역할이 필요합니다. 로컬에서 앱 실행 섹션으로 건너뛰어도 됩니다.
하지만 로컬 터미널 (Cloud Shell 아님)에서 실행하는 경우 애플리케이션 기본 사용자 인증 정보를 사용하여 Google API를 인증해야 합니다. 1) 사용자 인증 정보 (Datastore 사용자 역할이 있는 경우)를 사용하여 로그인하거나 2) 이 Codelab에서 사용된 서비스 계정을 가장하여 로그인할 수 있습니다.
옵션 1) ADC 사용자 인증 정보 사용
사용자 인증 정보를 사용하려면 먼저 gcloud auth list를 실행하여 gcloud에서 인증하는 방법을 확인할 수 있습니다. 다음으로 ID에 Vertex AI 사용자 역할을 부여해야 할 수 있습니다. ID에 소유자 역할이 있는 경우 이미 이 Datastore 사용자 사용자 역할이 있습니다. 그렇지 않은 경우 이 명령어를 실행하여 ID에 Vertex AI 사용자 역할과 Datastore 사용자 역할을 부여할 수 있습니다.
USER=<YOUR_PRINCIPAL_EMAIL> gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/datastore.user
그런 다음 다음 명령어를 실행합니다.
gcloud auth application-default login
옵션 2) ADC용 서비스 계정 가장
이 Codelab에서 만든 서비스 계정을 사용하려면 사용자 계정에 서비스 계정 토큰 생성자 역할이 있어야 합니다. 다음 명령어를 실행하여 이 역할을 획득할 수 있습니다.
gcloud projects add-iam-policy-binding $PROJECT_ID \ --member user:$USER \ --role=roles/iam.serviceAccountTokenCreator
다음으로 다음 명령어를 실행하여 서비스 계정으로 ADC를 사용합니다.
gcloud auth application-default login --impersonate-service-account=$SERVICE_ACCOUNT_ADDRESS
로컬에서 앱 실행
다음으로 Codelab의 루트 디렉터리 cloud-run-github-cd-demo에 있는지 확인합니다.
cd .. && pwd
이제 종속 항목을 설치합니다.
npm install
마지막으로 다음 스크립트를 실행하여 앱을 시작할 수 있습니다. 이 스크립트는 tailwindCSS에서 output.css 파일도 생성합니다.
npm run dev
이제 웹브라우저를 열고 http://localhost:8080으로 이동합니다. Cloud Shell에 있는 경우 웹 미리보기 버튼을 열고 포트 8080 미리보기를 선택하여 웹사이트를 열 수 있습니다.
이름 및 도시 입력 필드에 텍스트를 입력하고 저장을 누릅니다. 그런 다음 페이지를 새로고침합니다. town 필드가 유지되지 않은 것을 확인할 수 있습니다. 다음 섹션에서 이 버그를 수정합니다.
로컬에서 실행 중인 Express 앱을 중지합니다 (예: macOS에서 Ctrl^c).
8. GitHub 저장소 만들기
로컬 디렉터리에서 기본 브랜치 이름이 main인 새 저장소를 만듭니다.
git init git branch -M main
버그가 포함된 현재 코드베이스를 커밋합니다. 지속적 배포가 구성된 후 버그를 수정합니다.
git add . git commit -m "first commit for express application"
GitHub로 이동하여 비공개 또는 공개된 빈 저장소를 만듭니다. 이 Codelab에서는 저장소 이름을 cloud-run-auto-deploy-codelab로 지정할 것을 권장합니다. 빈 저장소를 만들려면 생성 시 기본적으로 콘텐츠가 저장소에 없도록 모든 기본 설정을 선택하지 않거나 '없음'으로 설정합니다. 예를 들면 다음과 같습니다.

이 단계를 올바르게 완료했다면 빈 저장소 페이지에 다음 안내가 표시됩니다.

다음 명령어를 실행하여 명령줄에서 기존 저장소 푸시 안내를 따릅니다.
먼저 다음을 실행하여 원격 저장소를 추가합니다.
git remote add origin <YOUR-REPO-URL-PER-GITHUB-INSTRUCTIONS>
그런 다음 기본 브랜치를 업스트림 저장소로 푸시합니다.
git push -u origin main
9. 지속적 배포 설정
이제 GitHub에 코드가 있으므로 지속적 배포를 설정할 수 있습니다. Cloud Run용 Cloud 콘솔로 이동합니다.
- 서비스 만들기를 클릭합니다.
- 저장소에서 지속적 배포를 클릭합니다.
- Cloud Build 설정을 클릭합니다.
- 소스 저장소에서
- 저장소 제공업체로 GitHub 선택
- 연결된 저장소 관리를 클릭하여 저장소에 대한 Cloud Build 액세스를 구성합니다.
- 저장소를 선택하고 다음을 클릭합니다.
- 빌드 구성에서
- 브랜치를 ^main$으로 둡니다.
- 빌드 유형에서 Google Cloud 빌드팩을 통한 Go, Node.js, Python, Java, .NET Core, Ruby 또는 PHP를 선택합니다.
- 빌드 컨텍스트 디렉터리를
/로 둡니다. - 저장을 클릭합니다.
- 인증
- 인증되지 않은 호출 허용을 클릭합니다.
- 컨테이너, 볼륨, 네트워킹, 보안
- '보안' 탭에서 이전 단계에서 만든 서비스 계정(예:
Cloud Run access to Firestore)을 선택합니다.
- '보안' 탭에서 이전 단계에서 만든 서비스 계정(예:
- 만들기를 클릭합니다.
이렇게 하면 다음 섹션에서 수정할 버그가 포함된 Cloud Run 서비스가 배포됩니다.
10. 버그 수정하기
코드에서 버그 수정
Cloud Shell 편집기에서 app.js 파일을 열고 //TODO: fix this bug이라는 주석으로 이동합니다.
다음 줄을
//TODO: fix this bug
await doc.set({
name: name
});
->
//fixed town bug
await doc.set({
name: name,
town: town
});
다음을 실행하여 수정사항을 확인합니다.
npm run start
웹브라우저를 엽니다. 도시의 데이터를 다시 저장하고 새로고침합니다. 새로 입력한 도시 데이터가 새로고침 시 올바르게 유지됩니다.
이제 수정사항을 확인했으므로 배포할 수 있습니다. 먼저 수정사항을 커밋합니다.
git add . git commit -m "fixed town bug"
그런 다음 GitHub의 업스트림 저장소로 푸시합니다.
git push origin main
Cloud Build가 변경사항을 자동으로 배포합니다. Cloud Run 서비스의 Cloud Console로 이동하여 배포 변경사항을 모니터링할 수 있습니다.
프로덕션에서 수정사항 확인
Cloud Run 서비스의 Cloud Console에 두 번째 버전이 이제 트래픽을 100% 처리하는 것으로 표시되면(예: https://console.cloud.google.com/run/detail/<YOUR_REGION>/<YOUR_SERVICE_NAME>/revisions) 브라우저에서 Cloud Run 서비스 URL을 열고 페이지를 새로고침한 후 새로 입력한 도시 데이터가 유지되는지 확인할 수 있습니다.
11. 수고하셨습니다
축하합니다. Codelab을 완료했습니다.
Cloud Run 및 git에서 지속적 배포 문서를 검토하는 것이 좋습니다.
학습한 내용
- Cloud Shell 편집기로 Express 웹 애플리케이션 작성
- 지속적 배포를 위해 GitHub 계정을 Google Cloud에 연결
- Cloud Run에 애플리케이션 자동 배포
- HTMX 및 TailwindCSS 사용 방법 알아보기
12. 삭제
의도치 않은 요금이 청구되지 않도록 하려면(예: Cloud Run 서비스가 무료 등급의 월별 Cloud Run 호출 할당량보다 더 많이 호출되는 경우) Cloud Run을 삭제하거나 2단계에서 만든 프로젝트를 삭제하면 됩니다.
Cloud Run 서비스를 삭제하려면 https://console.cloud.google.com/run에서 Cloud Run Cloud Console로 이동하여 이 Codelab에서 만든 Cloud Run 서비스를 삭제합니다(예: cloud-run-auto-deploy-codelab 서비스 삭제).
전체 프로젝트를 삭제하려면 https://console.cloud.google.com/cloud-resource-manager으로 이동하여 2단계에서 만든 프로젝트를 선택하고 삭제를 선택합니다. 프로젝트를 삭제하면 Cloud SDK에서 프로젝트를 변경해야 합니다. gcloud projects list를 실행하여 사용 가능한 모든 프로젝트의 목록을 볼 수 있습니다.