이 Codelab 정보
1. 소개
개요
이 Codelab에서는 Cloud Storage, Firestore, Cloud Run을 사용하여 이미지를 업로드하고 제공하는 방법을 알아봅니다. 인증을 위해 Google의 클라이언트 라이브러리를 사용하여 Gemini를 호출하는 방법도 알아봅니다.
학습할 내용
- Cloud Run에 FastAPI 앱을 배포하는 방법
- 인증에 Google 클라이언트 라이브러리를 사용하는 방법
- Cloud Run 서비스를 사용하여 Cloud Storage에 파일을 업로드하는 방법
- Firestore에 데이터를 읽고 쓰는 방법
- Cloud Run 서비스에서 Cloud Storage의 이미지를 가져와 표시하는 방법
2. 설정 및 요구사항
이 Codelab 전체에서 사용할 환경 변수를 설정합니다.
PROJECT_ID=dogfood-gcf-saraford
REGION=us-central1
GCS_BUCKET_NAME=dogfood-gcf-saraford-codelab-wietse-2
SERVICE_NAME=fastapi-storage-firestore
SERVICE_ACCOUNT=fastapi-storage-firestore-sa
SERVICE_ACCOUNT_ADDRESS=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com
API 사용 설정
gcloud services enable run.googleapis.com \
storage.googleapis.com \
firestore.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
이미지를 저장할 Cloud Storage 버킷 만들기
gsutil mb -p dogfood-gcf-saraford -l us-central1 gs://$GCS_BUCKET_NAME
웹사이트에 이미지를 업로드하고 표시할 수 있는 버킷에 대한 공개 액세스를 허용합니다.
gsutil iam ch allUsers:objectViewer gs://$GCS_BUCKET_NAME
다음 명령어를 실행하여 서비스 계정을 만듭니다.
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name="SA for CR $SERVICE_ACCOUNT"
그리고 SA에 Firestore 및 GCS 버킷에 대한 액세스 권한을 부여합니다.
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:$SERVICE_ACCOUNT_ADDRESS" \
--role="roles/datastore.user"
gsutil iam ch serviceAccount:$SERVICE_ACCOUNT_ADDRESS:roles/storage.objectAdmin gs://$GCS_BUCKET_NAME
3. Firestore 데이터베이스 만들기
다음 명령어를 실행하여 Firestore 데이터베이스를 만듭니다.
gcloud firestore databases create --location=nam5
4. 앱 만들기
코드의 디렉터리를 만듭니다.
mkdir codelab-cr-fastapi-firestore-gcs
cd codelab-cr-fastapi-firestore-gcs
먼저 템플릿 디렉터리를 만들어 html 템플릿을 만듭니다.
mkdir templates
cd templates
다음 콘텐츠로 index.html
라는 새 파일을 만듭니다.
<!DOCTYPE html>
<html>
<head>
<title>Cloud Run Image Upload Demo</title>
<style>
body { font-family: sans-serif; padding: 20px; }
.upload-form { margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; }
.image-list { margin-top: 30px; }
.image-item { border-bottom: 1px solid #eee; padding: 10px 0; }
.image-item img { max-width: 100px; max-height: 100px; vertical-align: middle; margin-right: 10px;}
.error { color: red; font-weight: bold; margin-top: 10px;}
</style>
</head>
<body>
<h1>Upload an Image</h1>
<p>Files will be uploaded to GCS bucket: <strong>{{ bucket_name }}</strong> and metadata stored in Firestore.</p>
<div class="upload-form">
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept="image/*" required>
<button type="submit">Upload Image</button>
</form>
{% if error_message %}
<p class="error">{{ error_message }}</p>
{% endif %}
</div>
<div class="image-list">
<h2>Recently Uploaded Images:</h2>
{% if images %}
{% for image in images %}
<div class="image-item">
<a href="{{ image.gcs_url }}" target="_blank">
<img src="{{ image.gcs_url }}" alt="{{ image.filename }}" title="Click to view full size">
</a>
<span>{{ image.filename }}</span>
<small>(Uploaded: {{ image.uploaded_at.strftime('%Y-%m-%d %H:%M:%S') if image.uploaded_at else 'N/A' }})</small><br/>
<small><a href="{{ image.gcs_url }}" target="_blank">{{ image.gcs_url }}</a></small>
</div>
{% endfor %}
{% else %}
<p>No images uploaded yet or unable to retrieve list.</p>
{% endif %}
</div>
</body>
</html>
이제 루트 디렉터리에 Python 코드와 기타 파일을 만듭니다.
cd ..
다음 콘텐츠로 .gcloudignore
파일을 만듭니다.
__pycache__
다음 콘텐츠로 main.py
이라는 파일을 만듭니다.
import os
import datetime
from fastapi import FastAPI, File, UploadFile, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from google.cloud import storage, firestore
# --- Configuration ---
# Get bucket name and firestore collection from Cloud Run env vars
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", "YOUR_BUCKET_NAME_DEFAULT")
FIRESTORE_COLLECTION = os.environ.get("FIRESTORE_COLLECTION", "YOUR_FIRESTORE_DEFAULT")
# --- Initialize Google Client Libraries ---
# These client libraries will use the Application Default Credentials
# for your service account within the Cloud Run environment
storage_client = storage.Client()
firestore_client = firestore.Client()
# --- FastAPI App ---
app = FastAPI()
templates = Jinja2Templates(directory="templates")
# --- Routes ---
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
"""Serves the main upload form."""
# Query Firestore for existing images to display
images = []
try:
docs = firestore_client.collection(FIRESTORE_COLLECTION).order_by(
"uploaded_at", direction=firestore.Query.DESCENDING
).limit(10).stream() # Get latest 10 images
for doc in docs:
images.append(doc.to_dict())
except Exception as e:
print(f"Warning: Could not fetch images from Firestore: {e}")
# Continue without displaying images if Firestore query fails
return templates.TemplateResponse("index.html", {
"request": request,
"bucket_name": GCS_BUCKET_NAME,
"images": images # Pass images to the template
})
@app.post("/upload")
async def handle_upload(request: Request, file: UploadFile = File(...)):
"""Handles file upload, saves to GCS, and records in Firestore."""
if not file:
return {"message": "No upload file sent"}
elif not GCS_BUCKET_NAME or GCS_BUCKET_NAME == "YOUR_BUCKET_NAME_DEFAULT":
return {"message": "GCS Bucket Name not configured."}, 500 # Internal Server Error
try:
# 1. Upload to GCS
# note: to keep the demo code short, there are no file verifications
# for an actual real-world production app, you will want to add checks
gcs_url = upload_to_gcs(file, GCS_BUCKET_NAME)
# 2. Save metadata to Firestore
save_metadata_to_firestore(file.filename, gcs_url, FIRESTORE_COLLECTION)
# Redirect back to the main page after successful upload
return RedirectResponse(url="/", status_code=303) # Redirect using See Other
except Exception as e:
print(f"Upload failed: {e}")
return templates.TemplateResponse("index.html", {
"request": request,
"bucket_name": GCS_BUCKET_NAME,
"error_message": f"Upload failed: {e}",
"images": [] # Pass empty list on error or re-query
}, status_code=500)
# --- Helper Functions ---
def upload_to_gcs(uploadedFile: UploadFile, bucket_name: str) -> str:
"""Uploads a file to Google Cloud Storage and returns the public URL."""
try:
bucket = storage_client.bucket(bucket_name)
# Create a unique blob name (e.g., timestamp + original filename)
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d%H%M%S")
blob_name = f"{timestamp}_{uploadedFile.filename}"
blob = bucket.blob(blob_name)
# Upload the file
# Reset file pointer just in case
uploadedFile.file.seek(0)
blob.upload_from_file(uploadedFile.file, content_type=uploadedFile.content_type)
print(f"File {uploadedFile.filename} uploaded to gs://{bucket_name}/{blob_name}")
return blob.public_url # Return the public URL
except Exception as e:
print(f"Error uploading to GCS: {e}")
raise # Re-raise the exception for FastAPI to handle
def save_metadata_to_firestore(filename: str, gcs_url: str, collection_name: str):
"""Saves image metadata to Firestore."""
try:
doc_ref = firestore_client.collection(collection_name).document()
doc_ref.set({
'filename': filename,
'gcs_url': gcs_url,
'uploaded_at': firestore.SERVER_TIMESTAMP # Use server timestamp
})
print(f"Metadata saved to Firestore collection {collection_name}")
except Exception as e:
print(f"Error saving metadata to Firestore: {e}")
# Consider raising the exception or handling it appropriately
raise # Re-raise the exception
다음 콘텐츠로 Dockerfile
를 만듭니다.
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /app
# Install poetry
RUN pip install poetry
RUN poetry self add poetry-plugin-export
# Copy poetry files
COPY pyproject.toml poetry.lock* ./
# Copy application code
COPY . .
# Export dependencies to requirements.txt
RUN poetry export -f requirements.txt --output requirements.txt
# Final stage
FROM python:3.12-slim
WORKDIR /app
# Copy files from builder
COPY --from=builder /app/ .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Compile bytecode to improve startup latency
# -q: Quiet mode
# -b: Write legacy bytecode files (.pyc) alongside source
# -f: Force rebuild even if timestamps are up-to-date
RUN python -m compileall -q -b -f .
# Expose port
EXPOSE 8080
# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]
다음과 같이 pyproject.toml
를 만들었습니다.
[tool.poetry]
name = "cloud-run-fastapi-demo"
version = "0.1.0"
description = "Demo FastAPI app for Cloud Run showing GCS upload and Firestore integration."
authors = ["Your Name <you@example.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.110.0"
uvicorn = {extras = ["standard"], version = "^0.29.0"} # Includes python-multipart
google-cloud-storage = "^2.16.0"
google-cloud-firestore = "^2.16.0"
jinja2 = "^3.1.3"
python-multipart = "^0.0.20"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
5. Cloud Run에 배포
다음은 Cloud Run에 배포하는 명령어입니다. 코드는 압축되어 Cloud Build로 전송되며 Cloud Build는 Dockerfile을 사용하여 이미지를 만듭니다.
Cloud Run에 소스 기반으로 배포하므로 서비스의 Cloud 콘솔에 코드가 포함된 소스 탭이 표시됩니다.
gcloud run deploy $SERVICE_NAME \
--source . \
--allow-unauthenticated \
--service-account=$SERVICE_ACCOUNT_ADDRESS \
--set-env-vars=GCS_BUCKET_NAME=$GCS_BUCKET_NAME \
--set-env-vars=FIRESTORE_COLLECTION=$FIRESTORE_COLLECTION
6. 서비스 테스트
웹브라우저에서 서비스 URL을 열고 이미지를 업로드합니다. 목록에 표시됩니다.
7. 공개 Cloud Storage 버킷의 권한 변경
앞서 언급했듯이 이 Codelab에서는 공개 GCS 버킷을 사용합니다. 다음 명령어를 실행하여 버킷을 삭제하거나 버킷에 대한 allUsers 액세스 권한을 삭제하는 것이 좋습니다.
gsutil iam ch -d allUsers:objectViewer gs://$GCS_BUCKET_NAME
다음 명령어를 실행하여 allUsers 액세스가 삭제되었는지 확인할 수 있습니다.
gsutil iam get gs://$GCS_BUCKET_NAME
8. 축하합니다
Codelab을 완료했습니다. 축하합니다.
학습한 내용
- Cloud Run에 FastAPI 앱을 배포하는 방법
- 인증에 Google 클라이언트 라이브러리를 사용하는 방법
- Cloud Run 서비스를 사용하여 Cloud Storage에 파일을 업로드하는 방법
- Firestore에 데이터를 읽고 쓰는 방법
- Cloud Run 서비스에서 Cloud Storage의 이미지를 가져와 표시하는 방법
9. 삭제
Cloud Run 서비스를 삭제하려면 https://console.cloud.google.com/run의 Cloud Run Cloud 콘솔로 이동하여 서비스를 삭제합니다.
Cloud Storage 버킷을 삭제하려면 다음 명령어를 실행합니다.
echo "Deleting objects in gs://$GCS_BUCKET_NAME..."
gsutil rm -r gs://$GCS_BUCKET_NAME/*
echo "Deleting bucket gs://$GCS_BUCKET_NAME..."
gsutil rb gs://$GCS_BUCKET_NAME
전체 프로젝트를 삭제하려면 https://console.cloud.google.com/cloud-resource-manager로 이동하여 2단계에서 만든 프로젝트를 선택하고 삭제를 선택합니다. 프로젝트를 삭제하면 Cloud SDK에서 프로젝트를 변경해야 합니다. gcloud projects list
를 실행하여 사용 가능한 모든 프로젝트 목록을 볼 수 있습니다.