Deploy a Release Dashboard on Cloud Run
Build a Dockerized dashboard and deploy it to Cloud Run with Terraform.
Introduction
30 Second Summary
A release can look healthy while leaving you unsure which version is actually live. That uncertainty makes every update harder to trust.
In this project, you will build a release dashboard on Google Cloud. You will package its Flask API with Docker before Terraform deploys it as a public Cloud Run service that reports its live runtime identity.
What You'll Build
Open the finished public URL to watch six dashboard cards report the exact runtime serving your release.
By the end of this project, you'll have:
- A static dashboard preview that you can open entirely in your browser to see the missing backend metadata for yourself.
- A versioned container image that you can run locally before publishing its visible v1 tag to Artifact Registry.
- A public release URL whose metadata cards expose the runtime identity supplied by Cloud Run.
- Secret Mission: Build and release version 2. Use Terraform to create a new Cloud Run revision without creating a second service.
Are there any prerequisites?
You need a Google account with access to a billing-enabled Cloud Billing account. The tutorial's expected usage fits within current free allowances, but overages are billable.
All development happens in Cloud Shell through your browser, so you do not need to install local software.
Before We Start
This is your moment to commit to what you are building before the hands-on work begins. You will create a public Cloud Release Dashboard that makes Docker packaging, Terraform-managed infrastructure, and Cloud Run runtime evidence visible.
Create Your Browser-Based Cloud Workspace
A dedicated Google Cloud project keeps this experiment separate from your other work. It also gives you one place to track its billing.
Cloud Shell gives you an authenticated browser terminal with Docker plus Terraform preinstalled. Your whole workspace stays in the browser.
In this step, get ready to:
- Create a billing-linked Google Cloud project with its permanent ID recorded.
- Configure Cloud Shell to use the new project.
- Prepare the release dashboard workspace with verified Docker and Terraform CLIs.
Create a billing-linked Google Cloud project
A project acts as the boundary for the dashboard resources you create later. Keeping this build in its own project makes its activity easier to identify.
Billing stays contained
Creating this project links its usage to your billing account.
Tutorial usage is expected to stay inside current free allowances. Usage beyond those allowances is billable.
- Open the Google Cloud console in your browser.
- Sign in with the Google account linked to your confirmed billing account if prompted.
- Open Manage resources.
- Click Create project.
- Enter Cloud Release Dashboard as the project name.
- Select your confirmed billing account.
- Choose No organization when that option is available.
- Click Create.
Good progress. You now have a billing boundary dedicated to this release dashboard.
- Record the permanent project ID here: your permanent project ID.
Why record the project ID?
The project name is the label you recognize in the console. The permanent project ID identifies this specific project in commands.
You reuse this ID when you configure Cloud Shell. Later steps also use it to address your cloud resources.
Activate Cloud Shell for your project
Cloud Shell authenticates with the Google credentials you used for the console. This lets its command-line tools work with your selected project.
The first session can take a moment to prepare. That pause means the browser environment is starting.
- Use the project selector in the console header to select Cloud Release Dashboard.
- Click Activate Cloud Shell in the console header.
You'll see an authenticated terminal inside the Google Cloud console.
- Set the active gcloud project by substituting your permanent project ID for PROJECT_ID before running this command:
gcloud config set project PROJECT_ID
What does this command do?
The gcloud CLI stores the project as the active project in its current configuration. Future Google Cloud commands can now target the dashboard project.
You'll see confirmation that the project property was updated. That confirms Cloud Shell is pointing at your new project.
Project update not succeeding?
- Copy the permanent project ID again from the project details.
- Confirm Cloud Shell uses the same Google account that created the project.
Ask for help with the project configuration.
Prepare the browser workspace
The project uses one folder for everything you build. Separate subdirectories keep the dashboard template apart from its infrastructure configuration.
- Create the two project directories plus enter release-dashboard by running these commands:
mkdir -p ~/release-dashboard/templates ~/release-dashboard/infra
cd ~/release-dashboard
How is the workspace organized?
- The templates directory holds the browser dashboard in the next step.
- The infra directory holds the Terraform configuration in a later step.
- The final command places your terminal inside ~/release-dashboard.
Cloud Shell Editor gives you a browser-based file tree plus code editor. Opening the current directory keeps every project file in view.
- Launch Cloud Shell Editor for the current project directory by running this command:
cloudshell edit .
What does this command do?
The command opens Cloud Shell Editor in the same console pane. The dot tells the editor to use your current directory.
You'll see templates plus infra beneath release-dashboard in the file tree.
Before you run these checks, which two CLI tools do you expect Cloud Shell to identify?
- Verify the preinstalled Docker plus Terraform CLIs by running these commands in the Cloud Shell terminal:
docker version
terraform version
What do these checks prove?
Each command asks a preinstalled CLI to report its version information. Successful output proves that both tools are available in your browser workspace.
You'll see version information from Docker plus Terraform. That is the setup hurdle cleared.
Missing version information?
- Run the checks in the Cloud Shell terminal inside the Google Cloud console.
- Enter each command on its own line.
Help me troubleshoot the missing CLI output.
Your Google Cloud project is selected. Your browser workspace is ready for the first visible dashboard preview.
Preview the Static Dashboard
Your Cloud Shell workspace is ready inside the new project. You can now turn that empty workspace into the first visible version of your release dashboard.
A static page can display the dashboard interface. Its metadata request exposes what a browser-only page can learn about the environment serving it.
In this step, get ready to:
- Create the release dashboard interface in templates/index.html.
- Add a browser request that populates the runtime metadata cards.
- Preview the page to expose the missing runtime metadata.
Build the dashboard structure
The HTML structure gives every runtime value its own card. Each card also has an element ID that the browser can update later.
- Return to the Cloud Shell Editor from the previous step.
- Right-click the templates folder in the Explorer sidebar.
- Select New File.
- Enter index.html.
- Paste this dashboard structure into templates/index.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cloud Release Dashboard</title>
</head>
<body>
<main>
<div class="eyebrow">Container release evidence</div>
<h1>Cloud Release Dashboard</h1>
<p class="lede">A small dashboard that makes the path from browser preview to Docker image to Terraform-managed Cloud Run revision visible.</p>
<div class="status"><span class="dot" id="status-dot"></span><span id="status-text">Checking runtime...</span></div>
<section class="grid">
<article class="card"><div class="label">Runtime</div><div class="value" id="runtime">Loading...</div></article>
<article class="card"><div class="label">Application version</div><div class="value" id="version">Loading...</div></article>
<article class="card"><div class="label">Cloud Run service</div><div class="value" id="service">Loading...</div></article>
<article class="card"><div class="label">Revision</div><div class="value" id="revision">Loading...</div></article>
<article class="card"><div class="label">Configuration</div><div class="value" id="configuration">Loading...</div></article>
<article class="card"><div class="label">Listening port</div><div class="value" id="port">Loading...</div></article>
</section>
<footer>The live values come from <code>/metadata</code> and Cloud Run environment variables.</footer>
</main>
</body>
</html>
What does this structure create?
- The status-text element reports whether runtime metadata is connected.
- The six cards reserve space for the runtime identity.
- The IDs let the browser place each response value into the correct card.
- Save templates/index.html.
A simple HTTP server can serve the file through Cloud Shell Web Preview. The server keeps this terminal occupied until you stop it with Control-C.
- Switch back to the Cloud Shell terminal from the previous step.
- Start the preview from the templates directory by running these commands:
cd templates
python -m http.server 8080
What do these commands do?
- The first command moves the terminal into ~/release-dashboard/templates.
- The second command serves the files in that directory on port 8080.
- Click Web Preview in the Cloud Shell toolbar.
- Select port 8080.
You'll see the unstyled Cloud Release Dashboard with six metadata cards. Each card currently displays Loading....
Preview not loading?
Confirm that the static server is still running in the Cloud Shell terminal. Start it from the templates directory if the browser displays a file listing instead of the dashboard.
Check that the preview uses port 8080.
Help me troubleshoot the Cloud Shell Web Preview.
Connect the metadata cards
The page now needs JavaScript that can place runtime values into those six cards. The first function handles that visual update.
- Find the closing </body> tag in templates/index.html.
- Paste this script directly above the closing </body> tag:
<script>
const fields = ["runtime", "version", "service", "revision", "configuration", "port"];
function show(data, healthy) {
fields.forEach((field) => {
document.getElementById(field).textContent = data[field];
});
document.getElementById("status-text").textContent = healthy ? "Runtime metadata connected" : "Static preview only";
document.getElementById("status-dot").style.background = healthy ? "#34d399" : "#fbbf24";
document.getElementById("status-dot").style.boxShadow = healthy ? "0 0 18px #34d399" : "0 0 18px #fbbf24";
}
</script>
How are the cards updated?
- The fields array lists the six card IDs.
- The show() function places one runtime value into each matching element.
- The healthy value controls the status text plus the status dot colour.
The browser also needs a function that requests /metadata. Its fallback makes the static page's limitation visible inside the dashboard.
- Find the closing </script> tag.
- Paste this function directly above the closing </script> tag:
async function loadMetadata() {
try {
const response = await fetch("/metadata");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
show(await response.json(), true);
} catch (error) {
show({
runtime: "Static preview",
version: "v0",
service: "No backend yet",
revision: "No backend yet",
configuration: "No backend yet",
port: "8080"
}, false);
}
}
loadMetadata();
What happens when the page loads?
- The loadMetadata() function requests runtime data from /metadata.
- A successful response is passed to show() as connected metadata.
- The fallback gives every card a clear static-preview value when the request fails.
Before you refresh, what do you think the static server can return when the browser requests /metadata?
- Save templates/index.html.
- Refresh the Web Preview tab.
Your status changes to Static preview only.
The runtime card shows Static preview. The version card shows v0.
The service, revision, and configuration cards each show No backend yet. The port card shows 8080.
The request to /metadata fails because the static server only serves files. This visible shortfall is the intended result.
Cards still showing Loading?
Confirm that both script chunks sit above the closing </body> tag. Check that loadMetadata(); appears before </script>.
Compare the element IDs with the values in the fields array. A spelling mismatch prevents the matching card from updating.
Help me fix metadata cards that remain on Loading.
Style and verify the dashboard
The runtime behavior now exposes the missing metadata source. CSS turns that working structure into a dashboard where the status and cards are easy to scan.
- Find the closing </head> tag in templates/index.html.
- Paste these page-level styles directly above the closing </head> tag:
<style>
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: #07111f;
color: #e8f0ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 15% 15%, #17365f 0, transparent 36%),
radial-gradient(circle at 85% 10%, #23335f 0, transparent 30%),
#07111f;
}
</style>
What do these styles control?
- The :root selector sets the dashboard's dark colour scheme.
- The universal selector keeps element sizing predictable.
- The body selector creates the layered blue background.
- Save templates/index.html.
- Refresh the Web Preview tab.
You'll see the page switch to a dark blue background. The text changes to a light colour.
Background still white?
Confirm that the opening <style> tag appears before :root.
Check that the closing </style> tag appears before </head>.
Help me troubleshoot the dashboard background styles.
The next styles establish the page width plus the release status area. They create the visual hierarchy above the metadata cards.
- Find the closing </style> tag.
- Paste these layout styles directly above the closing </style> tag:
main { width: min(1040px, 92vw); margin: 0 auto; padding: 64px 0; }
.eyebrow { color: #75d5ff; letter-spacing: .18em; text-transform: uppercase; font-size: .78rem; }
h1 { margin: 10px 0 12px; font-size: clamp(2.4rem, 7vw, 4.8rem); line-height: .98; }
.lede { max-width: 680px; color: #aabbd3; font-size: 1.08rem; line-height: 1.7; }
.status {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 22px 0 34px;
padding: 9px 14px;
border: 1px solid #315071;
border-radius: 999px;
background: #0b1a2c;
}
How does the header take shape?
- The main selector centres the dashboard within the browser.
- The eyebrow, heading, and lead styles establish the release theme.
- The .status selector turns the runtime message into a compact status badge.
- Save templates/index.html.
- Refresh the Web Preview tab.
You'll see a centred heading with a blue eyebrow label. The runtime status now sits inside a rounded badge.
Header still looks unformatted?
Confirm that these selectors appear inside the existing <style> element. Styles placed after </style> render as page text.
Check that every selector keeps its opening and closing braces.
Help me fix the dashboard header styles.
The final style group turns the six articles into a responsive card grid. It also completes the status dot plus footer treatment.
- Find the closing </style> tag again.
- Paste these card styles directly above the closing </style> tag:
.dot { width: 10px; height: 10px; border-radius: 50%; background: #fbbf24; box-shadow: 0 0 18px #fbbf24; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px; }
.card {
min-height: 150px;
padding: 22px;
border: 1px solid #233c59;
border-radius: 18px;
background: rgba(10, 25, 43, .86);
box-shadow: 0 18px 45px rgba(0, 0, 0, .22);
}
.label { color: #8298b5; font-size: .75rem; letter-spacing: .12em; text-transform: uppercase; }
.value { margin-top: 18px; color: #f7fbff; font-size: 1.05rem; font-weight: 700; overflow-wrap: anywhere; }
footer { margin-top: 28px; color: #6f86a5; font-size: .9rem; }
code { color: #8ee7ff; }
How do the cards work?
- The .grid selector arranges cards into columns that adapt to the browser width.
- The .card selector gives each metadata value its own bordered panel.
- The label and value styles separate each field name from its runtime value.
- Save templates/index.html.
- Refresh the Web Preview tab.
You'll see six dark cards arranged in a responsive grid. The amber status dot marks the static-preview state.
Cards appearing as plain text?
Check that the stylesheet contains both .grid and .card.
Confirm that the HTML uses class="grid" on the section. Confirm that every article uses class="card".
Help me troubleshoot the metadata card layout.
✔️ Awesome, I've got everything!
Great work. Save templates/index.html before completing the final preview check.
ⓧ I'd like to double check the full code
Compare your complete templates/index.html file with this reference.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cloud Release Dashboard</title>
<style>
:root {
color-scheme: dark;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
background: #07111f;
color: #e8f0ff;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(circle at 15% 15%, #17365f 0, transparent 36%),
radial-gradient(circle at 85% 10%, #23335f 0, transparent 30%),
#07111f;
}
main { width: min(1040px, 92vw); margin: 0 auto; padding: 64px 0; }
.eyebrow { color: #75d5ff; letter-spacing: .18em; text-transform: uppercase; font-size: .78rem; }
h1 { margin: 10px 0 12px; font-size: clamp(2.4rem, 7vw, 4.8rem); line-height: .98; }
.lede { max-width: 680px; color: #aabbd3; font-size: 1.08rem; line-height: 1.7; }
.status {
display: inline-flex;
align-items: center;
gap: 10px;
margin: 22px 0 34px;
padding: 9px 14px;
border: 1px solid #315071;
border-radius: 999px;
background: #0b1a2c;
}
.dot { width: 10px; height: 10px; border-radius: 50%; background: #fbbf24; box-shadow: 0 0 18px #fbbf24; }
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(230px, 1fr)); gap: 16px; }
.card {
min-height: 150px;
padding: 22px;
border: 1px solid #233c59;
border-radius: 18px;
background: rgba(10, 25, 43, .86);
box-shadow: 0 18px 45px rgba(0, 0, 0, .22);
}
.label { color: #8298b5; font-size: .75rem; letter-spacing: .12em; text-transform: uppercase; }
.value { margin-top: 18px; color: #f7fbff; font-size: 1.05rem; font-weight: 700; overflow-wrap: anywhere; }
footer { margin-top: 28px; color: #6f86a5; font-size: .9rem; }
code { color: #8ee7ff; }
</style>
</head>
<body>
<main>
<div class="eyebrow">Container release evidence</div>
<h1>Cloud Release Dashboard</h1>
<p class="lede">A small dashboard that makes the path from browser preview to Docker image to Terraform-managed Cloud Run revision visible.</p>
<div class="status"><span class="dot" id="status-dot"></span><span id="status-text">Checking runtime...</span></div>
<section class="grid">
<article class="card"><div class="label">Runtime</div><div class="value" id="runtime">Loading...</div></article>
<article class="card"><div class="label">Application version</div><div class="value" id="version">Loading...</div></article>
<article class="card"><div class="label">Cloud Run service</div><div class="value" id="service">Loading...</div></article>
<article class="card"><div class="label">Revision</div><div class="value" id="revision">Loading...</div></article>
<article class="card"><div class="label">Configuration</div><div class="value" id="configuration">Loading...</div></article>
<article class="card"><div class="label">Listening port</div><div class="value" id="port">Loading...</div></article>
</section>
<footer>The live values come from <code>/metadata</code> and Cloud Run environment variables.</footer>
</main>
<script>
const fields = ["runtime", "version", "service", "revision", "configuration", "port"];
function show(data, healthy) {
fields.forEach((field) => {
document.getElementById(field).textContent = data[field];
});
document.getElementById("status-text").textContent = healthy ? "Runtime metadata connected" : "Static preview only";
document.getElementById("status-dot").style.background = healthy ? "#34d399" : "#fbbf24";
document.getElementById("status-dot").style.boxShadow = healthy ? "0 0 18px #34d399" : "0 0 18px #fbbf24";
}
async function loadMetadata() {
try {
const response = await fetch("/metadata");
if (!response.ok) throw new Error(`HTTP ${response.status}`);
show(await response.json(), true);
} catch (error) {
show({
runtime: "Static preview",
version: "v0",
service: "No backend yet",
revision: "No backend yet",
configuration: "No backend yet",
port: "8080"
}, false);
}
}
loadMetadata();
</script>
</body>
</html>
What should match?
- The stylesheet sits inside the <head> element.
- The metadata cards use IDs that match the fields array.
- The script ends with the loadMetadata(); call.
Before your final refresh, which values do you expect the static server to leave unavailable?
- Refresh the Web Preview tab one final time.
You'll see the styled dashboard report Static preview only with an amber status dot. The runtime card shows Static preview.
You'll also see version v0 plus No backend yet in the service, revision, and configuration cards. The listening port remains 8080.
- Return to the Cloud Shell terminal running the static server.
- Press Control-C to stop the server.
- Return the terminal to ~/release-dashboard by running this command:
cd ..
What does this command do?
The command moves the terminal from ~/release-dashboard/templates back to ~/release-dashboard.
That's the first visible release state complete. Next, you'll use Docker to give the dashboard a metadata source inside a portable container.
Containerize the Dashboard
Your static preview exposed the missing backend. Its request to /metadata could only show placeholder cards.
Now you'll package a Flask API inside a Docker image. Gunicorn will serve the same dashboard from a portable container with pinned dependencies.
In this step, get ready to:
- Create the Flask backend with a runtime metadata route.
- Define the Docker image with exact Python dependencies.
- Build the image and test its metadata through Web Preview.
Create the Flask metadata API
The browser already knows how to request metadata. The backend now needs routes that serve the page and answer that request.
- Create app.py inside ~/release-dashboard with the Cloud Shell Editor file tree.
- Add the Flask application setup and dashboard route by pasting this code into app.py:
from os import environ
from flask import Flask, render_template
app = Flask(__name__)
APP_VERSION = "v1"
@app.route("/")
def dashboard():
return render_template("index.html")
What does this code do?
- The environ import gives the application access to runtime environment variables.
- Flask(__name__) creates the web application.
- APP_VERSION identifies this release as v1.
- dashboard() renders the existing templates/index.html file when someone visits the root route.
- Save app.py.
- Check the Cloud Shell Editor file tree for app.py inside release-dashboard.
You should see app.py beside the existing templates folder.
Can't find app.py?
Check that you created app.py directly inside ~/release-dashboard. It should sit outside the templates folder.
Make sure the filename ends with .py.
Help me place app.py correctly.
- Place your cursor below the dashboard() function in app.py.
- Add the metadata route by pasting this code:
@app.route("/metadata")
def metadata():
on_cloud_run = bool(environ.get("K_SERVICE"))
return {
"runtime": "Cloud Run" if on_cloud_run else "Docker container",
"version": APP_VERSION,
"service": environ.get("K_SERVICE", "Not supplied locally"),
"revision": environ.get("K_REVISION", "Not supplied locally"),
"configuration": environ.get("K_CONFIGURATION", "Not supplied locally"),
"port": environ.get("PORT", "8080"),
}
How does the metadata route work?
- K_SERVICE tells the route whether the application has a Cloud Run service identity.
- K_REVISION provides the immutable revision name after deployment.
- K_CONFIGURATION provides the revision configuration name.
- PORT reports the listening port supplied by the runtime.
- Not supplied locally makes each missing Cloud Run value visible during the local container test.
- Save app.py.
- Confirm that metadata() appears below dashboard().
Your backend now has one route for the dashboard and one route for its metadata.
Is the metadata route misplaced?
Check that @app.route("/metadata") starts at the left edge of the file. The route must sit below the complete dashboard() function.
Help me check my Flask routes.
The container needs repeatable dependency versions. A small requirements file gives every build the same Flask and Gunicorn releases.
- Create requirements.txt inside ~/release-dashboard with the Cloud Shell Editor file tree.
- Pin Flask 3.1.3 and Gunicorn 26.2.0 by adding this content:
Flask==3.1.3
gunicorn==26.2.0
Why pin these dependencies?
Flask handles the dashboard routes. Gunicorn runs the Flask application as the container's production web server.
Exact version pins keep later image builds reproducible.
- Save requirements.txt.
- Check the Cloud Shell Editor file tree for requirements.txt beside app.py.
You should now see both application files directly inside release-dashboard.
Seeing the wrong dependency names?
Check the capital letter in Flask. Confirm that each dependency uses two equals signs before its version.
Help me check requirements.txt.
Define the container image
A container image is the packaged blueprint for the application. The Dockerfile records how to assemble that blueprint from Python and your project files.
- Create Dockerfile inside ~/release-dashboard with the Cloud Shell Editor file tree.
- Define the image by adding this content to Dockerfile:
FROM python:3.14.8-slim-trixie
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY templates ./templates
ENV PORT=8080
EXPOSE 8080
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
How does the Dockerfile build the image?
- FROM python:3.14.8-slim-trixie supplies Python 3.14.8 in a slim base image.
- WORKDIR /app sets the container's application directory.
- RUN pip install --no-cache-dir -r requirements.txt installs the pinned dependencies inside the image.
- COPY adds the backend and existing dashboard template.
- ENV PORT=8080 gives local runs the same default port used by the application.
- CMD starts Gunicorn with the Flask object at app:app.
- Save Dockerfile.
- Check that its filename has no extension.
You should see a file named exactly Dockerfile in release-dashboard.
Does Dockerfile have an extension?
Rename the file if Cloud Shell Editor saved it as Dockerfile.txt or another variation. Docker expects the exact filename from the build context.
Help me check my Dockerfile.
Docker sends files from the current folder into the build context. A .dockerignore file keeps unrelated project data out of that context.
- Create .dockerignore inside ~/release-dashboard with the Cloud Shell Editor file tree.
- Exclude unrelated files by adding this content:
.git
__pycache__
*.pyc
infra
What stays out of the image?
- .git excludes repository history.
- __pycache__ excludes generated Python cache data.
- *.pyc excludes compiled Python bytecode.
- infra keeps later Terraform configuration outside the application image.
- Save .dockerignore.
- Check the Cloud Shell Editor file tree for all four new files.
You should see app.py, requirements.txt, Dockerfile, and .dockerignore beside the existing templates folder.
Is .dockerignore hidden?
Files beginning with a dot can be less prominent in a file tree. Confirm that the name is exactly .dockerignore.
Help me find .dockerignore.
✔️ Awesome, I've got everything!
Great. Double-check that you saved every new file before building the image.
ⓧ I'd like to double check the full code
Compare app.py with this complete file.
from os import environ
from flask import Flask, render_template
app = Flask(__name__)
APP_VERSION = "v1"
@app.route("/")
def dashboard():
return render_template("index.html")
@app.route("/metadata")
def metadata():
on_cloud_run = bool(environ.get("K_SERVICE"))
return {
"runtime": "Cloud Run" if on_cloud_run else "Docker container",
"version": APP_VERSION,
"service": environ.get("K_SERVICE", "Not supplied locally"),
"revision": environ.get("K_REVISION", "Not supplied locally"),
"configuration": environ.get("K_CONFIGURATION", "Not supplied locally"),
"port": environ.get("PORT", "8080"),
}
Compare requirements.txt with this complete file.
Flask==3.1.3
gunicorn==26.2.0
Compare Dockerfile with this complete file.
FROM python:3.14.8-slim-trixie
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
COPY templates ./templates
ENV PORT=8080
EXPOSE 8080
CMD exec gunicorn --bind :$PORT --workers 1 --threads 8 --timeout 0 app:app
Compare .dockerignore with this complete file.
.git
__pycache__
*.pyc
infra
Confirm that the existing templates/index.html file remains unchanged from the previous step.
Build and test the container image
The four files now describe a complete image. Building it turns that description into the reusable release artifact named release-dashboard:v1.
- Build the version v1 image from ~/release-dashboard by running:
docker build -t release-dashboard:v1 .
What does this command do?
The build reads Dockerfile from the current folder. The -t option gives the completed image the name release-dashboard:v1.
You should see the dependency installation and file-copy steps complete in the terminal. The final build output confirms that the image received its tag.
Did the image build fail?
Check that the terminal is still at ~/release-dashboard. Confirm that Dockerfile sits in that folder.
A failure during a COPY step usually means the referenced file is missing from release-dashboard.
Help me debug my Docker build.
The completed image is only a blueprint until Docker starts a container from it. The running container keeps this terminal occupied while Gunicorn serves requests.
Before you run it, which dashboard values do you expect a local container to know without a Cloud Run identity?
- Start the local container on port 8080 by running:
docker run --rm -p 8080:8080 release-dashboard:v1
What does this command do?
- --rm removes the test container after it stops.
- -p 8080:8080 publishes the container's port through Cloud Shell.
- release-dashboard:v1 selects the image you just built.
- Click Web Preview in the Cloud Shell toolbar.
- Select port 8080.
You should see the dashboard report Docker container with version v1. The service, revision, and configuration cards should each show Not supplied locally.
Still seeing the static preview?
Refresh the Web Preview page after the container starts. Confirm that the terminal running Docker remains active.
If port 8080 is busy, check that the earlier static HTTP server was stopped with Control-C.
Help me troubleshoot the container preview.
- Return to the Cloud Shell terminal running the container.
- Press Control-C to stop the test container.
You should see the command prompt return. Your temporary container has stopped.
You now have a tested image that serves real metadata without depending on the static server. Next, you'll create an Artifact Registry destination with Terraform so this release can move beyond Cloud Shell.
Provision a Registry and Publish the Image
Your tested Docker image proves that the dashboard works inside a container. The image still exists only inside your Google Cloud workspace.
Cloud Run needs an image stored at a remote address. You will use Terraform to create an Artifact Registry repository. You will then publish the tested v1 image to it.
In this step, get ready to:
- Describe the registry infrastructure with Terraform.
- Create the repository from a reviewed Terraform plan.
- Publish the tested v1 image to Artifact Registry.
Define the registry infrastructure
Infrastructure as code records cloud resources in configuration files. Terraform can compare those files with your Google Cloud project before it creates anything.
- Create infra/versions.tf by using the new-file control beside the existing infra folder in Cloud Shell Editor.
- Paste this provider configuration into infra/versions.tf:
terraform {
required_providers {
google = {
version = "8.6.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
What Does This Configuration Do?
- The required_providers block pins the Terraform Provider for Google Cloud to version 8.6.0.
- The provider block connects later resources to the project input.
- The same provider block applies the shared region input.
- Save infra/versions.tf.
- Confirm versions.tf appears beneath the infra folder in the file sidebar.
Provider File Showing a Warning?
Confirm that versions.tf sits inside infra. Compare each brace with the code block above.
Help me check the provider configuration.
- Create infra/variables.tf by using the new-file control beside the infra folder.
- Paste these project inputs into infra/variables.tf:
variable "project_id" {
description = "Google Cloud project ID"
type = string
}
variable "region" {
description = "Region shared by Artifact Registry and Cloud Run"
type = string
default = "us-west1"
}
What Do These Variables Control?
- The project_id variable identifies the permanent Google Cloud project that receives the resources.
- The region variable keeps the repository in us-west1.
- The next step uses that region for the Cloud Run service.
- Save infra/variables.tf.
- Confirm variables.tf appears beside versions.tf in the infra folder.
Variables File Showing a Warning?
Check that each variable block has one opening brace. Check that each variable block has one closing brace.
Help me fix the Terraform variables.
- Create infra/terraform.tfvars by using the new-file control beside the infra folder.
- Paste this project assignment into infra/terraform.tfvars:
project_id = "PROJECT_ID"
Why Does Terraform Need the Project ID?
This file supplies the permanent project ID to the project_id variable. Terraform uses that value to place every resource in the intended billing-enabled project.
- Replace PROJECT_ID with the permanent project ID you recorded earlier.
- Save infra/terraform.tfvars.
- Confirm the saved value matches the project already selected in Cloud Shell.
Unsure Which Project Value to Use?
Use the permanent project ID from the project creation screen. The display name Cloud Release Dashboard is a different value.
Help me identify the correct project ID.
- Create infra/registry.tf by using the new-file control beside the infra folder.
- Paste these API resources plus the repository resource into infra/registry.tf:
resource "google_project_service" "artifact_registry" {
project = var.project_id
service = "artifactregistry.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "cloud_run" {
project = var.project_id
service = "run.googleapis.com"
disable_on_destroy = false
}
resource "google_artifact_registry_repository" "dashboard" {
project = var.project_id
location = var.region
repository_id = "release-dashboard"
description = "Versioned images for the cloud release dashboard"
format = "DOCKER"
depends_on = [google_project_service.artifact_registry]
}
What Resources Does This Declare?
- The first google_project_service resource enables the Artifact Registry API.
- The second google_project_service resource enables the Cloud Run API for the deployment step.
- The google_artifact_registry_repository resource creates a Docker repository named release-dashboard.
- The depends_on value makes the repository wait for its API.
- Save infra/registry.tf.
- Confirm the file contains two API resources plus one repository resource.
Registry File Showing a Warning?
Compare both service values with the code block above. Confirm the repository block closes after its depends_on line.
Help me check the registry resources.
- Create infra/outputs.tf by using the new-file control beside the infra folder.
- Paste this repository output into infra/outputs.tf:
output "registry_uri" {
value = google_artifact_registry_repository.dashboard.registry_uri
}
Why Expose the Registry URI?
The registry_uri output prints the repository address after the apply. That address confirms where your published Docker images belong.
- Save infra/outputs.tf.
- Confirm all five Terraform files appear inside the infra folder.
Missing a Terraform File?
Check the infra folder for versions.tf, variables.tf, terraform.tfvars, registry.tf, plus outputs.tf.
Help me fix the Terraform file layout.
✔️ Awesome, I've got everything!
Your five Terraform files are saved inside infra. The configuration is ready for Terraform to initialize.
ⓧ I'd like to double check the full code
Compare infra/versions.tf with this complete reference:
terraform {
required_providers {
google = {
version = "8.6.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
This file pins the provider version. It connects the provider to the shared inputs.
Compare infra/variables.tf with this complete reference:
variable "project_id" {
description = "Google Cloud project ID"
type = string
}
variable "region" {
description = "Region shared by Artifact Registry and Cloud Run"
type = string
default = "us-west1"
}
This file defines the project input. It also defines the shared us-west1 region.
Compare the structure of infra/terraform.tfvars with this reference. Keep your permanent project ID in place of PROJECT_ID:
project_id = "PROJECT_ID"
This file supplies your permanent project ID to Terraform.
Compare infra/registry.tf with this complete reference:
resource "google_project_service" "artifact_registry" {
project = var.project_id
service = "artifactregistry.googleapis.com"
disable_on_destroy = false
}
resource "google_project_service" "cloud_run" {
project = var.project_id
service = "run.googleapis.com"
disable_on_destroy = false
}
resource "google_artifact_registry_repository" "dashboard" {
project = var.project_id
location = var.region
repository_id = "release-dashboard"
description = "Versioned images for the cloud release dashboard"
format = "DOCKER"
depends_on = [google_project_service.artifact_registry]
}
This file enables both APIs. It also declares the Docker repository.
Compare infra/outputs.tf with this complete reference:
output "registry_uri" {
value = google_artifact_registry_repository.dashboard.registry_uri
}
This file prints the repository address after Terraform creates it.
Create the registry with Terraform
Terraform must initialize the configuration before it can understand the provider resources. A plan then shows the proposed difference between your files and the empty Terraform state.
- Switch back to the Cloud Shell terminal from earlier.
- Change from ~/release-dashboard to its infra folder using terminal directory navigation.
- Initialize the Terraform configuration by running this command:
terraform init
Terraform downloads the pinned provider. It prepares the infra folder for plans plus state.
Initialization Failed?
Confirm that your terminal is working from ~/release-dashboard/infra. Check that versions.tf contains provider version 8.6.0.
Help me troubleshoot Terraform initialization.
- Standardize the Terraform file formatting by running this command:
terraform fmt
Terraform updates the configuration files to its standard spacing. Any filename printed in the terminal was reformatted.
- Check the formatted configuration by running this command:
terraform validate
Terraform checks whether the configuration is internally consistent. A successful result confirms that the provider references plus resource references connect correctly.
Validation Found a Problem?
Return to the file named in the validation output. Compare its referenced line with the full-code tab before saving the correction.
Help me fix the Terraform validation issue.
Before you inspect the plan, make a mental prediction about how many resources Terraform will propose.
- Preview the proposed infrastructure changes by running this command:
terraform plan
The plan proposes two API resources plus one Docker repository. It does not include a Cloud Run service or an IAM invoker grant.
Why Review the Plan?
The plan exposes the exact difference between your configuration and the current cloud state. That review gives you a chance to catch the wrong project before resources are created.
Plan Showing the Wrong Project?
Return to infra/terraform.tfvars. Confirm that its value matches the permanent project ID selected in Cloud Shell.
Help me correct the Terraform plan.
This apply creates persistent resources inside your billing-enabled project. Tutorial usage is expected to remain within the current free allowances.
- Start the reviewed infrastructure apply by running this command:
- Review the proposed resource changes when Terraform displays them again.
- Enter yes when Terraform requests approval.
terraform apply
What Does This Command Do?
Terraform recalculates the plan before changing Google Cloud. It then enables both APIs plus the release-dashboard repository after your approval.
The terminal can remain busy while Google Cloud enables the APIs. Keep the session open until Terraform returns the output.
You will see the registry_uri output when the apply succeeds. Its value follows us-west1-docker.pkg.dev/PROJECT_ID/release-dashboard with your permanent project ID in the address.
Apply Failed?
Confirm that terraform.tfvars contains your billing-enabled project ID. Use the named resource in the error to identify the relevant block in registry.tf.
Help me troubleshoot the Terraform apply.
Publish the version 1 image
The repository now has a durable address. Docker needs permission to use that registry before it can upload your local release-dashboard:v1 image.
- Return to ~/release-dashboard using terminal directory navigation.
- Configure Docker authentication for the repository host by running this command:
- Approve the Docker configuration update if Cloud Shell requests confirmation.
gcloud auth configure-docker us-west1-docker.pkg.dev
What Does This Command Change?
The command configures Docker to use your signed-in Google Cloud credentials for us-west1-docker.pkg.dev. It does not upload the image.
You will regain the terminal prompt when the authentication configuration is ready.
Docker Authentication Failed?
Confirm that Cloud Shell remains signed into the account used for this project. Check that the repository region is us-west1.
Help me fix Docker authentication.
- Replace your-project-id with your permanent project ID in this command.
- Store the project ID in the current terminal by running the completed command:
PROJECT_ID="your-project-id"
What Does PROJECT_ID Hold?
The PROJECT_ID shell variable stores your permanent project ID. Keep this terminal open so the remaining commands can reuse the value.
- Build the complete Artifact Registry image address by running this command:
IMAGE="us-west1-docker.pkg.dev/${PROJECT_ID}/release-dashboard/release-dashboard:v1"
How Is the Image Address Structured?
The address combines the registry host with your project ID. It also includes the repository name plus the v1 image tag.
- Attach the registry address to the tested local image by running this command:
docker tag release-dashboard:v1 "$IMAGE"
What Does Tagging Do?
The tag gives the existing local image a second name containing its registry destination. The underlying image content stays unchanged.
The terminal prompt returns when the local tag is ready.
Image Tagging Failed?
Confirm that the tested local image is named release-dashboard:v1. Check that both shell assignments ran in this terminal.
Help me troubleshoot the Docker tag.
Before you publish the image, predict which final detail will prove that Artifact Registry received this exact build.
- Upload the tagged v1 image by running this command:
docker push "$IMAGE"
What Does the Push Prove?
Docker uploads the image layers to the Terraform-created repository. The final digest identifies the immutable image content stored behind the v1 tag.
You will see layer upload progress followed by a digest. That is the hard part complete. Your tested image is now a deployable release artifact.
Image Push Failed?
Confirm that the Terraform apply completed successfully. Repeat the Docker authentication command if the current session lost its registry configuration.
Check that IMAGE contains your permanent project ID plus the release-dashboard repository name.
Help me fix the Artifact Registry push.
Your registry now stores the tested v1 image. Next, you will deploy that artifact as a public Cloud Run service with Terraform.
Deploy the Public Cloud Run Dashboard
Your v1 image is now stored in Artifact Registry. The remaining gap is runtime identity because the local container cannot supply live service or revision values.
Cloud Run supplies that identity through runtime environment variables. Terraform makes the public deployment repeatable from the infrastructure files you already created.
In this step, get ready to:
- Declare a public Cloud Run service in Terraform.
- Expose the service URL through a Terraform output.
- Deploy the dashboard.
Declare the Cloud Run service
The new service resource tells Google Cloud which image to run. It also caps scaling at one instance for this tutorial deployment.
- Click Open Editor in the Cloud Shell toolbar if the editor is hidden.
- Expand the release-dashboard folder in the Explorer sidebar.
- Select the infra folder.
- Create service.tf inside the infra folder using the file control at the top of the Explorer sidebar.
- Define the Cloud Run service by adding this resource to service.tf:
resource "google_cloud_run_v2_service" "dashboard" {
project = var.project_id
name = "release-dashboard"
location = var.region
deletion_protection = false
ingress = "INGRESS_TRAFFIC_ALL"
scaling {
max_instance_count = 1
}
template {
containers {
image = "${google_artifact_registry_repository.dashboard.registry_uri}/release-dashboard:v1"
}
}
depends_on = [google_project_service.cloud_run]
}
What does this resource do?
- The name value creates the release-dashboard service.
- The scaling block caps the service at one instance.
- The template block selects the published v1 image.
- The depends_on value waits for the Cloud Run API.
- Save service.tf.
You should see service.tf listed inside the infra folder.
Can't find service.tf?
Check that the file sits inside release-dashboard/infra. A file created beside the infra folder is outside the Terraform configuration.
Help me place the Cloud Run service file correctly
A public service also needs an IAM grant. The grant lets the allUsers principal invoke this specific service.
- Place your cursor below the closing brace in service.tf.
- Add the public invoker resource using this code:
resource "google_cloud_run_v2_service_iam_member" "public" {
project = var.project_id
location = google_cloud_run_v2_service.dashboard.location
name = google_cloud_run_v2_service.dashboard.name
role = "roles/run.invoker"
member = "allUsers"
}
How does public access work?
- The location value reuses the deployed service region.
- The name value targets the deployed dashboard service.
- The roles/run.invoker role allows requests from allUsers.
- Save service.tf.
You should now see two top-level resource blocks in service.tf.
Is the IAM resource highlighted as invalid?
Check that the IAM resource begins after the service resource's final closing brace. Nested resources are invalid in Terraform.
Help me fix the structure of service.tf
✔️ Awesome, I've got everything!
Your service.tf file now declares the service plus its public invoker grant.
- Save the file before returning to Cloud Shell.
ⓧ I'd like to double check the full code
resource "google_cloud_run_v2_service" "dashboard" {
project = var.project_id
name = "release-dashboard"
location = var.region
deletion_protection = false
ingress = "INGRESS_TRAFFIC_ALL"
scaling {
max_instance_count = 1
}
template {
containers {
image = "${google_artifact_registry_repository.dashboard.registry_uri}/release-dashboard:v1"
}
}
depends_on = [google_project_service.cloud_run]
}
resource "google_cloud_run_v2_service_iam_member" "public" {
project = var.project_id
location = google_cloud_run_v2_service.dashboard.location
name = google_cloud_run_v2_service.dashboard.name
role = "roles/run.invoker"
member = "allUsers"
}
What should you compare?
Check that the service resource comes first. Confirm that the IAM resource begins after its closing brace.
- Switch back to the Cloud Shell terminal from earlier.
- Return to ~/release-dashboard/infra using the same terminal navigation from the registry step.
- Format the new Terraform file by running:
terraform fmt
What does formatting check?
Terraform normalizes the spacing in your configuration. This keeps the new resources consistent with the existing infrastructure files.
The command returns you to the prompt after checking the files. It may print service.tf when it adjusts the file.
- Validate the combined Terraform configuration by running:
terraform validate
What does validation prove?
Validation checks the structure of every Terraform file in infra. It catches invalid references before the deployment reaches Google Cloud.
You should see confirmation that the configuration is valid.
Does validation report an error?
Use the referenced filename plus line number to locate the problem. Check the braces around both resources in service.tf.
Help me resolve my Terraform validation error
Expose the service URL and inspect the plan
A Terraform output makes the deployed address visible after each apply. The service resource exports its public URI for this purpose.
- Switch back to the Cloud Shell Editor from earlier.
- Open infra/outputs.tf from the Explorer sidebar.
- Place your cursor below the existing registry_uri output.
- Add the service URL output using this code:
output "service_url" {
value = google_cloud_run_v2_service.dashboard.uri
}
What does this output expose?
The service_url output reads the URI from the managed Cloud Run service. Terraform prints that value after a successful apply.
- Save outputs.tf.
You should now see one output for the registry plus one output for the service URL.
Is the service reference unresolved?
Confirm that service.tf is saved inside the same infra folder. The resource name must remain google_cloud_run_v2_service.dashboard.
Help me fix the service_url output
✔️ Awesome, I've got everything!
Your outputs.tf file is ready to print the registry URI plus the deployed service URL.
ⓧ I'd like to double check the full code
output "registry_uri" {
value = google_artifact_registry_repository.dashboard.registry_uri
}
output "service_url" {
value = google_cloud_run_v2_service.dashboard.uri
}
What should you compare?
Confirm that both output blocks have their own closing brace. The service_url output should reference the dashboard service URI.
- Switch back to the Cloud Shell terminal in ~/release-dashboard/infra.
- Format the updated output file by running:
terraform fmt
Why format again?
Formatting checks the output block you just added. It also keeps the whole Terraform configuration consistent before planning.
The terminal returns to the prompt when formatting finishes.
Before you inspect the plan, do you expect Terraform to replace the existing registry or add new deployment resources?
- Preview the infrastructure changes by running:
terraform plan
Why inspect the plan?
The plan compares your saved state with the updated configuration. It previews the proposed changes without creating cloud resources.
You should see proposed additions for google_cloud_run_v2_service.dashboard plus google_cloud_run_v2_service_iam_member.public. Your existing registry should remain managed in place.
Does the plan show unexpected changes?
Stop before applying if the plan proposes replacing the repository. Compare registry.tf with the version from the previous step.
Help me review my Terraform plan
Apply the deployment and verify the runtime
Applying this plan creates a public cloud service. Tutorial usage is expected to stay within the current free allowances, while usage beyond those allowances is billable.
Before you apply, which dashboard values do you expect the Cloud Run runtime to supply?
Terraform shows the plan again before it changes anything. Its approval prompt waits for explicit confirmation.
- Start the deployment by running:
terraform apply
What happens during apply?
Terraform creates the Cloud Run service from the published image. It also applies the public invoker grant.
- Review the repeated plan in Cloud Shell.
- Enter yes at the approval prompt.
When the apply completes, Terraform prints the new service_url output. That URL points to your public dashboard.
Did the deployment fail?
Check the first failed resource named in the output. An access policy can prevent the public IAM grant even when the service resource is valid.
Help me troubleshoot my Cloud Run deployment
That is the cloud deployment complete. Your container image now runs as a public service managed entirely through Terraform.
Before you open the URL, which metadata cards do you expect to change from the local placeholder values?
- Open the service_url link printed by Terraform in a new browser tab.
You should see Runtime metadata connected above the metadata cards. The dashboard should report Cloud Run as its runtime.
You should also see version v1 plus service release-dashboard. The revision and configuration cards should contain populated Cloud Run values.
- Compare the live revision value with the Not supplied locally placeholder from your local container.
The generated revision name proves that Cloud Run is serving this release. The configuration card plus port value provide the remaining runtime evidence.
Can't see the live dashboard?
Confirm that you opened the latest service_url printed after the apply. If access is denied, inspect the apply output for a failed public IAM grant.
Help me access my public Cloud Run dashboard
You made the full release path visible. The dashboard now proves which service version and revision are handling public traffic.
Secret mission
Secret Mission: Ship Version 2 as a New Revision
Version 1 is live. Publish a second image to the existing repository. Use Terraform to roll the current service forward as a new Cloud Run revision.
Clean Up Your Resources
Clean Up Your Resources
Cloud Shell is free. Decide whether to keep your Cloud Run service plus Artifact Registry images available, pause traffic, or delete the Terraform-managed resources before usage grows beyond the free allowances.
Cost warning
Artifact Registry storage from 0 to 0.5 GiB-month per billing account is free. Storage beyond that allowance is billable.
Public requests can consume the Cloud Run free allowance. Stored images remain until you delete the repository.
- Choose Pause to stop generating tutorial traffic.
- Choose Delete to remove the service plus its stored images.
Resources you used:
- A public Cloud Run service named release-dashboard.
- An invoker IAM grant for allUsers.
- An Artifact Registry repository named release-dashboard that stores the v1 plus v2 image tags.
- The ~/release-dashboard folder in Cloud Shell, including local Terraform state.
Your Google Cloud project remains available after this cleanup. The enabled APIs also remain because the Terraform configuration preserves them during destruction.
Keep everything running
No action needed. Choose this if you want the public dashboard available for future demonstrations.
- Keep the public service_url available.
- Retain the v1 plus v2 images in the release-dashboard repository.
- Keep ~/release-dashboard/infra so future Terraform plans use the existing state.
Pause - I'll come back to this later
Pause new traffic while keeping the service plus its images ready for later. Cloud Run can scale to zero because the service has no configured minimum instances.
- Stop visiting the public service_url.
- Stop sharing the public service_url.
Artifact Registry continues storing both image tags. That storage can become billable beyond the free allowance.
Delete - I don't want to use this again
Deleting the managed stack is deliberate. Terraform shows every proposed deletion before it asks you to confirm.
- Return to the Cloud Shell terminal from earlier.
- Change to ~/release-dashboard/infra.
- Start Terraform's deletion review by running this command:
terraform destroy
What Does This Command Remove?
Terraform reads the local state before proposing the removal of the Cloud Run service, its invoker IAM grant, plus the Artifact Registry repository.
The disable_on_destroy = false settings preserve the enabled APIs. Your Google Cloud project also remains available.
- Review the proposed deletion plan.
- Enter yes when Terraform asks for confirmation.
When the run finishes, Terraform confirms that the managed resources were removed. Your project remains ready for future Google Cloud learning.
The cloud resources are now gone. Removing the folder clears only your project files plus local Terraform state.
- Remove the local release-dashboard folder by running these commands:
cd ~
rm -rf release-dashboard
What Does This Cleanup Remove?
The first command moves Cloud Shell out of the project folder. The second command permanently deletes ~/release-dashboard plus its local Terraform state.
The release-dashboard folder is no longer present in your Cloud Shell home directory.
Nice Work!
Nice Work!
You did it! Your public release dashboard is serving version v2 on Cloud Run from a versioned Docker image. Terraform keeps its cloud infrastructure reproducible.
You've learned how to:
- Turn the failed static /metadata request into a Flask metadata API that fills the dashboard's runtime cards.
- Publish versioned Docker images to a Terraform-managed Artifact Registry repository.
- Serve a public Cloud Run dashboard that displays live service, revision, configuration, port, and application version metadata.
- Secret Mission: Release version v2 through the existing service. Confirm that Cloud Run serves a new revision without creating a second service.
Ready to quiz yourself?