Learn Fundamental Cloud Skills
Deploy a release dashboard with Docker, GCP, & Terraform
Introduction
30 Second Summary
A release can work perfectly on your laptop and still fall apart online. When your infrastructure lives in code, every deployment is repeatable, reviewable, and easier to maintain.
In this project, you will package a release dashboard with Docker. You will use Terraform to move the image through Google Cloud Artifact Registry into Google Cloud Run.
What You'll Build
Your finished dashboard will display its live release alongside the Cloud Run metadata behind it.
By the end of this project, you'll have:
- A local release dashboard that shows a healthy container with clear fallback metadata.
- A public Cloud Run demo that displays its live service identity.
- A repeatable release workflow that sends a versioned Linux AMD64 image from your Mac to Artifact Registry before Terraform deploys it.
- Secret Mission: Build a v2 image. Deploy it through Terraform. Prove Cloud Run created a new revision.
Are there any prerequisites?
You need a Mac running macOS 15 or newer with permission to install applications.
You also need a billing-enabled Google Cloud project where you can enable APIs plus create resources.
Before We Start
Before the hands-on work begins, take a moment to commit to the Cloud Release Dashboard you are building. Your goal is to show how one container moves from local development into reproducible Google Cloud infrastructure.
Install and Verify the macOS Toolchain
The release dashboard starts on your Mac. Every later command depends on a working local toolchain.
The Homebrew package manager will manage the missing tools. Terraform will define your cloud infrastructure.
The Google Cloud CLI will connect your terminal to Google Cloud. Docker Desktop will build containers through a running Docker engine.
In this step, get ready to:
- Prepare a supported Homebrew shell on macOS 15 or newer.
- Install Terraform 1.16.5, Google Cloud CLI 587.0.0, and Docker Desktop 4.94.0.
- Verify the command-line tools through one terminal session.
Check macOS and install Homebrew
Homebrew supports this project path on macOS 15 or newer. Checking your version first prevents an unsupported installation from blocking the rest of the workflow.
- Click the Apple menu in the upper-left corner of your screen.
- Select About This Mac.
- Confirm that the displayed macOS version is 15 or newer.
The installer may ask for your Mac login password. That authorization allows Homebrew to install the files it needs on your Mac.
- Press Cmd+Space to open Spotlight.
- Type Terminal and press Enter.
- Install Homebrew by running this command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
What does this installer do?
This command downloads Homebrew's official installation script over HTTPS. Bash then runs that script on your Mac.
The installer checks your system before placing Homebrew in its supported location. Its final output tells you whether your shell needs an extra path configuration.
- Follow the installer prompts until it reaches the Next steps section.
- Run the first command printed under Next steps if that section appears.
- Run the second command printed under Next steps if that section appears.
- Confirm that Terminal returns to its normal prompt after the setup completes.
Good progress. Homebrew is now available to install the pinned tools from the same shell.
Homebrew unavailable in Terminal?
Check the final installer output for a Next steps section. Run both shell configuration commands exactly as the installer printed them.
Still stuck? Help me finish my Homebrew installation.
Install the three missing tools
Pinned releases keep the project workflow reproducible. Your commands will match the Terraform plans and container steps used later.
These downloads can take a few minutes. Terminal may pause while Homebrew fetches the larger desktop applications.
- Bring the three pinned tools into your Homebrew-managed environment by running:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
brew install --cask gcloud-cli
brew install --cask docker-desktop
What do these commands install?
- The first command adds HashiCorp's official Homebrew source for Terraform.
- The second command installs Terraform 1.16.5 from that source.
- The third command installs Google Cloud CLI 587.0.0 as a macOS application.
- The fourth command installs Docker Desktop 4.94.0 as a macOS application.
- Confirm that Homebrew completes each installation without reporting a failure.
- Press Cmd+Space to open Spotlight.
- Type Docker Desktop and press Enter.
- Review the subscription agreement if Docker Desktop displays it.
- Accept the agreement to continue.
Docker Desktop's first launch can take a couple of minutes while its engine starts. A short wait here is expected.
- Wait until Docker Desktop reports that its engine is running.
Docker Desktop is now ready to answer commands from Terminal.
Docker Desktop not starting?
Keep Docker Desktop open while its engine initializes. Check for a macOS permission prompt if startup appears paused.
Still stuck? Help me start the Docker Desktop engine.
Prove the toolchain works
Version output proves that your shell resolves the intended tools. Docker's server information also proves that the installed client can reach the running engine.
Before you run these checks, which command do you expect to prove that a background engine is reachable?
- Check the full toolchain from the same Terminal window by running:
terraform version
gcloud version
docker version
What do these checks prove?
- The Terraform output identifies the infrastructure CLI available in your shell.
- The Google Cloud CLI output identifies the cloud command-line release available in your shell.
- The Docker output reports client information from the command-line tool. Its server information confirms that Docker Desktop's engine is running.
✔️ I see the pinned versions
That's the setup hurdle cleared. Your terminal can now reach every tool required for the local-to-cloud workflow.
The Terraform output reports v1.16.5. The Google Cloud CLI output includes 587.0.0.
The Docker output includes both client and server information. That result confirms that Docker Desktop 4.94.0 is installed with a reachable engine.
ⓧ I see an older version
An older result usually means the shell still resolves a previous installation. Reapplying the pinned Homebrew sources gives the current installation another chance to take precedence.
- Reapply the pinned installations by running:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
brew install --cask gcloud-cli
brew install --cask docker-desktop
Why repeat the installation?
These are the same official installation commands used for the project. Homebrew checks the managed packages against its current releases.
- Close Terminal after the commands finish.
- Press Cmd+Space to open Spotlight.
- Type Terminal and press Enter.
- Start Docker Desktop through Spotlight if it is not running.
- Wait until the Docker engine reports that it is running.
- Check the versions again by running:
terraform version
gcloud version
docker version
What should change?
The fresh shell should resolve Terraform v1.16.5 and Google Cloud CLI 587.0.0. Docker should show information for both its client and server.
ⓧ A command is missing
A missing command means its installation did not complete or its path is unavailable in the current shell. Installing the pinned toolset again restores the expected command-line entries.
- Install the pinned toolset by running:
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
brew install --cask gcloud-cli
brew install --cask docker-desktop
What does this repair?
Homebrew installs the missing command-line tools and Docker Desktop application. The shared installation path keeps the project commands available from Terminal.
- Close Terminal after the installations finish.
- Press Cmd+Space to open Spotlight.
- Type Terminal and press Enter.
- Start Docker Desktop through Spotlight.
- Wait until its engine reports that it is running.
- Verify the repaired toolchain by running:
terraform version
gcloud version
docker version
What confirms the repair?
Every command should now print information instead of being unavailable. Docker should print details for its client and server.
Docker shows no server information?
Client information proves that the Docker command is installed. Missing server information means Docker Desktop has not finished starting its engine.
Return to Docker Desktop and wait for its running status. Then run the three verification commands again.
Still stuck? Help me connect the Docker client to Docker Desktop.
Your Mac now has the complete local toolchain for the release dashboard. Next, you'll connect that toolchain to your billing-enabled Google Cloud project.
Connect Your Mac to Google Cloud
Your local toolchain is ready to build containers and manage infrastructure. It still has no authorized path to your Google Cloud project.
The Google Cloud CLI needs an active account plus a target project. Terraform uses Application Default Credentials to authenticate from your workstation.
In this step, get ready to:
- Initialize the Google Cloud CLI with your intended account and billing-enabled project.
- Create Application Default Credentials for Terraform.
- Enable the Service Usage API and verify your active configuration.
Initialize the Google Cloud CLI
The CLI stores your active identity plus your target project in its local configuration. This keeps later commands pointed at the project you intend to use.
The initialization flow opens a browser for sign-in. It also asks you to choose the billing-enabled project for this demo.
- Switch back to the Terminal window from the previous step.
- Start the Google Cloud CLI initialization by running this command:
gcloud init
What does this command configure?
This command authorizes the Google Cloud CLI. It also sets the default configuration that later commands use.
- Complete the browser sign-in with the Google Cloud account you intend to use.
- Allow the requested access if your browser asks for authorization.
- Return to Terminal after the browser confirms the sign-in.
- Select your billing-enabled Google Cloud project when the CLI asks for a project.
That first connection is in place. Your terminal now knows which account and project to use.
Having trouble with initialization?
- Check Terminal for browser sign-in instructions if the browser does not open automatically.
- Confirm that your chosen account has access to the billing-enabled project if the project is missing.
Still stuck? Help me complete Google Cloud CLI initialization on my Mac.
Create credentials for Terraform
Your active CLI account authorizes commands that you run with `gcloud`. Terraform's Google provider reads a separate local credential source.
This authorization stays on your workstation. You do not need to download or paste a service account key into the project.
- Create workstation credentials for Terraform by running this command:
gcloud auth application-default login
What does this command set up?
This command creates Application Default Credentials on your Mac. Terraform's Google provider can discover them automatically during later infrastructure operations.
- Complete the browser sign-in with the same Google Cloud account.
- Approve the requested authorization.
- Return to Terminal after the browser confirms the authorization.
Terminal confirms that the local credentials are available. Terraform now has an authenticated path to Google Cloud.
Credentials not created?
- Confirm that the browser sign-in completed before returning to Terminal.
- Check that you used the same account that has access to your selected project.
Need a hand? Help me diagnose my Application Default Credentials login.
Enable Service Usage and verify the connection
The Service Usage API prepares the project to accept API enablement requests. Terraform later uses this capability when it enables the services needed by the dashboard.
This action changes API availability only. It does not create application infrastructure.
- Enable the Service Usage API in your selected project by running this command:
gcloud services enable serviceusage.googleapis.com
Why enable this API?
The project needs Service Usage before tools can enable other Google Cloud APIs. This clears the way for the Terraform-managed services in later steps.
The command completes without a permissions error when your account can enable APIs in the selected project.
Seeing a permissions problem?
- Confirm that you selected the billing-enabled project during initialization.
- Ask the project owner for permission to enable APIs if your account cannot complete the command.
Need help reading the result? Help me troubleshoot Service Usage API enablement.
Before you check, picture the account and project you expect the commands to report.
- Check the active identity and selected project by running these commands:
gcloud auth list
gcloud config list
What should you check?
- The account list identifies your intended Google Cloud account as active.
- The configuration lists your billing-enabled project under core.project.
Your Mac is now connected to the correct Google Cloud project. The CLI plus Terraform both have the local authentication they need.
Your cloud connection is ready. Next up, you'll package the release dashboard as a local Docker image and see it running in your browser.
Run the Release Dashboard in Docker
Your Mac can now reach the selected Google Cloud project with the credentials Terraform needs. Your running Docker Desktop installation is ready to package the first working version of the dashboard.
A local container gives you an early visual baseline before any cloud infrastructure exists. You can use that baseline to compare the runtime details available locally with the details supplied by Cloud Run later.
In this step, get ready to:
- Create the application file and its container configuration.
- Package the release dashboard as a local Docker image.
- Run the container and inspect its local runtime metadata.
Create the application and container files
The dashboard uses only Python standard-library modules inside the container. The official python:3.14.8-slim-trixie image supplies the runtime, so your Mac does not need a separate Python installation.
You will assemble the three project files in VS Code. Keeping them together lets Docker send the correct files into the build.
- Open Spotlight by pressing Command-Space bar.
- Type VS Code.
- Press Return.
- Click File in the menu bar.
- Select Open Folder….
- Select the empty folder where you want these project files to live.
VS Code now shows the selected folder in the Explorer view. Every file you create below belongs at this top level.
- Select the Explorer view in the Activity Bar.
- Click the New File button.
- Enter app.py.
- Press Return.
- Copy the complete app.py file from the full-code reference below.
- Return to app.py in VS Code.
- Paste the copied code into app.py.
- Save app.py by pressing Command+S.
How does the dashboard work?
- The imports provide an HTTP server, JSON encoding, and environment variable access.
- RELEASE_MESSAGE identifies the release displayed by the dashboard.
- runtime_metadata() reads K_SERVICE, K_REVISION, and K_CONFIGURATION from the runtime environment.
- dashboard_html() turns that metadata into the styled dashboard page.
- DashboardHandler serves the page at /.
- DashboardHandler also serves the health response at /health.
A Docker image needs a recipe that defines its runtime and startup command. The Dockerfile provides that recipe.
- Click the New File button in the Explorer view.
- Enter Dockerfile.
- Press Return.
- Add the container recipe by pasting this code:
FROM python:3.14.8-slim-trixie
WORKDIR /app
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
What does this container recipe do?
- FROM selects the official Python base image that runs the application.
- WORKDIR sets /app as the container's application folder.
- COPY places app.py inside that folder.
- EXPOSE 8080 documents the port used by the web server.
- CMD starts the dashboard when the container launches.
- Save Dockerfile by pressing Command+S.
- Confirm the Explorer view lists app.py and Dockerfile.
Does Dockerfile have an extension?
The filename must be exactly Dockerfile. Remove any extension that VS Code or macOS added.
Still stuck? Help me check why Docker cannot find my Dockerfile.
Docker sends files from the selected project folder into the build context. A .dockerignore file keeps unrelated project data out of that context.
- Click the New File button in the Explorer view.
- Enter .dockerignore.
- Press Return.
- Define the excluded files by pasting this content:
.git
.gitignore
infra
README.md
__pycache__
What stays out of the image build?
The ignore rules exclude Git data, infrastructure files, documentation, and Python cache files. Docker only needs the application file and container recipe for this image.
- Save .dockerignore by pressing Command+S.
- Confirm the Explorer view lists app.py, Dockerfile, and .dockerignore at the same level.
Is .dockerignore missing from Explorer?
Check that the filename starts with a single period. Confirm the file does not have a hidden text extension.
Need another pair of eyes? Help me check my .dockerignore filename and contents.
✔️ Awesome, I've got everything!
Your selected project folder now contains all three saved files. You are ready to build the local image.
ⓧ I'd like to double check the full code
- Compare each saved file with the complete references below.
import http.server
import json
import os
RELEASE_MESSAGE = "Version 1: infrastructure online"
def runtime_metadata():
return {
"service": os.getenv("K_SERVICE", "local-not-set"),
"revision": os.getenv("K_REVISION", "local-not-set"),
"configuration": os.getenv("K_CONFIGURATION", "local-not-set"),
"release_message": RELEASE_MESSAGE,
}
def dashboard_html(metadata):
return f"""<!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, system-ui, sans-serif; }}
body {{ margin: 0; min-height: 100vh; display: grid; place-items: center; background: #07111f; color: #e8f0ff; }}
main {{ width: min(760px, 88vw); padding: 2rem; border: 1px solid #254466; border-radius: 18px; background: #0d1b2d; box-shadow: 0 18px 60px #0008; }}
h1 {{ margin-top: 0; color: #7dd3fc; }}
.status {{ display: inline-block; margin-bottom: 1rem; padding: .35rem .7rem; border-radius: 999px; background: #123f32; color: #86efac; }}
dl {{ display: grid; grid-template-columns: 9rem 1fr; gap: .8rem; }}
dt {{ color: #94a3b8; }}
dd {{ margin: 0; overflow-wrap: anywhere; font-family: ui-monospace, monospace; }}
footer {{ margin-top: 1.5rem; color: #94a3b8; }}
</style>
</head>
<body>
<main>
<span class="status">healthy</span>
<h1>Cloud Release Dashboard</h1>
<p>{metadata['release_message']}</p>
<dl>
<dt>Service</dt><dd>{metadata['service']}</dd>
<dt>Revision</dt><dd>{metadata['revision']}</dd>
<dt>Configuration</dt><dd>{metadata['configuration']}</dd>
</dl>
<footer>Docker image delivered through Artifact Registry and Terraform.</footer>
</main>
</body>
</html>"""
class DashboardHandler(http.server.BaseHTTPRequestHandler):
def send_body(self, status_code, content_type, body):
encoded_body = body.encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(encoded_body)))
self.end_headers()
self.wfile.write(encoded_body)
def do_GET(self):
if self.path == "/health":
self.send_body(
200,
"application/json",
json.dumps({"status": "healthy"}),
)
return
if self.path == "/":
self.send_body(
200,
"text/html; charset=utf-8",
dashboard_html(runtime_metadata()),
)
return
self.send_error(404, "Not Found")
if __name__ == "__main__":
port = int(os.getenv("PORT", "8080"))
server = http.server.ThreadingHTTPServer(("0.0.0.0", port), DashboardHandler)
print(f"Listening on 0.0.0.0:{port}", flush=True)
server.serve_forever()
FROM python:3.14.8-slim-trixie
WORKDIR /app
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
.git
.gitignore
infra
README.md
__pycache__
Build the local image
The source files describe the dashboard, but Docker still needs to turn them into a reusable image. Building the image packages the Python runtime and your application under one local tag.
- Click Terminal in the VS Code menu bar.
- Select New Terminal.
The integrated terminal starts in the folder currently open in VS Code. This is the folder that contains your three project files.
- Build the local dashboard image by running this command:
docker build --tag release-dashboard:local .
What does this build command do?
- docker build reads the container recipe from the current folder.
- --tag release-dashboard:local gives the finished image a local name and tag.
- . sends the current folder as the build context.
- Wait for the image build to finish.
The final build output should report a successful image build with the release-dashboard:local tag. The first build may pause while Docker downloads the Python base image layers.
Good progress. Your dashboard is now packaged into the same image format that you will publish to Google Cloud.
Did the image build fail?
- Confirm the terminal is using the same folder shown in the VS Code Explorer view.
- Check that the filename is exactly Dockerfile.
- Confirm app.py sits beside Dockerfile.
Still blocked? Help me diagnose why my release-dashboard Docker image does not build.
Run and inspect the local container
A container is a running instance of the image you just built. Publishing port 8080 lets your browser reach the dashboard through your Mac's loopback address.
Before you run the container, what runtime details do you think a local Mac can supply to this dashboard?
- Start the local dashboard container by running this command:
docker run --detach --name release-dashboard-local --publish 127.0.0.1:8080:8080 release-dashboard:local
What does this run command do?
- --detach keeps the container running in the background.
- --name release-dashboard-local assigns a reusable name to the container.
- --publish 127.0.0.1:8080:8080 maps your Mac's local port to the container's port.
- release-dashboard:local selects the image you built in the previous substep.
- Open http://localhost:8080 in your browser.
You should see the dark Cloud Release Dashboard with a healthy badge. The page should display Version 1: infrastructure online.
The service, revision, and configuration values each display local-not-set. This intended shortfall proves that the container works while cloud runtime metadata is absent.
- Open http://localhost:8080/health in your browser.
You should see {"status": "healthy"} as the health response. This confirms that the same server handles both the dashboard and its health endpoint.
That is your first complete release check. The local container serves the dashboard and exposes a working health endpoint from one image.
The detached container continues using local resources after you close the browser tab. Stop it now so the project ends this test with a clean local runtime.
- Stop the local dashboard container by running this command:
docker stop release-dashboard-local
What does this stop command do?
The command stops the running container named release-dashboard-local. The reusable release-dashboard:local image remains available on your Mac.
The terminal should return the container name after it stops. The local dashboard URL should stop responding once the container is no longer running.
Could Docker not stop the container?
Check that the container name in your command matches release-dashboard-local exactly. A different name points Docker at a container that does not exist.
Need help checking the local state? Help me find out why Docker cannot stop release-dashboard-local.
Your local baseline is complete. Next, you will use Terraform to provision Artifact Registry and publish a cloud-compatible version of this image.
Provision Artifact Registry and Publish the Image
Your release dashboard already runs inside a Docker container on your Mac. The image currently lives only on that machine.
Cloud Run needs a private image location before it can deploy the dashboard. Terraform will create Artifact Registry. Docker will publish a Linux AMD64 image to it.
In this step, get ready to:
- Create a separate Terraform root for the image repository.
- Provision the repository from a reviewed Terraform plan.
- Publish the Linux AMD64 v1 image to Artifact Registry.
Create the registry Terraform root
A Terraform root is a folder with its own configuration files and state. This root owns the APIs plus the repository that must exist before you publish an image.
- Switch back to the project in VS Code from earlier.
- Create the folder path infra/registry inside the project using the Explorer sidebar.
Terraform creates local state plus downloaded provider files. A project-level .gitignore keeps those generated files out of version control.
- Create .gitignore beside app.py by pasting this content:
.DS_Store
**/.terraform/*
*.tfstate
*.tfstate.*
**/tfplan
What does this file protect?
- The .terraform pattern excludes downloaded provider files.
- The state patterns exclude Terraform's local infrastructure records.
- The tfplan pattern excludes the saved plans you will create.
- Save .gitignore.
You should see .gitignore beside the dashboard files in the Explorer sidebar.
The first Terraform file pins the CLI to 1.16.5. It also pins the Google provider to 8.5.0 so future releases cannot silently change this root.
- Create infra/registry/versions.tf by pasting this configuration:
terraform {
required_version = "= 1.16.5"
required_providers {
google = {
source = "hashicorp/google"
version = "= 8.5.0"
}
}
}
What does this version file do?
- The Terraform constraint requires the same CLI version you verified earlier.
- The hashicorp/google declaration tells Terraform which provider manages Google Cloud resources.
- The exact provider constraint keeps planning behavior consistent.
- Save infra/registry/versions.tf.
You should see versions.tf inside the infra/registry folder.
Input variables keep the project ID specific to your account. The region stays reusable across the registry and the Cloud Run service.
- Create infra/registry/variables.tf by pasting these variable declarations:
variable "project_id" {
description = "The Google Cloud project ID used for the portfolio deployment."
type = string
}
variable "region" {
description = "The region shared by Artifact Registry and Cloud Run."
type = string
default = "us-central1"
}
What do these variables control?
- The project_id variable selects the billing-enabled Google Cloud project.
- The region variable keeps the repository in us-central1 by default.
- Save infra/registry/variables.tf.
You should now see both versions.tf and variables.tf in the registry folder.
The provider connects every resource in this root to one project. It also applies the selected region to regional resources.
- Create infra/registry/providers.tf by pasting this provider configuration:
provider "google" {
project = var.project_id
region = var.region
}
How does Terraform connect?
- The provider reads project_id from this root's variable values.
- The provider uses the Application Default Credentials you created earlier.
- Save infra/registry/providers.tf.
The Explorer sidebar should now list three Terraform configuration files inside infra/registry.
The main resource graph enables the Artifact Registry API plus the Cloud Run API. It then creates the Docker repository after Artifact Registry becomes available.
- Create infra/registry/main.tf by pasting these resources:
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" "app" {
project = var.project_id
location = var.region
repository_id = "release-dashboard"
description = "Docker images for the cloud release dashboard"
format = "DOCKER"
depends_on = [google_project_service.artifact_registry]
}
What does this resource graph create?
- The two google_project_service resources enable the APIs needed by this deployment.
- The disable_on_destroy setting leaves those APIs enabled during cleanup.
- The repository resource creates a private Docker repository named release-dashboard.
- The dependency waits for the Artifact Registry API before creating the repository.
- Save infra/registry/main.tf.
You should see main.tf alongside the three supporting Terraform files.
Outputs expose stable repository values after Terraform applies the root. The image output adds the immutable v1 tag that Docker will publish.
- Create infra/registry/outputs.tf by pasting these outputs:
output "repository_uri" {
description = "Artifact Registry repository URI."
value = google_artifact_registry_repository.app.registry_uri
}
output "image_uri" {
description = "Version 1 image URI."
value = "${google_artifact_registry_repository.app.registry_uri}/release-dashboard:v1"
}
Why expose two URIs?
- The repository_uri output identifies the repository without a specific image.
- The image_uri output identifies the exact release image that Cloud Run will deploy.
- Save infra/registry/outputs.tf.
The registry folder should now contain five .tf files.
The final file supplies concrete values for this root. Your selected project ID directs every planned resource to the correct billing-enabled project.
- Create infra/registry/terraform.tfvars by pasting this template:
project_id = "your-gcp-project-id"
region = "us-central1"
What does this values file do?
- The project_id value selects the project where Terraform creates the repository.
- The region places the repository near the Cloud Run service you will deploy next.
- In infra/registry/terraform.tfvars, replace your-gcp-project-id with your GCP project ID.
- Save infra/registry/terraform.tfvars.
Your registry root now targets the same project selected by the Google Cloud CLI.
Seeing a missing or misplaced file?
- Check that .gitignore sits beside app.py in the project root.
- Check that all six registry files sit inside infra/registry.
- Check every filename for the exact .tf or .tfvars ending shown above.
Still stuck? Help me check the file layout for my registry Terraform root.
✔️ Awesome, I've got everything!
- Confirm that every new file is saved in VS Code.
- Confirm that your saved terraform.tfvars contains your real Google Cloud project ID.
ⓧ I'd like to double check the full code
The reference copy of terraform.tfvars preserves the project template. Your saved copy contains your real project ID.
- Compare each project file with the cumulative reference below.
.git
.gitignore
infra
README.md
__pycache__
.DS_Store
**/.terraform/*
*.tfstate
*.tfstate.*
**/tfplan
FROM python:3.14.8-slim-trixie
WORKDIR /app
COPY app.py .
EXPOSE 8080
CMD ["python", "app.py"]
import http.server
import json
import os
RELEASE_MESSAGE = "Version 1: infrastructure online"
def runtime_metadata():
return {
"service": os.getenv("K_SERVICE", "local-not-set"),
"revision": os.getenv("K_REVISION", "local-not-set"),
"configuration": os.getenv("K_CONFIGURATION", "local-not-set"),
"release_message": RELEASE_MESSAGE,
}
def dashboard_html(metadata):
return f"""<!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, system-ui, sans-serif; }}
body {{ margin: 0; min-height: 100vh; display: grid; place-items: center; background: #07111f; color: #e8f0ff; }}
main {{ width: min(760px, 88vw); padding: 2rem; border: 1px solid #254466; border-radius: 18px; background: #0d1b2d; box-shadow: 0 18px 60px #0008; }}
h1 {{ margin-top: 0; color: #7dd3fc; }}
.status {{ display: inline-block; margin-bottom: 1rem; padding: .35rem .7rem; border-radius: 999px; background: #123f32; color: #86efac; }}
dl {{ display: grid; grid-template-columns: 9rem 1fr; gap: .8rem; }}
dt {{ color: #94a3b8; }}
dd {{ margin: 0; overflow-wrap: anywhere; font-family: ui-monospace, monospace; }}
footer {{ margin-top: 1.5rem; color: #94a3b8; }}
</style>
</head>
<body>
<main>
<span class="status">healthy</span>
<h1>Cloud Release Dashboard</h1>
<p>{metadata['release_message']}</p>
<dl>
<dt>Service</dt><dd>{metadata['service']}</dd>
<dt>Revision</dt><dd>{metadata['revision']}</dd>
<dt>Configuration</dt><dd>{metadata['configuration']}</dd>
</dl>
<footer>Docker image delivered through Artifact Registry and Terraform.</footer>
</main>
</body>
</html>"""
class DashboardHandler(http.server.BaseHTTPRequestHandler):
def send_body(self, status_code, content_type, body):
encoded_body = body.encode("utf-8")
self.send_response(status_code)
self.send_header("Content-Type", content_type)
self.send_header("Content-Length", str(len(encoded_body)))
self.end_headers()
self.wfile.write(encoded_body)
def do_GET(self):
if self.path == "/health":
self.send_body(
200,
"application/json",
json.dumps({"status": "healthy"}),
)
return
if self.path == "/":
self.send_body(
200,
"text/html; charset=utf-8",
dashboard_html(runtime_metadata()),
)
return
self.send_error(404, "Not Found")
if __name__ == "__main__":
port = int(os.getenv("PORT", "8080"))
server = http.server.ThreadingHTTPServer(("0.0.0.0", port), DashboardHandler)
print(f"Listening on 0.0.0.0:{port}", flush=True)
server.serve_forever()
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" "app" {
project = var.project_id
location = var.region
repository_id = "release-dashboard"
description = "Docker images for the cloud release dashboard"
format = "DOCKER"
depends_on = [google_project_service.artifact_registry]
}
output "repository_uri" {
description = "Artifact Registry repository URI."
value = google_artifact_registry_repository.app.registry_uri
}
output "image_uri" {
description = "Version 1 image URI."
value = "${google_artifact_registry_repository.app.registry_uri}/release-dashboard:v1"
}
provider "google" {
project = var.project_id
region = var.region
}
project_id = "your-gcp-project-id"
region = "us-central1"
variable "project_id" {
description = "The Google Cloud project ID used for the portfolio deployment."
type = string
}
variable "region" {
description = "The region shared by Artifact Registry and Cloud Run."
type = string
default = "us-central1"
}
terraform {
required_version = "= 1.16.5"
required_providers {
google = {
source = "hashicorp/google"
version = "= 8.5.0"
}
}
}
Review and apply the registry plan
Terraform turns the configuration into a reviewable plan before it changes Google Cloud. This gives you a clear checkpoint for the APIs plus the repository.
- Prepare the registry root for planning by running these commands in the project terminal:
terraform -chdir=infra/registry init
terraform -chdir=infra/registry fmt
terraform -chdir=infra/registry validate
What do these commands check?
- The initialization command downloads the pinned Google provider for this root.
- The formatting command applies Terraform's canonical layout to the configuration.
- The validation command checks the root for syntax plus internal consistency.
You should see provider installation output followed by confirmation that the configuration is valid.
Initialization or validation failed?
- Check that the terminal is running from the folder containing app.py.
- Check that all six Terraform files are inside infra/registry.
- Check each file against the full-code reference for missing braces or quotation marks.
Need a second pair of eyes? Help me diagnose my Terraform registry initialization or validation failure.
Before you create the plan, how many Google Cloud resources do you expect Terraform to propose?
- Create a saved registry plan by running this command:
terraform -chdir=infra/registry plan -out=tfplan
Why save the plan?
The command evaluates the configuration against the selected project. It saves the reviewed result as tfplan for the apply command.
The plan should propose two API service resources plus one Artifact Registry repository. Review the project ID and region before you continue.
Does the plan target the wrong project?
- Return to infra/registry/terraform.tfvars.
- Replace the project value with the ID of your selected billing-enabled project.
- Save the file before creating the plan again.
Still unsure? Help me verify which Google Cloud project this Terraform plan targets.
Applying this plan creates real resources in your billing-enabled project. This tutorial image is expected to stay within the published free storage allowance.
The API resources can take a few minutes to become available. A short pause during this apply is expected.
- Apply the reviewed saved plan by running this command:
terraform -chdir=infra/registry apply tfplan
What happens during apply?
- Terraform enables the Artifact Registry API.
- Terraform enables the Cloud Run API.
- Terraform creates the private release-dashboard Docker repository in us-central1.
That is the infrastructure layer complete. Terraform now manages the repository that stores your release images.
Apply blocked by project permissions?
- Confirm that the active account can enable services in the selected Google Cloud project.
- Confirm that the active account can create Artifact Registry repositories.
- Ask the project owner to grant the required project permissions if either action is blocked.
Need help interpreting the failure? Help me understand which Google Cloud permission blocked my Terraform apply.
Authenticate Docker and publish the image
The empty repository is ready for a container image. Docker first needs permission to send images to the regional Artifact Registry hostname.
- Configure Docker authentication for the repository hostname by running this command:
gcloud auth configure-docker us-central1-docker.pkg.dev
What does this authentication command do?
The Google Cloud CLI configures Docker to use its credential helper for us-central1-docker.pkg.dev. Docker can then request short-lived credentials when it pushes the image.
You should see confirmation that Docker's configuration was updated for the regional hostname.
Docker authentication failed?
- Confirm that the Google Cloud CLI still uses the account from the previous step.
- Confirm that the active project matches the project in terraform.tfvars.
- Confirm that Docker Desktop is still running.
Still blocked? Help me diagnose my Artifact Registry Docker authentication failure.
Cloud Run requires a Linux AMD64 image or a compatible multi-architecture image. The explicit platform target makes this build portable across Apple silicon Macs plus Intel Macs.
The first cloud build can take a few minutes while Docker prepares the builder and uploads image layers. The terminal may stay busy during that work.
- Build the v1 image for Linux AMD64 by running these commands:
IMAGE_URI="$(terraform -chdir=infra/registry output -raw image_uri)"
docker buildx build --platform linux/amd64 --tag "$IMAGE_URI" --push .
How does this publish the image?
- The first command reads the exact image_uri value from Terraform state.
- The --platform option builds the Linux AMD64 format required by Cloud Run.
- The tag points at the private repository with the immutable v1 release tag.
- The push option sends the completed image directly to Artifact Registry.
You should see the image layers complete followed by a successful registry push.
Image build or push failed?
- Confirm that Docker Desktop reports a running engine.
- Confirm that the Terraform apply completed before this build.
- Confirm that your active account can upload artifacts to the repository.
Need help with the output? Help me diagnose my Linux AMD64 image build or Artifact Registry push failure.
Before you print the Terraform output, do you expect it to match the image destination shown by Docker?
- Print the published image URI by running this command:
terraform -chdir=infra/registry output -raw image_uri
What does this output prove?
Terraform reads the image location derived from the repository it created. Matching that location with Docker's pushed tag proves the infrastructure output plus the published artifact use the same address.
You should see a URI beginning with us-central1-docker.pkg.dev and ending with /release-dashboard/release-dashboard:v1. It should match the image name shown during the push.
Your first cloud release artifact is published. The same dashboard image that ran on your Mac is now ready for Cloud Run.
Your registry layer now has a published v1 image. Next, you will deploy that image to Cloud Run and replace the local fallback metadata with real revision details.
Deploy the Dashboard to Cloud Run
Your version 1 image is stored in Artifact Registry. The local dashboard still shows local-not-set because it has no cloud runtime metadata.
In this step, you'll use Terraform to deploy the same image to Cloud Run. Cloud Run supplies the service details that were missing from the local container.
In this step, get ready to:
- Create a separate Terraform root for the Cloud Run service.
- Review the infrastructure plan before deploying the public service.
- Test the dashboard metadata, health response, and service logs.
Create the service root and README
The registry and service have different lifecycle boundaries. A separate service root lets you remove the live deployment while keeping the published image available.
Why use separate Terraform roots?
The registry must exist before Docker can publish the image that Cloud Run consumes. Separate roots make that dependency visible without relying on routine resource targeting.
You can destroy the service independently when you want to pause the public demo. The registry remains available for a later deployment.
- Return to the Explorer sidebar in VS Code.
- Expand the existing infra folder.
- Create a folder named service inside infra.
You should now see infra/service beside the existing infra/registry folder.
- Create versions.tf inside infra/service with VS Code's new file control.
- Paste this version configuration into versions.tf:
terraform {
required_version = "= 1.16.5"
required_providers {
google = {
source = "hashicorp/google"
version = "= 8.5.0"
}
}
}
What does this version configuration do?
- The Terraform constraint keeps this root on version 1.16.5.
- The provider block pins hashicorp/google to version 8.5.0.
- Save infra/service/versions.tf.
- Confirm the Explorer sidebar lists versions.tf under infra/service.
- Create providers.tf inside infra/service.
- Paste this provider configuration into providers.tf:
provider "google" {
project = var.project_id
region = var.region
}
What does this provider configuration do?
The Google provider uses your project and region variables. It also uses the Application Default Credentials you created earlier.
- Save infra/service/providers.tf.
- Confirm providers.tf appears beneath versions.tf in the Explorer sidebar.
- Create variables.tf inside infra/service.
- Paste these input variable definitions into variables.tf:
variable "project_id" {
description = "The Google Cloud project ID used for the portfolio deployment."
type = string
}
variable "region" {
description = "The region shared by Artifact Registry and Cloud Run."
type = string
default = "us-central1"
}
variable "image_tag" {
description = "The immutable image tag deployed to Cloud Run."
type = string
default = "v1"
}
What do these variables control?
- The project_id variable selects the billing-enabled Google Cloud project.
- The region variable keeps Cloud Run in us-central1 with the image repository.
- The image_tag variable selects the immutable release tag that Cloud Run deploys.
- Save infra/service/variables.tf.
- Confirm the file contains definitions for project_id, region, and image_tag.
- Create terraform.tfvars inside infra/service.
- Paste these service values into terraform.tfvars:
project_id = "your-gcp-project-id"
region = "us-central1"
image_tag = "v1"
What do these values select?
This root deploys the v1 image to us-central1. The project value must match the project that contains your Artifact Registry repository.
- Replace your-gcp-project-id with the same project ID already used in infra/registry/terraform.tfvars.
- Save infra/service/terraform.tfvars.
- Confirm the file still sets region to us-central1.
- Create main.tf inside infra/service.
- Paste this Cloud Run service definition into main.tf:
locals {
image_uri = "${var.region}-docker.pkg.dev/${var.project_id}/release-dashboard/release-dashboard:${var.image_tag}"
}
resource "google_cloud_run_v2_service" "app" {
project = var.project_id
name = "release-dashboard"
location = var.region
deletion_protection = false
ingress = "INGRESS_TRAFFIC_ALL"
invoker_iam_disabled = true
template {
scaling {
min_instance_count = 0
max_instance_count = 1
}
containers {
image = local.image_uri
ports {
container_port = 8080
}
liveness_probe {
http_get {
path = "/health"
}
}
}
}
}
What does the service configuration do?
- The local value builds the versioned Artifact Registry image address from your project settings.
- The service accepts public traffic because the Invoker IAM check is disabled.
- The scaling block allows the service to reach zero instances during idle periods. It limits automatic scaling to one instance.
- The container listens on port 8080. Cloud Run checks /health to confirm the container remains responsive.
- Save infra/service/main.tf.
- Confirm main.tf contains one google_cloud_run_v2_service resource named app.
Seeing mismatched brackets in main.tf?
Check that the liveness_probe block sits inside containers after the ports block. Each nested block needs its own closing brace.
Still stuck? Help me compare my Cloud Run Terraform resource with the expected main.tf structure.
- Create outputs.tf inside infra/service.
- Paste these output definitions into outputs.tf:
output "service_url" {
description = "Public HTTPS URL for the release dashboard."
value = google_cloud_run_v2_service.app.uri
}
output "latest_revision" {
description = "Latest Cloud Run revision serving traffic."
value = google_cloud_run_v2_service.app.latest_ready_revision
}
output "image_uri" {
description = "Container image deployed to Cloud Run."
value = local.image_uri
}
What do these outputs expose?
- The service_url output provides the stable public HTTPS address.
- The latest_revision output identifies the revision currently ready to serve traffic.
- The image_uri output records the exact container image selected by the service.
- Save infra/service/outputs.tf.
- Confirm the Explorer sidebar lists all six Terraform files under infra/service.
The infrastructure now has a reproducible definition. The README records the same workflow so another person can understand the architecture and repeat the deployment.
- Create README.md in the project root with VS Code's new file control.
- Paste this project overview and local demo section into README.md:
# Cloud Release Dashboard
A portfolio project that packages one release dashboard with Docker, stores the image in Google Cloud Artifact Registry, and deploys it to Cloud Run with Terraform.
## Architecture
Browser -> Cloud Run service -> Artifact Registry image
Terraform registry root -> Google Cloud APIs + Artifact Registry
Terraform service root -> Cloud Run service + scaling + health configuration
## Local demo
```bash
docker build --tag release-dashboard:local .
docker run --rm --publish 127.0.0.1:8080:8080 release-dashboard:local
```
Open `http://localhost:8080` and `http://localhost:8080/health`.
What does this README section explain?
The opening describes the finished project in one sentence. The architecture section shows how the browser, Cloud Run service, registry image, and Terraform roots relate.
The local demo records how to build and run the same container before deploying it.
- Save README.md.
- Confirm the editor shows the Architecture and Local demo headings.
- Append this deployment section below the local demo in README.md:
## Deploy
Replace `your-gcp-project-id` in both `terraform.tfvars` files, then run:
```bash
terraform -chdir=infra/registry init
terraform -chdir=infra/registry plan -out=tfplan
terraform -chdir=infra/registry apply tfplan
gcloud auth configure-docker us-central1-docker.pkg.dev
IMAGE_URI="$(terraform -chdir=infra/registry output -raw image_uri)"
docker buildx build --platform linux/amd64 --tag "$IMAGE_URI" --push .
terraform -chdir=infra/service init
terraform -chdir=infra/service plan -out=tfplan
terraform -chdir=infra/service apply tfplan
terraform -chdir=infra/service output -raw service_url
```
What does the deployment section capture?
This section records the registry-before-service order. It also preserves the Linux AMD64 build command and the commands used to deploy the service.
- Save README.md.
- Confirm the deployment section lists the registry workflow before the service workflow.
- Append these verification and cleanup sections at the bottom of README.md:
## Verify
Open the service URL and its `/health` path. Then read recent logs:
```bash
gcloud run services logs read release-dashboard --region=us-central1 --limit=10
```
## Clean up
Destroy the consumer before the image repository:
```bash
terraform -chdir=infra/service destroy
terraform -chdir=infra/registry destroy
```
Why document verification and cleanup?
The verification section gives a reviewer a concrete way to test the deployment. The cleanup section preserves the dependency order by removing the service before the image repository.
- Save README.md.
- Confirm the final headings are Verify and Clean up.
✔️ Awesome, I've got everything!
Your service root and README are ready. Double check that every new file is saved before running Terraform.
ⓧ I'd like to double check the full code
Compare each new file with the reference below. Keep your real project ID in infra/service/terraform.tfvars where the reference shows your-gcp-project-id.
terraform {
required_version = "= 1.16.5"
required_providers {
google = {
source = "hashicorp/google"
version = "= 8.5.0"
}
}
}
Versions File Check
This file pins Terraform 1.16.5 and the Google provider 8.5.0.
provider "google" {
project = var.project_id
region = var.region
}
Provider File Check
This file passes the selected project and region into the Google provider.
variable "project_id" {
description = "The Google Cloud project ID used for the portfolio deployment."
type = string
}
variable "region" {
description = "The region shared by Artifact Registry and Cloud Run."
type = string
default = "us-central1"
}
variable "image_tag" {
description = "The immutable image tag deployed to Cloud Run."
type = string
default = "v1"
}
Variables File Check
This file defines the project, region, and image tag inputs used by the service.
project_id = "your-gcp-project-id"
region = "us-central1"
image_tag = "v1"
Values File Check
This reference selects us-central1 and the v1 image tag. Your local copy must contain your real project ID.
locals {
image_uri = "${var.region}-docker.pkg.dev/${var.project_id}/release-dashboard/release-dashboard:${var.image_tag}"
}
resource "google_cloud_run_v2_service" "app" {
project = var.project_id
name = "release-dashboard"
location = var.region
deletion_protection = false
ingress = "INGRESS_TRAFFIC_ALL"
invoker_iam_disabled = true
template {
scaling {
min_instance_count = 0
max_instance_count = 1
}
containers {
image = local.image_uri
ports {
container_port = 8080
}
liveness_probe {
http_get {
path = "/health"
}
}
}
}
}
Service File Check
This file defines the public Cloud Run service, scaling limits, container port, and liveness probe.
output "service_url" {
description = "Public HTTPS URL for the release dashboard."
value = google_cloud_run_v2_service.app.uri
}
output "latest_revision" {
description = "Latest Cloud Run revision serving traffic."
value = google_cloud_run_v2_service.app.latest_ready_revision
}
output "image_uri" {
description = "Container image deployed to Cloud Run."
value = local.image_uri
}
Outputs File Check
This file exposes the service URL, latest ready revision, and deployed image address.
# Cloud Release Dashboard
A portfolio project that packages one release dashboard with Docker, stores the image in Google Cloud Artifact Registry, and deploys it to Cloud Run with Terraform.
## Architecture
Browser -> Cloud Run service -> Artifact Registry image
Terraform registry root -> Google Cloud APIs + Artifact Registry
Terraform service root -> Cloud Run service + scaling + health configuration
## Local demo
```bash
docker build --tag release-dashboard:local .
docker run --rm --publish 127.0.0.1:8080:8080 release-dashboard:local
```
Open `http://localhost:8080` and `http://localhost:8080/health`.
## Deploy
Replace `your-gcp-project-id` in both `terraform.tfvars` files, then run:
```bash
terraform -chdir=infra/registry init
terraform -chdir=infra/registry plan -out=tfplan
terraform -chdir=infra/registry apply tfplan
gcloud auth configure-docker us-central1-docker.pkg.dev
IMAGE_URI="$(terraform -chdir=infra/registry output -raw image_uri)"
docker buildx build --platform linux/amd64 --tag "$IMAGE_URI" --push .
terraform -chdir=infra/service init
terraform -chdir=infra/service plan -out=tfplan
terraform -chdir=infra/service apply tfplan
terraform -chdir=infra/service output -raw service_url
```
## Verify
Open the service URL and its `/health` path. Then read recent logs:
```bash
gcloud run services logs read release-dashboard --region=us-central1 --limit=10
```
## Clean up
Destroy the consumer before the image repository:
```bash
terraform -chdir=infra/service destroy
terraform -chdir=infra/registry destroy
```
README File Check
This file documents the architecture, local demo, deployment workflow, verification steps, and cleanup order.
Review and apply the service plan
A Terraform plan shows the exact infrastructure change before it reaches Google Cloud. This checkpoint gives you a chance to confirm that the deployment creates only the intended Cloud Run service.
- Initialize the service root, format its files, validate the configuration, and save the proposed plan by running:
terraform -chdir=infra/service init
terraform -chdir=infra/service fmt
terraform -chdir=infra/service validate
terraform -chdir=infra/service plan -out=tfplan
What do these Terraform commands do?
- The initialization command installs the pinned Google provider for this separate root.
- The formatting command applies Terraform's canonical layout to the service files.
- The validation command checks syntax and internal references.
- The planning command writes the reviewed proposal to tfplan.
- Review the plan summary in the terminal.
- Confirm the plan creates one google_cloud_run_v2_service.app resource.
- Confirm the image address ends with release-dashboard:v1.
- Confirm the plan shows a minimum instance count of 0.
- Confirm the plan shows a maximum instance count of 1.
Terraform Cannot Validate the Service?
Check that all six files are inside infra/service. A file placed directly inside infra belongs to a different Terraform root.
Check that your real project ID replaced your-gcp-project-id in the service variable values file.
Still stuck? Help me diagnose my Terraform initialization or validation failure.
📣 Public Service and Billing
Applying this plan creates a public endpoint in your billing-enabled project. Tutorial-scale use should remain within the published monthly free allowances.
Public traffic above those allowances can create charges. The service can scale to zero during idle periods.
Before you apply the saved plan, make a quick mental prediction about the resource Terraform will create.
- Apply the reviewed plan by running:
terraform -chdir=infra/service apply tfplan
What does this apply command do?
Terraform executes the exact saved plan you reviewed. It creates the Cloud Run service without generating a different plan at apply time.
The deployment can take a few minutes while Cloud Run creates the service and starts the first revision.
You should see Terraform report a successful apply with one resource created. The output section includes a public service URL and the latest ready revision.
That's the deployment complete. Your version 1 container is now serving requests from Google Cloud.
Cloud Run Service Creation Failed?
Confirm the active project contains the release-dashboard repository and the published v1 image.
Confirm your account can create Cloud Run resources in the selected project. Missing project permissions can stop the apply before the service is created.
Still stuck? Help me diagnose why Terraform could not create my Cloud Run service.
Demo the service and its logs
The stable service URL lets you test the exact container that Cloud Run is serving. Its metadata should now identify the service, revision, and configuration.
- Print the public dashboard URL by running:
terraform -chdir=infra/service output -raw service_url
What does this output command do?
The raw output command prints only the public HTTPS URL. This makes the value easy to copy into your browser.
- Copy the printed URL here: your Cloud Run service URL.
Before you load the page, make a quick mental prediction about whether the metadata will still show local-not-set.
- Open a new browser tab.
- Paste your Cloud Run service URL into the address bar.
- Load the dashboard.
You should see Cloud Release Dashboard with Version 1: infrastructure online. The service, revision, and configuration fields now contain Cloud Run-provided values.
The move from local-not-set to real runtime values proves that the same container is now running inside Cloud Run.
Before you test the health route, make a quick mental prediction about the response the liveness probe expects.
- Enter your Cloud Run service URL followed by /health in the browser address bar.
- Load the health endpoint.
You should see {"status": "healthy"}. This is the same route used by the configured liveness probe.
Before you read the logs, make a quick mental prediction about what the dashboard and health requests recorded.
- Read the ten most recent service log entries by running:
gcloud run services logs read release-dashboard --region=us-central1 --limit=10
What does this log command show?
The command reads recent logs for the release-dashboard service in us-central1. The limit keeps the result focused on the requests you just made.
You should see recent entries from the running service. These entries confirm that requests reached the deployed Cloud Run revision.
Dashboard or Logs Missing?
If the browser cannot load the service, confirm you copied the complete HTTPS URL from the Terraform output. Check that the apply completed successfully.
If the logs are empty, refresh the dashboard and health endpoint before running the log command again.
Still stuck? Help me debug my deployed Cloud Run dashboard and recent service logs.
Your public release dashboard now proves the complete container delivery path. The optional Secret Mission gives you a chance to push that workflow further.
Secret mission
Ship a Version 2 Revision
Publish a Linux AMD64 version 2 image to Artifact Registry. Update the Terraform image tag. Prove Cloud Run created a new revision while the public service stayed healthy.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep your Google Cloud resources running, pause the public demo, or delete the managed infrastructure. Billing remains enabled for the selected project after this project.
Cost warning
Cloud Run includes the first 2 million requests per month in its request-based free tier.
Artifact Registry storage from 0 to 0.5 GiB-month is free per billing account.
Unexpected public traffic or data transfer outside free conditions can create charges.
Enabling the Container Scanning API adds billed vulnerability scanning where supported.
Resources you used:
- Public Cloud Run service release-dashboard with its revisions in us-central1.
- Artifact Registry Docker repository release-dashboard with its stored v1 image and v2 image.
- Local Terraform state, provider cache, and saved plan data under infra/service and infra/registry.
Keep everything running
No action needed. Choose this option if you want the public dashboard to remain available for demonstrations.
- Leave the infra/service Terraform root applied.
- Leave the infra/registry Terraform root applied.
- Limit where you share the public service URL to reduce unexpected traffic.
Cloud Run can scale to zero when idle. Your stored images remain in Artifact Registry.
Pause - I'll come back to this later
Shut down the public demo while keeping its container images for a later redeployment.
Removing the service is a deliberate destructive action. The Artifact Registry repository stays intact for when you return.
Terraform presents a destruction plan before asking for confirmation.
- Switch back to the terminal you used for Terraform in the folder that contains app.py.
- Remove the Cloud Run service by running this command:
terraform -chdir=infra/service destroy
What does this command remove?
Terraform reads the local state in infra/service and deprovisions the managed Cloud Run service.
The Artifact Registry repository remains available for a later deployment.
- Review the destruction plan.
- Enter the requested confirmation to approve the destroy.
- Wait for the terminal to confirm that the managed service was destroyed.
- Refresh the public service URL from earlier.
The dashboard should no longer load from its former Cloud Run URL.
Terraform cannot destroy the service?
- Confirm that infra/service/terraform.tfstate still exists.
- Confirm that the active Google Cloud project matches the project in infra/service/terraform.tfvars.
- Help me diagnose why the service destroy is blocked.
Delete - I don't want to use this again
Remove all managed cloud resources from the selected Google Cloud project. Terraform must destroy the service consumer before deleting the image repository it depends on.
This cleanup is deliberately final. Your application code and Terraform configuration remain on your Mac.
Terraform presents a destruction plan before asking for confirmation.
- Switch back to the terminal you used for Terraform in the folder that contains app.py.
- Start with the Cloud Run service by running this command:
terraform -chdir=infra/service destroy
Why destroy the service first?
The infra/service root manages the Cloud Run service that consumes the image.
Removing that consumer first preserves the dependency order represented by the two Terraform roots.
- Review the service destruction plan.
- Enter the requested confirmation to approve the destroy.
- Wait for the terminal to confirm that the managed service was destroyed.
Once the service is gone, the repository can be removed with its stored images.
Terraform presents a second destruction plan before asking for confirmation.
- Remove the Artifact Registry repository by running this command:
terraform -chdir=infra/registry destroy
What does this command remove?
Terraform reads the state in infra/registry and deletes the release-dashboard repository.
Deleting the repository also removes its stored release dashboard images.
- Review the registry destruction plan.
- Enter the requested confirmation to approve the destroy.
- Wait for the terminal to confirm that the managed repository was destroyed.
What stays enabled?
The artifactregistry.googleapis.com and run.googleapis.com APIs stay enabled because both google_project_service resources set disable_on_destroy = false.
Enabled APIs do not incur charges merely by being enabled.
Generated Terraform data remains on your Mac after the cloud resources are gone. You can remove it while preserving the project source files.
- Remove the generated Terraform state, provider cache, and saved plans by running this command:
rm -rf infra/service/.terraform infra/service/terraform.tfstate* infra/service/tfplan infra/registry/.terraform infra/registry/terraform.tfstate* infra/registry/tfplan
What does this command remove?
This removes generated provider caches, state files, state backups, and saved plans from both Terraform roots.
Your .tf files and .tfvars files remain available as project source.
- Return to the Explorer sidebar in VS Code.
- Confirm that .terraform, terraform.tfstate, and tfplan are absent from both infrastructure roots.
Destroy command blocked?
- Confirm that Terraform can read your earlier Application Default Credentials sign-in.
- Confirm that the selected project matches the value in both terraform.tfvars files.
- Help me troubleshoot a blocked Terraform destroy.
That is the cloud cleanup complete. Your Google Cloud runtime and stored images are gone.
Nice Work!
Nice Work!
You made it! Terraform now deploys your Docker-packaged release dashboard to a public Cloud Run service.
You've learned how to:
- Built a styled release dashboard with visible runtime metadata. Exposed a JSON health response at /health. Ran the same image locally to reveal local-not-set metadata.
- Used separate Terraform roots to model the registry-before-runtime dependency. Provisioned Artifact Registry before publishing the Linux AMD64 image. Recorded the reproducible workflow in README.md.
- Configured scale to zero for the public service. Added a liveness probe for /health. Read service logs to confirm requests reached the deployment.
- Completed the Secret Mission by publishing an immutable v2 image. Applied the image change through Terraform. Proved the rollout created a new Cloud Run revision displaying Version 2: revision rollout complete.
Ready to quiz yourself?