Why zero ClickOps
Why every piece of infrastructure in this course lives in git as Pulumi C#, and how the course and its labs work.
Before you write a line of code, I want to explain the one rule that shapes every module of this course: you never create or change infrastructure by clicking in the Google Cloud console. This lesson is about why, what we use instead, and how the course itself works.
What ClickOps is
Section titled “What ClickOps is”ClickOps is configuring infrastructure by hand through a web console. You open the console, find the right page, fill in a form, press “Create”. It is the fastest way to get something running the first time, and the console is genuinely good at it. That is exactly the trap.
Here is a story you may recognise. A small team builds an invoicing service. One engineer, call her Dana, sets up the cloud project in an afternoon: a container service, a registry, a database, a couple of secrets, a service account with “Editor” because the deploy kept failing with permission errors. It works. Everyone moves on.
Eight months later the team needs a staging environment. Nobody remembers which of the 40 settings on the service page Dana changed from the defaults. Dana has left. Someone screenshots every page of production and recreates it by hand in a second project, which takes two days and still ends up different: staging has a newer runtime, a different memory limit and a secret that points to the production database. The first time staging behaves differently from production, nobody can say whether it is the code or the environment.
Then finance asks why the bill has an extra 140 dollars a month. It turns out there is a forgotten VM from an experiment, a load balancer for a service that was deleted, and a 900 GB bucket of old images. Nobody knows whether any of it is still needed, so nobody dares to delete it.
None of this needed a bad engineer. It only needed the console.
Five costs of configuring by hand
Section titled “Five costs of configuring by hand”The story above contains five separate problems. They are worth naming because each of them is solved by the same practice.
- Not repeatable. A console session leaves no recipe. You cannot run “the thing Dana did” a second time, for a second environment, a second region or a disaster recovery drill.
- Not reviewable. A change made in the console is live the moment you press the button. Nobody reads it before it happens. In application code you would never accept “I pushed straight to production, trust me”.
- No history. Cloud Audit Logs record that a setting changed and who changed it, but not why, and reading them is archaeology. A git log with a pull request link is history a human can use.
- Drift between environments. Two environments built by hand diverge on day one and keep diverging. Every “works in staging” bug that turns out to be a config difference is drift.
- Cannot delete cleanly. If you do not have a list of what you created, you cannot remove all of it. Forgotten resources keep running and keep billing. For a course like this one, where you will create and destroy things every module, this is the cost that hits your own card.
The rule of this course
Section titled “The rule of this course”Every resource in this course is defined in code, in git, and changed by a pull request. Concretely:
- All infrastructure is a Pulumi program in C#, in the
infra/folder of your repository. - You apply changes with
pulumi up(from your laptop in modules 1-5, from Cloud Build from module 6 on). - The console is read-only. You use it to look: logs, metrics, the revision list, the IAM page to confirm what your code did. You do not use it to change anything.
- If you find yourself about to click “Create” or “Edit”, stop and write the code instead. If you clicked anyway, delete what you clicked and redo it in code. It is cheaper now than in eight months.
The payoff is that your entire project becomes reproducible with one command. Delete the project, create a new one, run the bootstrap script and pulumi up, and you are back where you were. That is also how you will clean up after every lab.
Why Pulumi in C#
Section titled “Why Pulumi in C#”There are several good infrastructure-as-code tools. I chose Pulumi with C# for this course for reasons specific to you, a .NET developer:
- One language for the app and the infrastructure. You already know C#. The Cloud Run service, its service account and its secrets are C# objects in a project that sits next to your API in the same solution.
- Types and IntelliSense. Property names, required fields and enum values are checked by the compiler. A typo in a region name is still a runtime error, but a typo in a property name is a red squiggle, not a failed deploy.
- Real code tools. Loops, functions, classes, NuGet packages, refactoring in your IDE, and unit tests with xUnit against your infrastructure definitions. When three services share the same security settings, you write a method, not a copy.
- Reproducible with one command.
pulumi upcomputes the difference between your code and what exists, shows you a preview, and applies only that difference.pulumi destroyremoves everything the stack owns.
Pulumi’s state (the record of which real resources belong to your program) will live in a Cloud Storage bucket in your own project, and its secrets will be encrypted with a Cloud KMS key you own. No third-party service holds anything of yours. You will set both up in the next lesson.
Being honest about Terraform
Section titled “Being honest about Terraform”If you search job ads for “infrastructure as code”, most of them say Terraform, and some now say OpenTofu, the open-source fork created after HashiCorp moved Terraform to the Business Source License in 2023. Pulumi is common but not dominant. I do not want this course to leave you unable to read the tool most teams use.
So every infrastructure snippet in this course has a Terraform tab next to the Pulumi one. The main path, the labs and the explanations use Pulumi; the Terraform tab shows the same resources in HCL so you can map one to the other. Most of the time it is a near one-to-one translation, because the Pulumi Google Cloud provider is generated from the same upstream provider code that Terraform uses, and resource and property names line up closely.
| Pulumi | Terraform / OpenTofu | |
|---|---|---|
| Language | C#, TypeScript, Python, Go, Java, YAML | HCL, a declarative configuration language |
| Logic | Full language: loops, functions, classes, tests | count, for_each, modules, expressions; limited by design |
| State | Pulumi Cloud by default, or self-managed (GCS, S3, Azure Blob, local) | Local by default, or a backend (GCS, S3, HCP Terraform, others) |
| Ecosystem | Google Cloud provider covers the same resources as Terraform’s; fewer community modules | Largest provider and module registry; most examples online are HCL |
| Hiring | Asked for in some teams, often .NET or TypeScript shops | Asked for in most IaC job ads |
| License | Apache 2.0 | Terraform: BSL 1.1. OpenTofu: MPL 2.0 |
My honest summary: Terraform’s main strength is ubiquity and its main weakness is that HCL is not a programming language, which you notice the day you need real logic or tests. Pulumi’s main strength is that it is your language and your tooling, and its weakness is a smaller community and fewer copy-paste examples. The concepts (resources, state, plan, apply, drift) transfer completely. If you can do this course in Pulumi, you can read and write Terraform within a week.
The two exceptions
Section titled “The two exceptions”“Zero ClickOps” is the rule, but there are two places where you cannot avoid a manual step. I would rather name them than pretend they do not exist.
1. The bootstrap (module 0). Pulumi needs somewhere to store its state and a key to encrypt secrets before it can manage anything, and someone has to link billing to the project and switch on the APIs. That is a chicken-and-egg problem: the tool that creates infrastructure needs infrastructure to exist first. We solve it with a short bash script, scripts/bootstrap.sh, that runs gcloud commands once. It is acceptable because it is a script in git: reviewable, repeatable and safe to run twice. It is still not a console click.
2. The GitHub app install (module 6). To let Cloud Build read your GitHub repository, GitHub requires a human to install the Google Cloud Build GitHub app and approve access in the browser. That is GitHub’s security model, and it is a good one: no API should be able to grant repository access on your behalf without you. Everything around it (the connection, the repository link, the trigger) is in Pulumi.
That is the complete list. If a later lesson ever asks you to click something else, treat it as a bug in the course and tell me.
How this course works
Section titled “How this course works”The course has a module 0 (this lesson and setup), six skill modules and a final project. Every skill module follows the same shape.
An incident log first. Each module opens with a short log from a realistic failure: a revision that will not start, a 900 MB image running as root, a leaked key. The logs are invented, but every one of them is a failure I or someone I worked with has seen. The module then builds the knowledge you need to explain and fix it. You learn the tool because you need it, not because it is next in the documentation.
The running app. You will deploy Sniplink, a small URL shortener built with ASP.NET Core minimal APIs. It keeps its data in memory on purpose, because persistence is the topic of a later course, not this one. In the final project you build a second service, Dayquote, from an empty folder without step-by-step instructions.
Labs verified by URL. Every module ends with a lab. You deploy to your own Google Cloud project, paste your service URL into the lab page, and the platform checks your service from outside. It never needs access to your project. The service proves it is really running on Cloud Run in your project by returning a Google-signed ID token, which the platform validates against Google’s public keys.
LabKit. The verification endpoints come from a small open-source NuGet package, CuriousDev.LabKit. You add two lines to Program.cs and it maps a few /_lab/* endpoints. LabKit follows one rule: it automates plumbing that is the same for everyone and is not what the lab teaches, and it never implements the skill being tested. It fetches the ID token for you; it does not read your secret, write your health checks or configure your service. Those are yours. LabKit endpoints never return secrets or environment variables, only evidence such as an HMAC over a random nonce.
Some checks are self-reported: the service tells the platform it runs as a non-root user, and the platform has to take its word for it. When a check is self-reported, the lesson says so and explains what a stricter check would need.
The certificate. Pass the labs of modules 1 to 7 and you receive the certificate “Containerizing & Deploying .NET on Google Cloud”. At launch this is the Completed tier: every check is external, through your URL. A stricter Verified tier is coming later. It is optional and will need you to grant the platform narrow read-only access to an isolated course project, so it can check things a URL cannot show, such as image size and IAM bindings. You will never need it to finish the course, but it is one more reason to use a fresh project for the course, which the next lesson asks you to do.
What you learned
Section titled “What you learned”- ClickOps costs you repeatability, review, history, environment parity and clean deletion, and the last one shows up on your bill.
- In this course every resource is Pulumi C# in git, changed by pull request, and the console is read-only.
- Pulumi in C# gives you one language, compiler checks and real tests for your infrastructure; Terraform gets a tab in every module because the job market uses it.
- There are exactly two manual exceptions: the bootstrap script and the GitHub app install, and both are deliberate.
- Each module runs incident, fix, lab; labs are checked from outside through your URL with help from LabKit, which handles plumbing and never the skill.