เกี่ยวกับ Codelab นี้
1 บทนำ
ภาพรวม
ในโค้ดแล็บนี้ คุณจะได้เรียนรู้วิธีอัปโหลดและแสดงรูปภาพโดยใช้ Cloud Storage, Firestore และ Cloud Run นอกจากนี้ คุณยังจะได้เรียนรู้วิธีใช้ไลบรารีไคลเอ็นต์ของ Google เพื่อตรวจสอบสิทธิ์เพื่อเรียกใช้ Gemini
สิ่งที่คุณจะได้เรียนรู้
- วิธีทำให้แอป FastAPI ใช้งานได้ใน Cloud Run
- วิธีใช้ไลบรารีของไคลเอ็นต์ Google สำหรับการตรวจสอบสิทธิ์
- วิธีอัปโหลดไฟล์ไปยัง Cloud Storage โดยใช้บริการ Cloud Run
- วิธีอ่านและเขียนข้อมูลไปยัง Firestore
- วิธีเรียกข้อมูลและแสดงรูปภาพจาก Cloud Storage ในบริการ Cloud Run
2 การตั้งค่าและข้อกําหนด
ตั้งค่าตัวแปรสภาพแวดล้อมที่จะใช้ในโค้ดแล็บนี้
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"
และมอบสิทธิ์เข้าถึง Firestore และที่เก็บข้อมูล GCS ให้แก่ SA
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 ระบบจะใส่โค้ดของคุณลงในไฟล์ ZIP และส่งไปยัง Cloud Build ซึ่งจะใช้ Dockerfile เพื่อสร้างอิมเมจ
เนื่องจากเป็นการทำให้ใช้งานได้ใน Cloud Run โดยอิงตามแหล่งที่มา คุณจึงจะเห็นแท็บแหล่งที่มาที่มีโค้ดของคุณใน Cloud Console ของบริการ
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 เปลี่ยนสิทธิ์ในที่เก็บข้อมูลระบบคลาวด์สาธารณะ
ดังที่ได้กล่าวไว้ก่อนหน้านี้ว่าโค้ดแล็บนี้ใช้ที่เก็บข้อมูล GCS สาธารณะ เราขอแนะนำให้คุณลบที่เก็บข้อมูลหรือนำสิทธิ์เข้าถึงที่เก็บข้อมูลของ allUsers ออกโดยเรียกใช้คำสั่งต่อไปนี้
gsutil iam ch -d allUsers:objectViewer gs://$GCS_BUCKET_NAME
คุณสามารถตรวจสอบว่านำสิทธิ์เข้าถึง allUsers ออกแล้วโดยเรียกใช้คำสั่งนี้
gsutil iam get gs://$GCS_BUCKET_NAME
8 ขอแสดงความยินดี
ยินดีด้วยที่ทํา Codelab จนเสร็จสมบูรณ์
สิ่งที่เราได้พูดถึงไปแล้ว
- วิธีทำให้แอป FastAPI ใช้งานได้ใน Cloud Run
- วิธีใช้ไลบรารีของไคลเอ็นต์ Google สำหรับการตรวจสอบสิทธิ์
- วิธีอัปโหลดไฟล์ไปยัง Cloud Storage โดยใช้บริการ Cloud Run
- วิธีอ่านและเขียนข้อมูลไปยัง Firestore
- วิธีเรียกข้อมูลและแสดงรูปภาพจาก Cloud Storage ในบริการ Cloud Run
9 ล้างข้อมูล
หากต้องการลบบริการ Cloud Run ให้ไปที่คอนโซล Cloud ของ Cloud Run ที่ https://console.cloud.google.com/run แล้วลบบริการ
หากต้องการลบที่เก็บข้อมูล 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