06 · App Engine¶
Level 1 deployed containers with Cloud Run and VMs with Compute Engine. App Engine predates both — it's Google's original fully-managed Platform-as-a-Service: push code, and GCP handles the runtime, scaling, and load balancing for you, with a narrower but even simpler deployment model than Cloud Run.
Standard vs. Flexible environment¶
| Standard | Flexible | |
|---|---|---|
| Runtime | Sandboxed language runtimes (Python, Node, Go, Java, etc.) | Any Docker container |
| Cold start | Milliseconds — instances scale to zero | Minutes — runs on Compute Engine VMs under the hood |
| Scale to zero | Yes | No — always at least one instance running |
| Custom binaries/native deps | Restricted | Fully supported |
| Best for | Stateless HTTP APIs, low-traffic apps | Workloads needing custom runtime dependencies |
This module uses Standard — it's the cheaper, faster-scaling default for the common case (a web app with no unusual system dependencies).
Enable the API and initialize¶
An App Engine application is a one-time, per-project, permanent choice — once created, its region cannot be changed, and a project can have at most one App Engine application (though it can host many services within it).
The app.yaml¶
# app.yaml
runtime: python312
entrypoint: gunicorn -b :$PORT main:app
instance_class: F1
automatic_scaling:
min_instances: 0
max_instances: 5
target_cpu_utilization: 0.65
env_variables:
ENVIRONMENT: "production"
# main.py
from flask import Flask
app = Flask(__name__)
@app.route("/")
def index():
return "Hello from App Engine Standard!"
Deploy¶
Services to deploy:
descriptor: [app.yaml]
source: [/home/user/app]
target project: [gcp-mastery-path-123]
target service: [default]
target version: [20260803t101500]
target url: [https://gcp-mastery-path-123.uc.r.appspot.com]
Beginning deployment of service [default]...
Updating service [default]...done.
Deployed service [default] to [https://gcp-mastery-path-123.uc.r.appspot.com]
gcloud app browse # opens the deployed URL
curl https://gcp-mastery-path-123.uc.r.appspot.com
# Hello from App Engine Standard!
Versions and traffic splitting¶
Every gcloud app deploy creates a new version without touching
existing ones — nothing is overwritten by default:
gcloud app versions list
# SERVICE VERSION.ID TRAFFIC_SPLIT LAST_DEPLOYED
# default 20260803t101500 1.00 2026-08-03T10:15:00Z
Deploy a second version, then split traffic between them for a canary rollout instead of an all-at-once cutover:
gcloud app deploy app.yaml --version=v2-canary --no-promote
gcloud app services set-traffic default \
--splits=20260803t101500=0.9,v2-canary=0.1
gcloud app versions list
# SERVICE VERSION.ID TRAFFIC_SPLIT
# default 20260803t101500 0.90
# default v2-canary 0.10
--no-promote deploys the version without sending it any traffic
automatically — without that flag, gcloud app deploy promotes the new
version to 100% traffic immediately, which is rarely what you want for a
production canary.
Multiple services¶
App Engine supports multiple services (formerly "modules") inside one
application, each with its own app.yaml and independent scaling/versions:
# api-service/app.yaml
service: api
runtime: python312
automatic_scaling:
min_instances: 1
max_instances: 10
gcloud app deploy api-service/app.yaml
gcloud app services list
# SERVICE NUM_VERSIONS
# default 2
# api 1
Cron jobs¶
# cron.yaml
cron:
- description: "Daily report email"
url: /tasks/daily-report
schedule: every 24 hours
timezone: America/Chicago
App Engine cron calls your own app's URL on schedule with a special header
(X-Appengine-Cron: true) — your route handler should verify that header (or
restrict the route to internal traffic) so the endpoint can't be triggered by
an arbitrary public request.
Gotchas¶
- The region choice is permanent.
gcloud app create --region=cannot be changed later without deleting and recreating the entire application (and every service/version in it) — pick deliberately. min_instances: 0means real cold starts. Standard environment cold starts are fast (milliseconds to low seconds) but not zero — a latency-sensitive API should setmin_instances: 1+and accept the small always-on cost instead.--no-promoteis easy to forget. Omitting it sends 100% of traffic to the brand-new version immediately — always use it for anything you intend to canary or manually verify first.- Old versions keep costing money/quota until explicitly deleted —
gcloud app deploynever removes previous versions automatically, so a team that deploys often accumulates dozens of dormant versions. - Flexible environment has no scale-to-zero, so it bills continuously even at rest — don't reach for it unless Standard's sandboxed runtimes genuinely can't support what you need.
Cleanup¶
gcloud app services set-traffic default --splits=20260803t101500=1.0
gcloud app versions delete v2-canary --service=default --quiet
gcloud app versions delete 20260803t101500 --service=default --quiet
gcloud app services delete api --quiet
An App Engine application itself cannot be deleted independently of the GCP project — the practical cleanup for a learning project is deleting unused versions/services, or deleting the whole project when you're done with it entirely.
Cheat sheet¶
| Command | Purpose |
|---|---|
gcloud app create --region= |
One-time creation of the App Engine application. |
gcloud app deploy app.yaml |
Deploy a new version (promotes to 100% traffic by default). |
gcloud app deploy --version= --no-promote |
Deploy without shifting traffic (for canaries). |
gcloud app services set-traffic --splits= |
Split traffic between versions by percentage. |
gcloud app versions list / delete |
Inspect and remove old versions. |
gcloud app services list / delete |
Inspect and remove services. |
gcloud app deploy cron.yaml |
Deploy scheduled tasks. |
How It Actually Works¶
App Engine Standard runs your code inside a sandboxed, language-specific runtime that Google controls end-to-end — this is what enables true scale-to-zero with fast cold starts, because the platform can snapshot and restore runtime state far more cheaply than booting a general-purpose container, at the cost of restricting what your code can do (no arbitrary binaries, limited filesystem access). App Engine Flexible instead runs your app inside a normal Compute Engine-backed VM wrapped in a Docker container, trading that cold-start speed for full runtime freedom and a floor of at least one always-on instance per version. Traffic splitting works by rewriting the routing weight in App Engine's front-end proxy layer — deploying a new version doesn't send it any traffic automatically; a version only receives requests once you shift percentage-based routing (or default-serving) to it, which is what makes canary releases and instant rollback both just routing-table changes, not redeploys.
Exercise¶
Deploy a minimal Flask (or equivalent) app to App Engine Standard, confirm it
responds via gcloud app browse. Make a small change, deploy it as
--version=v2-canary --no-promote, then split traffic 90/10 between the two
versions and confirm with repeated curl calls that roughly 1 in 10 requests
hits the new version. Finish by promoting v2-canary to 100%, then delete
the older version.