Sobre este codelab
1. Introdução
Visão geral
Neste codelab, você vai aprender a fazer upload e exibir imagens usando o Cloud Storage, o Firestore e o Cloud Run. Você também vai aprender a usar as bibliotecas de cliente do Google para autenticação e fazer chamadas para o Gemini.
O que você vai aprender
- Como implantar o app FastAPI no Cloud Run
- Como usar as bibliotecas de cliente do Google para autenticação
- Como fazer upload de um arquivo para o Cloud Storage usando um serviço do Cloud Run
- Como ler e gravar dados no Firestore
- Como recuperar e exibir imagens do Cloud Storage em um serviço do Cloud Run
2. Configuração e requisitos
Defina as variáveis de ambiente que serão usadas neste 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
Ativar APIs
gcloud services enable run.googleapis.com \
storage.googleapis.com \
firestore.googleapis.com \
cloudbuild.googleapis.com \
artifactregistry.googleapis.com
Crie um bucket do Cloud Storage para armazenar as imagens
gsutil mb -p dogfood-gcf-saraford -l us-central1 gs://$GCS_BUCKET_NAME
Permita o acesso público ao bucket em que você pode fazer upload e exibir imagens no site:
gsutil iam ch allUsers:objectViewer gs://$GCS_BUCKET_NAME
Execute este comando para criar uma conta de serviço:
gcloud iam service-accounts create $SERVICE_ACCOUNT \
--display-name="SA for CR $SERVICE_ACCOUNT"
E conceda à SA acesso ao Firestore e ao bucket do 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. Criar o banco de dados do Firestore
Execute o comando a seguir para criar um banco de dados do Firestore
gcloud firestore databases create --location=nam5
4. Criar o app
Crie um diretório para o código.
mkdir codelab-cr-fastapi-firestore-gcs
cd codelab-cr-fastapi-firestore-gcs
Primeiro, você vai criar os modelos HTML criando um diretório de modelos.
mkdir templates
cd templates
Crie um novo arquivo chamado index.html
com o seguinte conteúdo:
<!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>
Agora crie seu código Python e outros arquivos no diretório raiz
cd ..
Crie um arquivo .gcloudignore
com o seguinte conteúdo:
__pycache__
Crie um arquivo chamado main.py
com o conteúdo a seguir:
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
Crie um Dockerfile
com o seguinte conteúdo:
# 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"]
e criou um pyproject.toml
com o seguinte:
[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. Implantar no Cloud Run
Confira abaixo o comando para implantar no Cloud Run. O código é compactado e enviado ao Cloud Build, que usa o Dockerfile para criar a imagem.
Como esta é uma implantação baseada em origem no Cloud Run, no console do Cloud para o serviço, você verá uma guia "Origem" com seu código.
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. Testar o serviço
Abra o URL do serviço no navegador da Web e faça upload de uma imagem. Ele vai aparecer na lista.
7. Alterar as permissões no bucket público do Cloud Storage
Como mencionado anteriormente, este codelab usa um bucket público do GCS. É recomendável excluir o bucket ou remover o acesso de allUsers ao bucket executando o seguinte comando:
gsutil iam ch -d allUsers:objectViewer gs://$GCS_BUCKET_NAME
Para confirmar que o acesso de allUsers foi removido, execute este comando:
gsutil iam get gs://$GCS_BUCKET_NAME
8. Parabéns
Parabéns por concluir o codelab.
O que aprendemos
- Como implantar o app FastAPI no Cloud Run
- Como usar as bibliotecas de cliente do Google para autenticação
- Como fazer upload de um arquivo para o Cloud Storage usando um serviço do Cloud Run
- Como ler e gravar dados no Firestore
- Como recuperar e exibir imagens do Cloud Storage em um serviço do Cloud Run
9. Limpar
Para excluir o serviço do Cloud Run, acesse o Console do Cloud Run em https://console.cloud.google.com/run e exclua o serviço.
Para excluir o bucket do Cloud Storage, execute os seguintes comandos:
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
Se você quiser excluir o projeto inteiro, acesse https://console.cloud.google.com/cloud-resource-manager, selecione o projeto criado na etapa 2 e escolha "Excluir". Se você excluir o projeto, vai precisar mudar os projetos no Cloud SDK. Para conferir a lista de todos os projetos disponíveis, execute gcloud projects list
.