In a nutshell
Ansible can build and run Google Cloud for you the same way it runs servers: you describe the end state in a playbook, and the google.cloud collection makes the GCP API calls needed to get there. If you have provisioned VMs by hand in the Cloud Console and thought “there must be a repeatable way to do this,” this is it.
Picture a GCP project as a single fenced building lot. Everything you construct sits inside that one lot: the VPC is the plot’s internal roads, the VMs are the buildings, the storage buckets are the sheds, the firewall rules are the perimeter fence. The permits (IAM policies) are stamped for that lot, and the utility meters (API quotas) are read per lot. That is exactly why every google.cloud module needs a project: — there is no “somewhere in the city” default to fall back on.
To work on the lot, Ansible shows a badge at the gate rather than carrying a master key to the whole city. The badge is a service account, and the cleanest badges are keyless: with Workload Identity Federation, a GitHub Actions run or an on-prem controller borrows a short-lived badge just for that one job, so there is no JSON key file lying around to leak. Your playbook is a job sheet written in the “there should be…” mood — there should be a VPC, a subnet, a firewall rule, an e2-micro VM — so running it twice does not rebuild anything the second time. That property is idempotency, and it is the whole reason to reach for Ansible instead of a pile of one-off gcloud commands. Finally, a dynamic inventory (the gcp_compute plugin) is the clipboard that walks the lot and reports what is actually there, grouped by label.
Level: Expert · Time: ~35 min
New to driving cloud from Ansible? Skim the Ansible for AWS or Ansible for Azure lesson first — the shape is identical across all three clouds; only the module names and the authentication story change. GCP happens to have the cleanest of the three.
GCP’s mental model is the cleanest of the three big clouds: a project is a single, hard boundary that holds every resource, every IAM policy, every API quota, every billing line. There is no “tenant + subscription + resource group” hierarchy to memorise — you have organization → folder → project, and the project is the only scope most automation cares about. The auth model is just as clean: every API call is a Google Credential object, resolved through the Application Default Credentials chain (env vars → SA key file → metadata server → gcloud user creds), and the IAM primitive is “principal × role × resource” where principal is one of user:, serviceAccount:, group:, or domain:. Once you internalise project scope and ADC, the modules in google.cloud become mechanical: gcp_<service>_<resource> modules with consistent shape, declarative idempotency, and a dynamic inventory (gcp_compute) that maps GCE labels and metadata into Ansible groups.
This lesson is the exhaustive tour. We start with the GCP mental model — projects, folders, organisations, IAM scopes, labels — and the Application Default Credentials chain that every module uses. We walk google.cloud module-by-module, focusing on the modules you actually use in production: networking (gcp_compute_network, gcp_compute_subnetwork, gcp_compute_firewall, gcp_compute_router, gcp_compute_address), compute (gcp_compute_instance, gcp_compute_instance_template, gcp_compute_instance_group_manager, gcp_compute_target_pool), data (gcp_storage_bucket, gcp_sql_instance, gcp_secret_manager_secret, gcp_kms_key_ring), identity (gcp_iam_role, gcp_iam_service_account, gcp_iam_policy_binding), and Kubernetes (gcp_container_cluster, gcp_container_node_pool). We cover the four authentication modes — ADC default chain, explicit Service Account JSON, gcloud user creds, and Workload Identity Federation for keyless auth from GitHub Actions / GitLab CI / on-prem AAP — and the auth_kind decision matrix. We re-meet the google.cloud.gcp_compute dynamic inventory plugin from a deeper angle than the dynamic inventory lesson, focusing on the GCP-specific knobs (auth_kind, projects:, zones:, filters:, hostnames, vars_prefix). We finish on multi-project patterns, label-driven grouping, idempotency for the awkward modules, GKE-native ops, and packaging a GCP-aware Execution Environment for AAP. Everything targets current Ansible (ansible-core 2.17+, google.cloud 1.4+, the Google Auth Python SDK google-auth / google-auth-oauthlib / google-cloud-* packages, 2026), uses FQCN throughout, and ends with a free hands-on lab that uses a GCP free-tier project plus the always-free e2-micro VM.
Learning objectives
After this lesson you can:
- Explain GCP’s project-scoped model and how IAM, quotas, and APIs hang off it.
- Pick the right
auth_kindfor the host running Ansible:application(ADC),serviceaccount(key file),accesstoken(short-lived),machineaccount(GCE metadata). - Authenticate with a Service Account JSON key (and rotate cleanly) and with Workload Identity Federation (keyless from GitHub Actions / on-prem AAP).
- Drive
google.cloud’s headline modules across networking, compute, data, IAM, and GKE. - Configure the
gcp_computedynamic inventory plugin with the GCP-specific knobs. - Operate a multi-project estate with per-task
project:and per-source inventory files. - Write a label schema that turns the inventory into clean cross-cutting groups (
label_environment_prod,zone_us_central1_a). - Ship a GCP-aware Execution Environment for AAP.
Prerequisites & where this fits
You should already be comfortable with playbooks and tasks, variables and the precedence rules, Jinja templating, roles and collections, and dynamic inventory in general. The companion expert lessons that compound here are Ansible for AWS, Ansible for Azure, Ansible for Kubernetes (for GKE-native ops), and Hybrid Orchestration. In the Ansible Zero-to-Hero programme this is the Cloud expert (GCP) lesson and a textbook EX374-grade topic.
Core concepts
Five mental models carry the whole lesson.
1. Project is the boundary. Every GCP resource lives in exactly one project. Every IAM policy is scoped to organization, folder, project, or resource. Every API quota is per-project. Every API has to be enabled per-project (compute.googleapis.com, container.googleapis.com, …). For Ansible this means project: is a required parameter on essentially every module — set it once via module_defaults, not per task.
2. Application Default Credentials is the auth chain. Every Google client library walks the ADC chain: GOOGLE_APPLICATION_CREDENTIALS env (path to JSON key) → gcloud auth application-default login cache → GCE metadata server (when running on GCE) → external account (Workload Identity Federation). You set the environment; ADC resolves. The Ansible parameter auth_kind lets you pin a specific source.
3. Workload Identity Federation is the keyless future. WIF lets a non-GCP identity (a GitHub Actions OIDC token, a GitLab CI JWT, an AWS IAM role, an Azure managed identity, or any OIDC-issuing IdP) impersonate a GCP service account without a JSON key. The pattern: configure a Workload Identity Pool + Provider in GCP, federate a non-GCP identity, point Ansible at the resulting external_account credential file. Net result: GitHub Actions runs Ansible against GCP with zero long-lived secrets.
4. Labels are the inventory. GCP labels (environment=prod, role=web, team=platform) flow into the gcp_compute plugin’s keyed_groups. A consistent label schema turns “all hosts” into clean cross-cutting groups. GCP also supports resource-level labels on most things (VMs, disks, buckets, SQL instances), so the schema scales beyond compute.
5. Most gcp_* modules are present/absent only. Like Azure, GCP modules don’t expose the seven-state network-module API. state: present reconciles; state: absent deletes. The module computes the diff from current → desired internally. The two awkwardnesses are: (a) properties that are immutable after creation (machine_type changes need a stop, then resize, then start — handled by gcp_compute_instance_machine_type rather than re-running gcp_compute_instance); (b) the name-based identity is global within a project for some resources (firewall rules) and per-zone for others (instances).
Keep these terms straight: project (scope boundary), organization/folder (parent containers), service account (automation identity in GCP IAM), Application Default Credentials (ADC) (the standard auth-chain), auth_kind (Ansible’s selector: application/serviceaccount/accesstoken/machineaccount), Workload Identity Federation (keyless cross-cloud identity), gcp_compute plugin (dynamic inventory), labels (the inventory key), scopes (compute-engine OAuth scopes — almost always cloud-platform for full access).
The GCP mental model
Organization (acme.com)
├── Folder: production
│ ├── Project: prod-eu-app (id: prod-eu-app-7fa2)
│ │ ├── VPC: prod-eu-vpc
│ │ ├── VM: prod-web-eu-1 (zone: europe-west1-b)
│ │ └── SQL: prod-app-db (region: europe-west1)
│ └── Project: prod-us-app
├── Folder: staging
│ └── Project: stg-eu-app
└── Folder: sandbox
└── Project: sandbox-engineer-X
IAM bindings live at: organization | folder | project | resource
IAM principals: user:, serviceAccount:, group:, domain:
Authentication — the four modes
auth_kind |
When to use | What you set | Risk |
|---|---|---|---|
application (ADC) |
Default; let the chain resolve | GOOGLE_APPLICATION_CREDENTIALS env or gcloud auth application-default login |
Depends on what’s in the env |
serviceaccount |
Explicit JSON key file | service_account_file: parameter |
Long-lived JSON key on disk |
accesstoken |
Short-lived OAuth token (e.g. from gcloud auth print-access-token) |
access_token: parameter |
Token expires; refresh logic on you |
machineaccount |
Control node is a GCE VM | nothing — uses GCE metadata server | Almost nothing — no key on disk |
Pattern A — control node on GCE (recommended)
Run AAP on a GCE VM with an attached service account. Modules use auth_kind: machineaccount. Zero credentials anywhere:
- name: Create VPC (uses GCE metadata creds)
google.cloud.gcp_compute_network:
name: prod-eu-vpc
auto_create_subnetworks: false
auth_kind: machineaccount
project: prod-eu-app-7fa2
state: present
Pattern B — Workload Identity Federation (keyless from anywhere)
Configure WIF in GCP:
PROJECT_ID=prod-eu-app-7fa2
POOL_ID=github-actions-pool
PROVIDER_ID=github-provider
SA_EMAIL=ansible-automation@$PROJECT_ID.iam.gserviceaccount.com
# 1. create the pool + OIDC provider
gcloud iam workload-identity-pools create $POOL_ID \
--location=global --display-name="GitHub Actions"
gcloud iam workload-identity-pools providers create-oidc $PROVIDER_ID \
--location=global \
--workload-identity-pool=$POOL_ID \
--issuer-uri=https://token.actions.githubusercontent.com \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository"
# 2. allow your SA to be impersonated by the federation
gcloud iam service-accounts add-iam-policy-binding $SA_EMAIL \
--role=roles/iam.workloadIdentityUser \
--member="principalSet://iam.googleapis.com/projects/$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')/locations/global/workloadIdentityPools/$POOL_ID/attribute.repository/myorg/myrepo"
In GitHub Actions:
# .github/workflows/ansible.yml
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github-actions-pool/providers/github-provider
service_account: ansible-automation@prod-eu-app-7fa2.iam.gserviceaccount.com
- run: ansible-playbook play.yml
The auth@v2 action writes an external_account credential file and points GOOGLE_APPLICATION_CREDENTIALS at it. Your Ansible play uses auth_kind: application — it picks up the federated identity transparently. Zero JSON keys committed anywhere.
Pattern C — Service Account JSON key (legacy / on-prem)
gcloud iam service-accounts create ansible-automation
gcloud iam service-accounts keys create ~/sa.json \
--iam-account=ansible-automation@$PROJECT_ID.iam.gserviceaccount.com
gcloud projects add-iam-policy-binding $PROJECT_ID \
--member="serviceAccount:ansible-automation@$PROJECT_ID.iam.gserviceaccount.com" \
--role=roles/editor
Then either:
- module_defaults:
group/google.cloud.gcp:
auth_kind: serviceaccount
service_account_file: /etc/ansible/sa.json
project: prod-eu-app-7fa2
…or set GOOGLE_APPLICATION_CREDENTIALS=/etc/ansible/sa.json and use auth_kind: application.
auth_kind decision matrix
| Where Ansible runs | auth_kind |
Why |
|---|---|---|
Engineer laptop with gcloud auth application-default login |
application |
Reads ADC cache |
| GitHub Actions / GitLab CI with WIF | application |
Reads federated external_account |
| AAP Controller on GCE | machineaccount |
Uses metadata server |
| AAP Container Group on GKE with Workload Identity (k8s) | machineaccount |
GKE Workload Identity injects the metadata bridge |
| AAP Controller on-prem with SA key | serviceaccount |
Explicit JSON key file |
| Short-lived demo / break-glass | accesstoken |
gcloud auth print-access-token |
google.cloud — the headline modules
# requirements.yml
collections:
- name: google.cloud
version: ">=1.4.0"
ansible-galaxy collection install -r requirements.yml
pip install requests google-auth google-auth-httplib2
| Module | Purpose | Idempotent? | Notes |
|---|---|---|---|
gcp_compute_network |
VPCs | Yes | auto_create_subnetworks: false for custom mode |
gcp_compute_subnetwork |
Subnets | Yes | Per-region |
gcp_compute_firewall |
Firewall rules | Yes | Project-scoped, applies to a network |
gcp_compute_address |
Static IPs | Yes | Regional or global |
gcp_compute_router |
Cloud Router | Yes | Required for Cloud NAT |
gcp_compute_instance |
VM instances | Idempotent on name: + zone |
Per-zone naming |
gcp_compute_instance_template |
Templates | Yes | The unit of MIG |
gcp_compute_instance_group_manager |
Managed Instance Groups | Yes | Regional or zonal |
gcp_compute_target_pool |
Target pools (legacy LB) | Yes | Use gcp_compute_backend_service for new LBs |
gcp_storage_bucket |
GCS buckets | Yes | Names globally unique |
gcp_sql_instance |
Cloud SQL | Yes | database_version: POSTGRES_16 etc. |
gcp_iam_role |
Custom IAM roles | Yes | Project or org scope |
gcp_iam_service_account |
Service accounts | Yes | Per-project |
gcp_kms_key_ring / gcp_kms_crypto_key |
KMS | Yes | Per-region |
gcp_secret_manager_secret |
Secret Manager | Yes | Use lookups to read secrets |
gcp_container_cluster |
GKE clusters | Idempotent on name: |
Heavy module — Terraform usually wins for cluster creation |
gcp_container_node_pool |
GKE node pools | Yes | Day-2 ops on existing clusters |
A canonical play with module_defaults:
- name: Provision a web tier in eu-prod
hosts: localhost
gather_facts: false
connection: local
module_defaults:
group/google.cloud.gcp:
auth_kind: machineaccount
project: prod-eu-app-7fa2
tasks:
- name: VPC
google.cloud.gcp_compute_network:
name: prod-eu-vpc
auto_create_subnetworks: false
state: present
- name: Subnet
google.cloud.gcp_compute_subnetwork:
name: web-eu-w1
region: europe-west1
ip_cidr_range: 10.42.1.0/24
network:
selfLink: projects/prod-eu-app-7fa2/global/networks/prod-eu-vpc
private_ip_google_access: true
state: present
- name: Firewall — allow HTTPS
google.cloud.gcp_compute_firewall:
name: allow-https
network:
selfLink: projects/prod-eu-app-7fa2/global/networks/prod-eu-vpc
direction: INGRESS
allowed:
- ip_protocol: tcp
ports: ["443"]
source_ranges: ["0.0.0.0/0"]
target_tags: ["web"]
state: present
- name: Web VM
google.cloud.gcp_compute_instance:
name: prod-web-eu-1
machine_type: e2-medium
zone: europe-west1-b
disks:
- auto_delete: true
boot: true
initialize_params:
source_image: projects/ubuntu-os-cloud/global/images/family/ubuntu-2404-lts
disk_size_gb: 30
disk_type: pd-balanced
network_interfaces:
- subnetwork:
selfLink: projects/prod-eu-app-7fa2/regions/europe-west1/subnetworks/web-eu-w1
access_configs:
- name: External NAT
type: ONE_TO_ONE_NAT
tags:
items: ["web"]
labels:
environment: prod
role: web
metadata:
ssh-keys: "ansible:{{ ssh_pubkey }}"
state: present
register: vm
Notice the selfLink: references — every cross-resource pointer in google.cloud is a selfLink (a fully-qualified URL into the GCP API), not just a name. This is verbose but unambiguous.
gcp_compute dynamic inventory — GCP-specific knobs
| Knob | Default | Purpose |
|---|---|---|
auth_kind |
(none — required) | Same selector as modules |
projects: |
(none — required) | List of project IDs to enumerate |
zones: |
(all) | Pre-filter by zone — performance lever |
filters: |
(none) | API-side filter expression (status = RUNNING AND labels.environment = prod) |
hostnames: |
[name] |
What field becomes the Ansible host name |
vars_prefix: |
gcp_ |
Prefix for hostvars |
compose: |
(none) | Same universal lever — Jinja-derived hostvars |
keyed_groups: |
(none) | Same universal lever — groups by key |
groups: |
(none) | Same universal lever — named groups via Jinja |
cache: / cache_plugin: / cache_timeout: |
(none) | Cache the inventory query |
Production-grade GCP inventory file:
# inventory/prod.gcp.yml
plugin: google.cloud.gcp_compute
auth_kind: machineaccount
projects:
- prod-eu-app-7fa2
- prod-us-app-3a91
zones:
- europe-west1-b
- europe-west1-c
- us-central1-a
filters:
- "status = RUNNING AND labels.environment = prod"
hostnames:
- name
compose:
ansible_host: networkInterfaces[0].networkIP
env: labels.environment | default('unknown')
role: labels.role | default('unknown')
keyed_groups:
- prefix: label
key: labels
- prefix: zone
key: zone | basename
- prefix: machine
key: machineType | basename
- prefix: project
key: project_id
groups:
prod_eu: labels.environment == 'prod' and zone is search('europe-')
needs_patch: labels.patched is not defined or labels.patched != 'true'
cache: true
cache_plugin: jsonfile
cache_connection: /var/cache/ansible_inventory
cache_timeout: 600
filters: is the single biggest performance lever — it’s a server-side filter expression in the GCE API’s filter language. Use it aggressively on big projects.
Multi-project patterns
GCP makes multi-project automation easy because projects are first-class.
Option A — single inventory, multiple projects
projects: in one inventory file lists all the projects you want to enumerate. The plugin stitches them together.
Option B — one inventory file per project
Cleaner for large fleets:
# inventory/prod-eu.gcp.yml
plugin: google.cloud.gcp_compute
projects: [prod-eu-app-7fa2]
auth_kind: machineaccount
# inventory/prod-us.gcp.yml
plugin: google.cloud.gcp_compute
projects: [prod-us-app-3a91]
auth_kind: machineaccount
Point inventory: at the directory; merging is automatic.
Option C — one play per project (for provisioning)
For provisioning plays (where you’re calling gcp_compute_* modules), one play per project with module_defaults setting project::
- import_playbook: plays/project-prod-eu.yml
- import_playbook: plays/project-prod-us.yml
- import_playbook: plays/project-stg.yml
Label strategy
Same shape as AWS tags / Azure tags — required first, governed via Organization Policy:
| Label | Required | Purpose |
|---|---|---|
environment |
Yes | prod/stg/dev |
role |
Yes | web/db/worker |
owner |
Yes | Team email (with hyphens — labels can’t have @) |
costcenter |
Yes | Finance attribution |
project_id_app |
Recommended | Distinguishes app-level project IDs |
patchgroup |
Recommended | Drives OS patching automation |
Enforce with the compute.requireOsLogin and custom Organization Policies that mandate label keys.
Idempotency & check-mode for awkward modules
| Module | Idempotency | Sharp edge |
|---|---|---|
gcp_compute_instance |
Idempotent on name: + zone |
machine_type: change requires stop → resize → start (use gcp_compute_instance_machine_type instead) |
gcp_compute_firewall |
Idempotent on name: (project-global) |
Firewalls are project-global; check naming collisions |
gcp_storage_bucket |
Globally unique name: |
Watch for collisions across all of GCP |
gcp_sql_instance |
Idempotent on name: |
Some properties require failover/restart |
gcp_container_cluster |
Idempotent on name: |
Don’t manage with Ansible if Terraform owns the cluster |
gcp_iam_* |
bindings: updates can be additive or replacing — read the docs |
GKE-native ops
For GKE-native ops (deploying workloads into a cluster) use the kubernetes.core collection, not google.cloud. The latter operates the cluster object (creates/scales node pools, upgrades the cluster); the former operates resources inside the cluster (Deployments, Services, ConfigMaps).
- name: Get GKE credentials and run a Deployment
hosts: localhost
gather_facts: false
tasks:
- name: Fetch kubeconfig
ansible.builtin.command:
cmd: gcloud container clusters get-credentials prod-eu --region europe-west1 --project prod-eu-app-7fa2
- name: Apply manifest
kubernetes.core.k8s:
state: present
src: manifests/web-deployment.yaml
Going deeper
Everything above is the “what.” This section is the “how it actually works” — the internals an experienced engineer needs before running google.cloud against a real estate.
How Application Default Credentials actually resolves
Every google.cloud module imports the google-auth Python library and calls google.auth.default(), which walks a fixed order: (1) the GOOGLE_APPLICATION_CREDENTIALS env var pointing at a JSON file — and that file may be a service-account key or an external_account (Workload Identity Federation) config; the library dispatches on the file’s top-level "type" field; (2) the gcloud-managed ADC cache at ~/.config/gcloud/application_default_credentials.json; (3) the GCE / GKE metadata server at 169.254.169.254. Ansible’s auth_kind is a short-circuit over that chain: serviceaccount forces path (1) with an explicit service_account_file:, machineaccount forces path (3), accesstoken hands the module a token you already minted, and application lets google.auth.default() run normally.
The practical upshot is why auth_kind: application is the portable default: it is the only value that behaves identically on a laptop (gcloud ADC), in CI (a WIF external_account), and on a GCE box (metadata) — the play never changes, only the environment does.
What “idempotent” means at the API level
Each gcp_* module does a GET on the resource’s self-link, compares the returned fields against the ones you declared, then issues a POST (create), PATCH/PUT (update), or DELETE — and only when they differ — reporting changed: true/false accordingly. Two consequences follow:
- Fields you don’t specify are generally left alone. A partial task won’t wipe unmanaged properties — but the flip side is the module can’t detect drift on a property you never mention. If you want a value enforced, you must declare it.
- Immutable fields can’t be PATCHed. Change a VM’s
machine_type:or its boot-disksource_image:and the API refuses the in-place update; the module errors rather than silently deleting and recreating the VM. That is a safety feature, not a bug — Ansible will never quietly destroy a stateful resource because you edited one line.
Out-of-band changes (someone edits in the Cloud Console) are caught on the next run because the GET returns the console’s value and the module reconciles it back to your declared state. That reconcile-on-every-run behaviour is your drift control.
Check-mode, --diff, and the honest caveat
Run any provisioning play with --check --diff and the modules perform the read-only GETs but skip the writes, reporting what would change. The caveat: check-mode accuracy depends on each module predicting the API’s behaviour without calling it, and for a few resources where the API computes values server-side (auto-assigned IPs, generated self-links, server-defaulted fields) the predicted diff can be noisier than a real apply. Treat --check as a strong signal, not a contract. The authoritative idempotency test is still apply, then apply again and confirm changed=0.
Performance and scale
Three levers matter once the estate is large:
- Inventory: filter server-side. The
filters:knob is a GCE API filter expression evaluated in Google’s datacentre, so a 5,000-VM project filtered tolabels.environment = prodreturns ~200 rows in one paginated call instead of streaming all 5,000 to the control node. Combine it withzones:to prune whole regions andcache: trueto avoid re-querying between plays in the same run. - Provisioning: the API is the bottleneck, not Ansible.
gcp_*tasks run onlocalhostagainst the API, so raisingforksdoes nothing for a single serial play. Parallelise independent resources withasync:+poll: 0andansible.builtin.async_status, or split the work across projects into separate plays. - Quota and 429s. Compute Engine enforces per-project, per-region write quotas. A big fan-out of instance creates can hit
RESOURCE_EXHAUSTED/ HTTP 429. Throttle withthrottle:on the task orserial:on the play, and request quota increases ahead of a large rollout, not during the incident.
Token lifetime on long plays
Federated and access-token credentials are short-lived (typically one hour). A play that runs for more than an hour on a single token can fail mid-way with an auth error. With auth_kind: application + WIF, the google-auth library re-reads the external_account file and refreshes automatically in most cases; with accesstoken you own the refresh entirely. For genuinely long rollouts, split into shorter plays or refresh the credential in a pre_tasks step rather than betting the whole run on a single mint.
google.cloud vs community.google
The supported, actively-maintained collection is google.cloud — the gcp_* modules and the gcp_compute inventory plugin. A handful of lookups and older interactions historically live in community.google (for example, reading a Secret Manager value at play time via a community.google.gcp_secret_manager_secret lookup, as used in the Security notes below). The rule of thumb: the manage path (create/update/delete a secret) is a google.cloud module; a read-at-runtime path may be a community.google lookup. If you use both, pin both in requirements.yml.
Packaging a GCP Execution Environment (internals)
For AAP, the modules and their Python SDK must live inside an Execution Environment image, built with ansible-builder. See the building Execution Environments lesson for the full workflow; the GCP-specific execution-environment.yml (ansible-builder v3 schema) looks like:
# execution-environment.yml
version: 3
images:
base_image:
name: registry.redhat.io/ansible-automation-platform-25/ee-minimal-rhel9:latest
dependencies:
galaxy:
collections:
- name: google.cloud
version: ">=1.4.0"
- name: kubernetes.core
python:
- requests
- google-auth
- google-auth-httplib2
- google-cloud-storage
system:
- git
options:
package_manager_path: /usr/bin/microdnf
If your plays use the GKE auth bridge (gcloud container clusters get-credentials … before a kubernetes.core.k8s task), you also need the Cloud SDK inside the image — add it via an additional_build_steps.append_final RUN step that installs gcloud. Push the finished image to Private Automation Hub so execution nodes pull a signed, air-gap-friendly EE rather than reaching the public internet at run time.
Hands-on free lab — GCP free-tier
GCP gives every new account $300 credit + an always-free e2-micro VM in us-central1/-east1/-east4. The lab uses both.
# create a project (or use existing)
gcloud projects create my-ansible-lab --name="Ansible Lab"
gcloud config set project my-ansible-lab
gcloud auth application-default login
gcloud services enable compute.googleapis.com storage.googleapis.com
# install collection + deps
ansible-galaxy collection install google.cloud
pip install requests google-auth google-auth-httplib2
# play.yml
- hosts: localhost
gather_facts: false
connection: local
module_defaults:
group/google.cloud.gcp:
auth_kind: application
project: my-ansible-lab
tasks:
- name: VPC
google.cloud.gcp_compute_network:
name: lab-vpc
auto_create_subnetworks: false
state: present
- name: Subnet
google.cloud.gcp_compute_subnetwork:
name: lab-sub
region: us-central1
ip_cidr_range: 10.42.1.0/24
network:
selfLink: projects/my-ansible-lab/global/networks/lab-vpc
state: present
- name: Firewall
google.cloud.gcp_compute_firewall:
name: lab-allow-ssh
network:
selfLink: projects/my-ansible-lab/global/networks/lab-vpc
direction: INGRESS
allowed:
- ip_protocol: tcp
ports: ["22"]
source_ranges: ["0.0.0.0/0"]
state: present
- name: e2-micro (always-free)
google.cloud.gcp_compute_instance:
name: lab-vm
machine_type: e2-micro
zone: us-central1-a
disks:
- auto_delete: true
boot: true
initialize_params:
source_image: projects/debian-cloud/global/images/family/debian-12
disk_size_gb: 10
disk_type: pd-standard
network_interfaces:
- subnetwork:
selfLink: projects/my-ansible-lab/regions/us-central1/subnetworks/lab-sub
access_configs:
- name: External NAT
type: ONE_TO_ONE_NAT
labels:
environment: lab
role: vm
state: present
ansible-playbook play.yml --diff
ansible-playbook play.yml --diff # second run — changed=0
Inventory test:
# inv.gcp.yml
plugin: google.cloud.gcp_compute
auth_kind: application
projects: [my-ansible-lab]
hostnames: [name]
keyed_groups:
- prefix: label
key: labels
ansible-inventory -i inv.gcp.yml --graph
Tear down:
ansible localhost -m google.cloud.gcp_compute_instance \
-a "name=lab-vm zone=us-central1-a project=my-ansible-lab auth_kind=application state=absent"
Practice challenges
Work these in order against your free-tier project — they escalate from beginner to advanced. Each has a worked solution; peek only after you’ve tried.
1. (Beginner) Stop repeating project: and auth_kind:. Take a two-task play and move the auth/project settings so no task carries them.
<details> <summary>Solution</summary>
- hosts: localhost
gather_facts: false
connection: local
module_defaults:
group/google.cloud.gcp:
auth_kind: application
project: my-ansible-lab
tasks:
- name: VPC (no project:/auth_kind: on the task)
google.cloud.gcp_compute_network:
name: lab-vpc
auto_create_subnetworks: false
state: present
Why: the group/google.cloud.gcp action group applies the defaults to every gcp_* module in the play, so auth and project live in exactly one place and can’t drift task to task.
</details>
2. (Beginner) Create a custom-mode VPC + one subnet, and prove it’s idempotent. Add a subnet, run the play twice, and confirm the second run changes nothing.
<details> <summary>Solution</summary>
- name: Subnet
google.cloud.gcp_compute_subnetwork:
name: lab-sub
region: us-central1
ip_cidr_range: 10.20.0.0/24
network:
selfLink: projects/my-ansible-lab/global/networks/lab-vpc
state: present
Run ansible-playbook play.yml twice; the second run reports changed=0.
Why: the module GETs the subnet, sees the CIDR/region already match your declaration, and issues no PATCH — that unchanged second run is the definition of idempotent. </details>
3. (Intermediate) Allow HTTPS only to web-tagged instances — and understand the name scope. Write a firewall rule scoped by tag, then reason about whether a second network could reuse the name.
<details> <summary>Solution</summary>
- name: Allow HTTPS to the web tier
google.cloud.gcp_compute_firewall:
name: lab-allow-https
network:
selfLink: projects/my-ansible-lab/global/networks/lab-vpc
direction: INGRESS
allowed:
- ip_protocol: tcp
ports: ["443"]
source_ranges: ["0.0.0.0/0"]
target_tags: ["web"]
state: present
Why: target_tags scopes the rule to instances tagged web; the firewall name is unique across the whole project, not per network — a second VPC cannot reuse lab-allow-https, so real fleets prefix names (prod-eu-allow-https).
</details>
4. (Intermediate) Build an inventory that returns only RUNNING prod VMs, grouped by label and zone. Verify the groups with --graph.
<details> <summary>Solution</summary>
# prod.gcp.yml
plugin: google.cloud.gcp_compute
auth_kind: application
projects: [my-ansible-lab]
filters:
- "status = RUNNING AND labels.environment = prod"
keyed_groups:
- prefix: label
key: labels
- prefix: zone
key: zone | basename
ansible-inventory -i prod.gcp.yml --graph
Why: filters: runs server-side so only RUNNING prod VMs cross the wire, and keyed_groups turns each label and the zone into groups like label_environment_prod and zone_us_central1_a — the cheap, correct way to target a slice of the fleet.
</details>
5. (Advanced) Convert a JSON-key play to keyless. Assume WIF is already configured and the google-github-actions/auth@v2 step has written an external_account file. Change only the auth block.
<details> <summary>Solution</summary>
Before:
module_defaults:
group/google.cloud.gcp:
auth_kind: serviceaccount
service_account_file: /etc/ansible/sa.json
project: my-ansible-lab
After:
module_defaults:
group/google.cloud.gcp:
auth_kind: application
project: my-ansible-lab
Why: application resolves ADC, which now finds the federated external_account credential GOOGLE_APPLICATION_CREDENTIALS points at — the tasks are byte-for-byte identical, only the credential source changed, and no JSON key exists on disk to leak or rotate.
</details>
6. (Advanced) Fire an e2-micro create asynchronously, then prove SSH is up via a dynamically-added host. Preview with --check --diff first, then apply for real.
<details> <summary>Solution</summary>
- name: Create e2-micro (fire and forget)
google.cloud.gcp_compute_instance:
name: lab-vm
machine_type: e2-micro
zone: us-central1-a
disks:
- auto_delete: true
boot: true
initialize_params:
source_image: projects/debian-cloud/global/images/family/debian-12
network_interfaces:
- subnetwork:
selfLink: projects/my-ansible-lab/regions/us-central1/subnetworks/lab-sub
access_configs:
- name: External NAT
type: ONE_TO_ONE_NAT
labels:
environment: lab
role: web
state: present
register: vm
async: 300
poll: 0
- name: Wait for the create to finish
ansible.builtin.async_status:
jid: "{{ vm.ansible_job_id }}"
register: job
until: job.finished
retries: 30
delay: 10
- name: Add the new VM to inventory (return keys are camelCase, per the module RETURN block)
ansible.builtin.add_host:
name: "{{ job.networkInterfaces[0].accessConfigs[0].natIP }}"
groups: just_built
- name: Confirm SSH is reachable
ansible.builtin.wait_for_connection:
timeout: 120
delegate_to: "{{ job.networkInterfaces[0].accessConfigs[0].natIP }}"
Why: poll: 0 fires the create and returns immediately; async_status polls to completion; add_host + wait_for_connection prove the VM is reachable without ever writing a static inventory entry. Preview with --check --diff to see the plan before you commit.
</details>
Common beginner mistakes
These are misconceptions, not error messages — the wrong mental model that produces the error later.
- “I’ll just set
project:on the first task and the rest inherit it.” They don’t. Everygcp_*task is independent; an unsetproject:on a later task errors out or, worse, targets the wrong project. The right model: setproject:(andauth_kind:) once inmodule_defaultsongroup/google.cloud.gcp, never per task. - “Ansible will enable the APIs I need.” It won’t.
compute.googleapis.com,container.googleapis.com,sqladmin.googleapis.comand friends must be enabled first. API enablement is a one-time bootstrap step (gcloud services enable …or a Terraform bootstrap), separate from provisioning. - “I need a JSON key to authenticate.” A JSON key is the least preferred option, not the default. The right model: keyless first — Workload Identity Federation off-GCP, the metadata server on-GCP — and treat a downloaded SA key as a fallback you must rotate and guard.
- “A firewall rule belongs to a network, so its name only has to be unique within that network.” Firewall names are project-global. Two VPCs in the same project cannot both have
allow-https. The right model: prefix names by scope (prod-eu-allow-https). - “I can reference a VPC by its name.” Occasionally, but the collection expects a
selfLink:— a full API URL likeprojects/X/global/networks/Y. The right model: treat cross-resource pointers as selfLinks by default; name-only references are the exception. - “
changed=0on the first run means it worked.” On the first run a correct play should reportchanged>0— it built things. It is the second run that must bechanged=0. Changed-on-apply, unchanged-on-re-apply is the idempotency signature;changed=0first time usually means nothing ran. - “Terraform and Ansible are an either/or choice.” They’re complementary. The right model: Terraform owns birth-to-death of stateful infra (VPCs, GKE clusters) through its state file; Ansible owns configuration, day-2 ops, and orchestration.
gcp_container_clusteris for day-2 operations on an existing cluster, not for creating the cluster Terraform already manages.
Common mistakes & troubleshooting
ImportError: No module named google.auth. The Execution Environment doesn’t have google-auth. Bake requests, google-auth, google-auth-httplib2 into your EE.
PermissionDenied on every API call. The service account / federated identity lacks the right role. Start with roles/editor for lab; lock down to per-API roles (roles/compute.admin, roles/storage.admin) in production.
Inventory returns 0 hosts. Either: (a) enable_plugins doesn’t list google.cloud.gcp_compute; (b) the file isn’t named *.gcp.yml; © auth_kind doesn’t match what your environment actually has; (d) projects: is missing or wrong; (e) filters: excludes everything.
API not enabled. Run gcloud services enable compute.googleapis.com (and any others you need). Ansible doesn’t auto-enable APIs.
Firewall name collision across networks. Firewall names are project-global, not network-scoped. Use prefixes (prod-eu-allow-https).
gcp_compute_instance rebuilds the VM unexpectedly. You changed machine_type: or the boot disk. Use gcp_compute_instance_machine_type for size changes; boot disk is essentially write-once.
Workload Identity Federation token expired mid-play. Long plays past 1h can hit token expiry. Configure ttl: 3600s on the federation provider; for very long plays, refresh the token explicitly in a pre-task or split the work into multiple shorter plays.
shell: gcloud compute instances create … everywhere. Replace with gcp_compute_instance. The CLI is for humans; the module is for automation.
Best practices
auth_kind: machineaccountwhen the control node is GCE;auth_kind: application+ WIF when it’s anywhere else.module_defaultswithgroup/google.cloud.gcpto setauth_kind,project, and (where applicable)service_account_fileonce.projects:in inventory at the source level so each file targets one project.- Label schema enforced via Organization Policy.
filters:aggressively in the inventory — server-side filtering wins.- Cache the inventory.
cache: truewith a 5-10 minute timeout. - Pin collection versions.
google.cloud 1.4+. - Build a GCP EE with
google.cloud,requests,google-auth,google-auth-httplib2, and thegcloudCLI forkubernetes.coreGKE auth. - Leave cluster creation to Terraform; use Ansible for in-cluster ops.
- Mesh execution nodes inside the VPC. Cross-VPC firewall holes are a security anti-pattern.
Security notes
- No long-lived SA JSON keys in production. Workload Identity Federation is the standard for non-GCP CI; Workload Identity (the GKE feature) is the standard for GKE pods; instance metadata is the standard for GCE control nodes.
- Use Cloud Audit Logs. Every Ansible API call shows up in Admin Activity logs (and Data Access logs if enabled). Aggregate to BigQuery for forensics.
- Per-API roles, not
roles/editor. The Ansible service account should have only the roles it needs —roles/compute.admin,roles/storage.admin, etc. - Tag-based / label-based IAM conditions: write a role binding that allows
compute.instances.startonly when the instance labelteam == ${user.team}. Ansible can’t accidentally start the wrong team’s VMs. - Vault any database passwords. Or use Secret Manager and
lookup('community.google.gcp_secret_manager_secret', 'projects/X/secrets/Y/versions/latest'). - Air-gap-friendly EE. Push to Private Automation Hub.
- Block public GCS buckets by default.
iam_configuration: { uniform_bucket_level_access: true }on everygcp_storage_bucket.
Interview & exam Q&A
Q1. What’s the auth chain a google.cloud module walks?
The Application Default Credentials chain: explicit module params → GOOGLE_APPLICATION_CREDENTIALS env (path to JSON or external_account file) → gcloud auth application-default login cache → GCE metadata server. Pinned by auth_kind:.
Q2. Why prefer Workload Identity Federation to a Service Account JSON key? WIF is keyless: a federated OIDC identity (GitHub Actions OIDC token, AWS role, Azure MI) impersonates a GCP service account via short-lived tokens. No JSON key on disk, no rotation burden, audit trail in Cloud Audit Logs shows the federated principal.
Q3. What’s the difference between auth_kind: application and auth_kind: serviceaccount?
application resolves via ADC — let the chain pick the right credential. serviceaccount pins a specific JSON key file via service_account_file:. In production, prefer application so the same play works on engineer laptops, CI, and AAP.
Q4. Why is module_defaults with group/google.cloud.gcp important?
Every gcp_* module needs auth_kind and project. Without module_defaults, every task repeats them, and inevitable drift causes auth bugs. Set them once at play level.
Q5. How does the gcp_compute plugin handle multiple projects?
projects: is a list — the plugin enumerates VMs across each. Or use one inventory file per project; both files are merged when inventory: points at the directory.
Q6. What’s the most performance-impactful inventory knob?
filters: — server-side filter expression in GCE API syntax. A 5,000-instance project filtered by labels.environment = prod returns 200 rows in one API call.
Q7. When would you prefer google.cloud.gcp_container_cluster over Terraform for GKE?
For day-2 ops on a cluster that already exists (scaling node pools, enabling features, upgrading). For creation of new GKE clusters Terraform usually wins because of state-file dependency tracking.
Q8. Difference between GKE Workload Identity and Workload Identity Federation? Workload Identity (GKE feature) lets pods in a GKE cluster impersonate a GCP service account via the cluster’s metadata bridge. Workload Identity Federation (IAM feature) lets external identities (GitHub Actions, AWS, Azure) impersonate a GCP service account via OIDC. Different scopes, same goal: keyless auth.
Q9. What goes into a production GCP EE?
ansible-builder with google.cloud, the SDK (requests, google-auth, google-auth-httplib2, google-cloud-storage, google-cloud-secret-manager if you read secrets), the gcloud CLI (for GKE auth bridge), and your shared utility collections.
Q10. How do you handle multi-project IAM consistently?
Group IAM bindings by team in YAML, loop gcp_iam_policy_binding over them. The data file is your audit trail: who has what role at what scope.
Q11. When does gcp_compute_instance rebuild the VM?
When you change a property that isn’t in-place mutable: machine_type:, the boot disk’s source_image:, network attachments. Use the dedicated machine-type module for size changes; boot disk is write-once.
Q12. Why is selfLink: so prevalent in google.cloud?
Because GCP cross-resource references are URL-shaped — projects/X/global/networks/Y — and unambiguous across projects, regions, and zones. Verbose but precise; you’ll never accidentally reference the wrong VPC in another project.
Q13. How do you read a Secret Manager secret at play time?
community.google.gcp_secret_manager_secret lookup, or google.cloud.gcp_secret_manager_secret module with state: present for management; lookup for reading. Combine with module_defaults so credentials never appear in role defaults.
Q14. What’s the correct auth choice for AAP Container Groups in GKE?
Configure GKE Workload Identity on the cluster, annotate the EE pod’s ServiceAccount with iam.gke.io/gcp-service-account=<SA-email>, set auth_kind: machineaccount in plays. The cluster’s metadata bridge does the rest.
Quick check
- What
auth_kindshould an AAP Controller running on a GCE VM use? - How do you authenticate a GitHub Actions runner to GCP without a JSON key?
- Which inventory plugin knob does server-side filtering?
- Why is
project:always required ongcp_*modules? - What’s the relationship between
google.cloud.gcp_container_clusterandkubernetes.core?
(Answers: machineaccount; Workload Identity Federation with google-github-actions/auth@v2; filters: (uses GCE API filter syntax); because every resource lives in exactly one project — there is no implicit default; the former operates the cluster object, the latter operates resources inside the cluster.)
Exercise
With your free-tier project:
- Create a service account and either run
gcloud auth application-default login(laptop) or attach the SA to a tiny e2-micro and useauth_kind: machineaccount. - Write a play that creates a VPC, subnet, firewall, and an
e2-microalways-free VM with proper labels. - Add an
add_hoststep using the VM’s external IP and await_for_connectionto confirm SSH. - Build a
gcp_computeinventory file pointing at your project; verify--graphshowslabel_environment_labetc. - (Stretch) Set up Workload Identity Federation for a GitHub Actions repo and run the play from CI keylessly.
- Run with
--check --diff. Then for real. Then again —changed=0.
Certification mapping
| Cert | Coverage |
|---|---|
| EX374 — Red Hat Certified Specialist in Ansible Automation | Direct: cloud collections, dynamic inventory, EE. |
| Google Associate Cloud Engineer | Indirect: project / IAM / VPC mental model. |
| Google Professional Cloud DevOps Engineer | Direct: deployment automation, federated auth. |
Glossary
- Project — GCP scope boundary; every resource lives in one.
- Organization / folder — parent containers above project for IAM and policy.
- Application Default Credentials (ADC) — the standard auth chain.
auth_kind— Ansible’s auth-mode selector (application/serviceaccount/accesstoken/machineaccount).- Service account — automation identity in GCP IAM (with optional JSON key).
- Workload Identity Federation — keyless cross-cloud identity via OIDC.
- GKE Workload Identity — pod-level identity inside a GKE cluster.
selfLink— fully-qualified URL reference to a GCP resource.gcp_computeplugin — dynamic inventory plugin ingoogle.cloud.- Labels — key/value metadata on resources; the inventory key.
module_defaults/ action group — thegroup/google.cloud.gcpaction group lets you setauth_kind,project, andservice_account_fileonce for everygcp_*module in a play.external_account— the credential-file"type"that Workload Identity Federation writes; ADC treats it as a keyless identity.- Metadata server — the
169.254.169.254endpoint on GCE/GKE that hands the attached service account’s token toauth_kind: machineaccount. keyed_groups— an inventory lever that turns a field (likelabels) into Ansible groups such aslabel_environment_prod.filters:— an inventory knob that pushes a GCE-API filter expression to Google’s side so only matching VMs are returned.- Execution Environment (EE) — the container image (built with
ansible-builder) that carries the collection plus its Python SDK so AAP runs the plays reproducibly. - Check mode (
--check) — a dry run: modules do read-only GETs and report what would change, without writing. - Organization Policy — an org/folder/project-level guardrail (e.g. mandating label keys or
requireOsLogin) enforced by GCP, not by Ansible. - Quota /
RESOURCE_EXHAUSTED— the per-project, per-region API write limits; a large fan-out can hit HTTP 429 and needs throttling.
Next steps
You can now drive GCP from Ansible. With AWS, Azure, and GCP under your belt, continue with Ansible for Windows for the third major OS family, Ansible for Kubernetes for cluster-internal ops on GKE/EKS/AKS, and Hybrid Multi-Cloud Orchestration to compose all three clouds in a single workflow.