Everything AlpinaShop has built over seven modules works because three people remember. Marta remembers not to create VMs with public IPs. Dani remembers to label resources. Lucía remembers not to export data to an external bucket. The SLOs live on a dashboard somebody maintains, the runbooks get updated because somebody reviews them, and the error budget policy is respected because the team agreed it one afternoon.
None of that is wrong. All of it is fragile.
Because today, technically, nothing stops somebody creating a machine with a public IP in Iowa tomorrow, disabling an audit log, sharing a bucket with allUsers, or downloading a service account JSON key. The good practices in this course are agreements, not controls. And an agreement breaks the day a new person joins, the day somebody is in a hurry, or the day the company goes from three technical people to eight.
Governance is exactly that: turning agreements into controls that enforce themselves, and having a reliable record of everything that happens. It is the difference between "we trust nobody will do it" and "it cannot be done".
And there is a misunderstanding worth clearing up from the first line: governance is not something you put in when you are big. You put it in earlier, because retrofitting policies onto an organization with fifty projects and a thousand resources is a months-long job, while applying them to five projects is an afternoon. AlpinaShop is late, but not too late. That is the only reason this lesson is still feasible in a day's work.
By the end you will know how to structure an organization and understand the consequences of each criterion, apply organization policies that are inherited and really do block, inventory everything that exists and monitor changes in real time, enable and read the audit logs that matter, set up immutable retention in a project not even the administrators can write to, and deploy all of that in one go as a landing zone with Terraform.
And you will close module 7 and, with it, AlpinaShop's complete journey.
Contents
- What cloud governance is and why you need it sooner than you think
- The organization structure: criteria and consequences
- Organization policies: what they are and how they are inherited
- The policies AlpinaShop applies, one by one
- Dry-run mode and the danger of blocking the team
- Custom constraints and their relationship with Policy Controller
- Quotas and limits at the organization level
- Cloud Asset Inventory: knowing what exists
- Feeds: monitoring changes in real time
- Cloud Audit Logs properly
- The audit queries to have ready
- Immutable retention in a separate project
- Landing zones and the order that matters
- Change management: reviews, lifecycle and exceptions
- AlpinaShop's maturity, and what is left
- What cloud governance is and why you need it sooner than you think
Governance is the set of structures, policies and processes that ensure the use of the cloud is consistent with what the organization wants. It covers four questions:
| Question | Mechanism |
|---|---|
| What can be created, where and how? | Organization policies |
| What exists right now? | Cloud Asset Inventory |
| Who did what and when? | Cloud Audit Logs |
| Who pays for what? | Project structure and labels |
And the reason you need it before "being big", which is purely arithmetic:
| Moment | Projects | Resources | Cost of applying policies |
|---|---|---|---|
| Now (AlpinaShop) | 5 | ~120 | One day |
| In two years | 15 | ~600 | A week, with negotiations |
| In five years | 60 | ~5,000 | Months, and it probably will not get done |
The cost does not grow with the number of resources: it grows with the number of exceptions you have to negotiate. Applying the policy "no creating VMs with public IPs" today affects zero existing resources. Applying it in five years' time means finding the forty VMs that breach it, working out why, talking to six teams and accepting twenty permanent exceptions that hollow the policy out.
The rule: policies go in when they bother nobody. After that they never go in at all.
- The organization structure: criteria and consequences
The hierarchy from 01-04, revisited now with the full context:
flowchart TB
ORG["Organization<br/>alpinashop.example"]
ORG --> F1["Folder produccion"]
ORG --> F2["Folder desarrollo"]
ORG --> F3["Folder compartido"]
ORG --> F4["Folder seguridad"]
F1 --> P1["alpinashop-prod"]
F2 --> P2["alpinashop-dev"]
F3 --> P3["alpinashop-datos"]
F3 --> P4["alpinashop-cicd"]
F3 --> P5["alpinashop-red"]
F4 --> P6["alpinashop-auditoria"]
style F4 fill:#e6f4ea,stroke:#34a853
style P6 fill:#e6f4ea,stroke:#34a853
Two changes from 01-04: the alpinashop-red project from 07-03 lives in compartido, and a new folder appears, seguridad, with alpinashop-auditoria — the immutable log project from section 12. It is separated on purpose: it is the only one not even Marta can write to.
The three criteria for structuring
You rarely use only one; the usual thing is to combine two levels.
| Criterion | Structure | Advantage | Drawback |
|---|---|---|---|
| By environment | produccion / desarrollo / pruebas |
Very different policies per environment; clean separation of permissions | A team with several applications has them scattered |
| By team or unit | tienda / analitica / corporativo |
Direct billing per team; autonomy | Production and development mixed: the same policies for both |
| By application | One folder per product | Maximum isolation | An explosion of folders; duplication |
The consequences of the choice, which is what almost nobody thinks about before deciding:
| Aspect | Effect of the structure |
|---|---|
| Permissions | They are inherited downwards. A role on the produccion folder applies to all its projects |
| Organization policies | Inherited the same way. That is why separating environments allows harder policies in production |
| Billing | It is aggregated by folder and project. The structure is the cost breakdown |
| Quotas | They belong to the project, not to the folder. One project per application isolates quota exhaustion |
| Blast radius | The project is the deletion boundary. Deleting a project takes everything inside it |
AlpinaShop's choice is by environment at the first level, and it is the right one for its size for a specific reason: the difference in strictness between production and development is far greater than the difference between teams. Nobody is owner of production, but in development you can be editor without drama. With a team-based structure, that distinction would be impossible to express.
When AlpinaShop has four products, the natural evolution is a second level: produccion/tienda, produccion/alpinapro, desarrollo/tienda… The folder structure can be changed; moving projects between folders is one command. What cannot be changed is the projectId.
- Organization policies: what they are and how they are inherited
The Organization Policy Service is what turns agreements into controls.
| IAM | Organization policies | |
|---|---|---|
| Answers | Who can do what? | What can be done, regardless of who? |
| Subject | Identities | Resources and configurations |
| Example | "Marta can create VMs" | "Nobody can create a VM with a public IP" |
| Bypassed with | A higher role | Nothing. Not even an organization owner |
The last row is the essence: an organization policy is not a permission. Even if you have roles/owner on the project, if the policy forbids public IPs, you cannot create one. Only somebody with roles/orgpolicy.policyAdmin at the level where it is defined can lift it.
Types of constraint
| Type | What it does | Example |
|---|---|---|
| Boolean | Enables or disables a behaviour | compute.requireOsLogin, storage.uniformBucketLevelAccess |
| List | Allows or denies specific values | gcp.resourceLocations with the list of regions |
| Custom | Your own CEL over the resource's attributes | "VMs must be of type e2 or n2" |
Inheritance, which is where the subtlety lives
Policies are inherited from organization → folder → project, and at each level you can:
| Action | Effect |
|---|---|
| Inherit (default) | The parent's applies |
Merge (inheritFromParent: true) |
The parent's plus your own |
Replace (inheritFromParent: false) |
Only your own; the parent is ignored |
| Restore the default value | Removes the policy at that level |
flowchart TB
O["Organization<br/>regions: europe-west1, europe-southwest1"] --> F1["Folder produccion<br/>inherits"]
O --> F2["Folder desarrollo<br/>inherits + adds europe-west4"]
F1 --> P1["alpinashop-prod<br/>only europe-west1, europe-southwest1"]
F2 --> P2["alpinashop-dev<br/>3 regions"]
style F1 fill:#e6f4ea,stroke:#34a853
style F2 fill:#fef7e0,stroke:#fbbc04
And the rule that surprises everybody: for list constraints, a deny at any level always wins. A child cannot allow what the parent denies. It is what makes the model safe: you cannot escape a policy by going downwards.
Practical consequence: policies are defined at the highest level where they make sense, and relaxed downwards only with additional
allowentries on constraints the parent did not explicitly deny. If you find yourself needing to "un-deny" something, the policy was placed wrongly.
- The policies AlpinaShop applies, one by one
These are the nine, with their reason, their level and their risk.
4.1 Forbid public IPs on VMs
gcloud resource-manager org-policies enable-enforce \
constraints/compute.vmExternalIpAccess \
--organization=ORG_IDWith the exception declared, if some legitimate VM ever needed it:
# politica-ip-externa.yaml
constraint: constraints/compute.vmExternalIpAccess
listPolicy:
deniedValues:
- "under:organizations/ORG_ID"
# Explicit, documented exception, if one were needed:
# allowedValues:
# - "projects/alpinashop-dev/zones/europe-west1-b/instances/bastion-pruebas"Reason: at AlpinaShop there is no VM left in the serving path (07-02), and the development ones go out through Cloud NAT. A public IP is attack surface directly from the internet. Level: organization. Risk of blocking: low.
4.2 Restrict the regions to the EU
constraint: constraints/gcp.resourceLocations
listPolicy:
allowedValues:
- in:europe-west1-locations
- in:europe-southwest1-locations
- in:eu-locations # EU multi-region: BigQuery, bucketsReason: data residency in the EU (07-01, 07-04) and control of intercontinental traffic costs (07-05).
Level: organization. Risk: medium-high, and it is the policy that causes the most grief. Many services have global or multi-region components, and in:eu-locations is essential for multi-region BigQuery and some buckets to keep working. This one gets tested in dry-run, no arguments.
4.3 Disable service account key creation
gcloud resource-manager org-policies enable-enforce \
constraints/iam.disableServiceAccountKeyCreation \
--organization=ORG_IDReason: it is debt #1 from 07-04. With identity federation and impersonation already in use, there is no legitimate case that needs a downloadable key. Level: organization. Risk: medium. It can break old integrations — which is why the inventory in section 8 comes first and the policy afterwards.
And its companion, which stops an existing key being valid forever:
constraint: constraints/iam.serviceAccountKeyExpiryHours
listPolicy:
allowedValues:
- "24h" # if one is exceptionally allowed, it expires in 24 hours4.4 Require OS Login
gcloud resource-manager org-policies enable-enforce \
constraints/compute.requireOsLogin \
--organization=ORG_IDReason: OS Login ties SSH access to the Google identity instead of to loose SSH keys in the metadata. That means offboarding a person from the directory removes their access to every machine, which is exactly what does not happen with keys in metadata. Level: organization. Risk: low (there are hardly any VMs left).
4.5 Forbid public buckets
gcloud resource-manager org-policies enable-enforce \
constraints/storage.publicAccessPrevention \
--organization=ORG_IDReason: a public bucket is the commonest data leak in the cloud, across every provider.
Level: organization. Risk: low… with a trap to be resolved beforehand: the alpinashop-catalogo bucket serves images to the shop. If those images are served through the load balancer with bb-catalogo-imagenes (03-02), the bucket does not need to be public and the policy breaks nothing. If somebody were serving them by a direct Cloud Storage URL, it would. Checking is part of the preparatory work.
4.6 Uniform bucket-level access
gcloud resource-manager org-policies enable-enforce \
constraints/storage.uniformBucketLevelAccess \
--organization=ORG_IDReason: it disables per-object ACLs, which are a permission system running in parallel to IAM, invisible in audits and responsible for countless leaks. With uniform access, IAM is the only truth.
4.7 Require CMEK and limit the key projects
# Require a customer-managed key in the services that support it
constraint: constraints/gcp.restrictNonCmekServices
listPolicy:
deniedValues:
- bigquery.googleapis.com
- sqladmin.googleapis.com
- storage.googleapis.com# And the key comes ONLY from our keyring
constraint: constraints/gcp.restrictCmekCryptoKeyProjects
listPolicy:
allowedValues:
- projects/alpinashop-prod # where alpinashop-keyring livesReason: it closes off the control from 03-06. The second policy is the one almost nobody puts in and it prevents somebody encrypting data with a key from a project you do not control.
Level: the produccion folder. Risk: high. Requiring CMEK breaks resource creation if the key does not have the right permissions granted to the corresponding service agent. dry-run mandatory.
4.8 Prevent automatic grants to the default accounts
gcloud resource-manager org-policies enable-enforce \
constraints/iam.automaticIamGrantsForDefaultServiceAccounts \
--organization=ORG_IDReason: it is debt #2 from 07-04. It stops new projects being born with the default Compute account holding the Editor role. It does not fix existing projects, which have to be corrected by hand.
4.9 Restrict sharing by domain
constraint: constraints/iam.allowedPolicyMemberDomains
listPolicy:
allowedValues:
- "C03xxxxxx" # Cloud Identity customer ID of alpinashop.exampleReason: it prevents granting permissions to Google accounts outside the organization. It is the policy that avoids the mistake of giving [email protected] access "temporarily".
Risk: high. It breaks any legitimate collaboration with outsiders, and it breaks some Google service agents. It requires careful exceptions and a long dry-run.
Summary
| # | Constraint | Level | Risk | Debt it closes |
|---|---|---|---|---|
| 1 | compute.vmExternalIpAccess |
Organization | Low | — |
| 2 | gcp.resourceLocations |
Organization | Medium-high | EU residency |
| 3 | iam.disableServiceAccountKeyCreation |
Organization | Medium | 07-04 #1 |
| 4 | compute.requireOsLogin |
Organization | Low | — |
| 5 | storage.publicAccessPrevention |
Organization | Low | — |
| 6 | storage.uniformBucketLevelAccess |
Organization | Low | — |
| 7 | gcp.restrictNonCmekServices + key projects |
produccion folder |
High | Closes 03-06 |
| 8 | iam.automaticIamGrants... |
Organization | Low | 07-04 #2 |
| 9 | iam.allowedPolicyMemberDomains |
Organization | High | — |
- Dry-run mode and the danger of blocking the team
Organization policies support dry-run mode, just like Cloud Armor (03-05), Policy Controller (07-01) and VPC Service Controls (07-03). It is the same pattern for the fourth time in the course, and that is no coincidence: every control that denies must be measured before it is enforced.
# Apply in dry-run mode ONLY
gcloud org-policies set-policy politica-regiones.yaml --update-mask=dryRunSpec# politica-regiones.yaml
name: organizations/ORG_ID/policies/gcp.resourceLocations
dryRunSpec:
rules:
- values:
allowedValues:
- in:europe-west1-locations
- in:europe-southwest1-locations
- in:eu-locationsAnd the query for what it would have blocked:
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS who,
protopayload_auditlog.methodName AS operation,
protopayload_auditlog.resourceName AS resource_name,
protopayload_auditlog.metadata.dryRun AS in_dry_run,
protopayload_auditlog.metadata.constraint AS constraint_name
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_policy`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 14 DAY)
AND protopayload_auditlog.metadata.dryRun = TRUE
ORDER BY timestamp DESCThe safe procedure, and the order matters
| Step | What | Duration |
|---|---|---|
| 1 | Inventory what already breaches it (section 8) | 1 h |
| 2 | Fix or document every breach | Variable |
| 3 | Apply in dryRunSpec |
10 min |
| 4 | Wait 14 days, covering weekly and monthly processes | 14 days |
| 5 | Review the violations and decide on exceptions | 1 h |
| 6 | Enforce for real, starting with alpinashop-dev |
10 min |
| 7 | A week in development with no incidents | 7 days |
| 8 | Enforce in production | 10 min |
Step 1 is the one everybody skips and the one that avoids disaster. Applying dry-run to policies that are already being breached generates hundreds of violations nobody is going to analyse, and the practical result is that they all get ignored.
The five ways to lock yourself out
Real ones, all seen in production:
| Mistake | Consequence | Prevention |
|---|---|---|
allowedPolicyMemberDomains with no exceptions for Google's service agents |
Internal services stop working in inexplicable ways | A long dry-run; a list of agents |
resourceLocations without in:eu-locations |
Multi-region BigQuery and some buckets can no longer be created | Always include the multi-regions you use |
disableServiceAccountKeyCreation with old integrations still alive |
An external system stops authenticating with no warning | Inventory the keys first |
| CMEK required without granting the service agent permissions on the key | New resources cannot be created | Grant cloudkms.cryptoKeyEncrypterDecrypter first |
| A policy enforced on a Friday | A whole weekend unable to deploy | Enforce on a Tuesday or Wednesday morning |
And the safeguard to prepare before all of this: that at least two people have
roles/orgpolicy.policyAdminon the organization, with verified access. If the only person who can lift a policy locks themselves out, the way out is via Google support and a bad day.
- Custom constraints and their relationship with Policy Controller
When no predefined constraint fits, you write a custom constraint with CEL (Common Expression Language):
# restriccion-tipos-maquina.yaml
name: organizations/ORG_ID/customConstraints/custom.tiposMaquinaPermitidos
resourceTypes:
- compute.googleapis.com/Instance
methodTypes:
- CREATE
- UPDATE
condition: "resource.machineType.contains('e2-') || resource.machineType.contains('n2-')"
actionType: ALLOW
displayName: "Only the e2 and n2 machine families"
description: "Avoids expensive or specialised families without justification (07-05)."gcloud org-policies set-custom-constraint restriccion-tipos-maquina.yaml
gcloud resource-manager org-policies enable-enforce \
custom.tiposMaquinaPermitidos --organization=ORG_IDAnother example, this time for the mandatory labels from 07-05:
name: organizations/ORG_ID/customConstraints/custom.etiquetasObligatorias
resourceTypes:
- compute.googleapis.com/Instance
- storage.googleapis.com/Bucket
methodTypes: [CREATE]
condition: "has(resource.labels['centro-coste']) && has(resource.labels['entorno'])"
actionType: ALLOW
displayName: "centro-coste and entorno labels mandatory"Organization policies versus Policy Controller
Two systems that do similar things in different scopes. The confusion is common:
| Organization policies | Policy Controller (07-01) | |
|---|---|---|
| Scope | Google Cloud resources | Kubernetes resources |
| Where it acts | The Google Cloud API | The cluster's admission webhook |
| Language | CEL | Rego (OPA/Gatekeeper) |
| Reach | The whole organization | The fleet's clusters |
| Example | "No VM with a public IP" | "No privileged pod" |
| Cost | Included | GKE base tier / GKE Enterprise |
They do not compete: they complement each other. An organization policy cannot stop a pod running as root, and Policy Controller cannot stop a public bucket being created. An organization with Kubernetes needs both.
For AlpinaShop, which took the production workloads off GKE in 07-02, the organization policies are the ones that matter. Policy Controller remains as a control for the test cluster, which is consistent with what was decided in DA-004.
- Quotas and limits at the organization level
Quotas are managed per project, but they can be governed from above through policies and automation.
| Lever | What it does |
|---|---|
| Per-project quota adjustment | Lowering the vCPU quota in development (07-05) |
compute.quotaOverrides |
Restricting expensive machine families |
| BigQuery cost quotas | Bytes per user per day |
| API rate quotas | Containing runaway loops |
# Lower the CPU quota in development: a HARD brake on spend
gcloud alpha services quota update \
--service=compute.googleapis.com \
--consumer=projects/alpinashop-dev \
--metric=compute.googleapis.com/cpus \
--unit=1/{project}/{region} \
--dimensions=region=europe-west1 \
--value=8The essential distinction, already seen in 07-05 and put in its proper place here: budgets warn, quotas prevent. A governed organization needs both, and quotas are the only mechanism that does not depend on somebody reacting.
- Cloud Asset Inventory: knowing what exists
You cannot govern what you do not know exists. Cloud Asset Inventory is the catalogue of all the organization's resources, IAM policies and configurations, with their history.
Searching for resources
# Everything that exists in the organization
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--format="table(name, assetType, location, project)"
# VMs with a public IP: the inventory PRIOR to policy 4.1
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--asset-types=compute.googleapis.com/Instance \
--query="natIP:*" \
--format="table(name, project, location)"
# Resources outside Europe: the inventory prior to policy 4.2
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--query="NOT location:europe AND NOT location:eu AND NOT location:global" \
--format="table(name, assetType, location, project)"
# Resources WITHOUT a cost centre label (07-05)
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--query="NOT labels.centro-coste:*" \
--format="table(name, assetType, project)"Searching IAM policies
# Who has owner anywhere in the organization
gcloud asset search-all-iam-policies --scope=organizations/ORG_ID \
--query="policy:roles/owner" \
--format="table(resource, policy.bindings.members)"
# Anything granted to an external account
gcloud asset search-all-iam-policies --scope=organizations/ORG_ID \
--query="policy:gmail.com" \
--format="table(resource, policy.bindings.role, policy.bindings.members)"These two queries, run before applying policies 4.1, 4.2 and 4.9, are exactly step 1 of the procedure in section 5. Without them, you are applying blind.
Exporting to BigQuery
gcloud asset export --organization=ORG_ID \
--content-type=resource \
--bigquery-table=projects/alpinashop-auditoria/datasets/inventario/tables/recursos \
--output-bigquery-force
gcloud asset export --organization=ORG_ID \
--content-type=iam-policy \
--bigquery-table=projects/alpinashop-auditoria/datasets/inventario/tables/politicas_iam \
--output-bigquery-forceWith a scheduled daily export (Cloud Scheduler + Workflows, 04-06), you get something very valuable: the history of the inventory, which lets you answer "what was there on 3 June?" — a question that comes up in every investigation and every audit.
-- Resources created in the last 7 days, by type and project
SELECT
asset_type,
SPLIT(name, '/')[SAFE_OFFSET(4)] AS project_id,
COUNT(*) AS created
FROM `alpinashop-auditoria.inventario.recursos`
WHERE DATE(update_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
GROUP BY asset_type, project_id
ORDER BY created DESC
- Feeds: monitoring changes in real time
The inventory tells you what is there. Feeds tell you when something changes, as it happens.
# 1. Topic the notifications will arrive on
gcloud pubsub topics create cambios-criticos --project=alpinashop-auditoria
# 2. Feed on IAM policy changes across the WHOLE organization
gcloud asset feeds create feed-iam \
--organization=ORG_ID \
--content-type=iam-policy \
--asset-types="cloudresourcemanager.googleapis.com/Project,cloudresourcemanager.googleapis.com/Folder" \
--pubsub-topic=projects/alpinashop-auditoria/topics/cambios-criticos
# 3. Feed on the creation or change of sensitive resources
gcloud asset feeds create feed-recursos-sensibles \
--organization=ORG_ID \
--content-type=resource \
--asset-types="compute.googleapis.com/Firewall,storage.googleapis.com/Bucket,iam.googleapis.com/ServiceAccountKey" \
--pubsub-topic=projects/alpinashop-auditoria/topics/cambios-criticosAnd the function that decides what deserves an immediate warning (Cloud Functions 2nd gen, 06-03):
# main.py — evaluar-cambio-critico
import base64, json, logging
CRITICOS = [
("roles/owner", "OWNER has been granted"),
("roles/editor", "EDITOR has been granted"),
("allUsers", "PUBLIC ACCESS granted"),
("allAuthenticatedUsers", "SEMI-PUBLIC ACCESS granted"),
("roles/iam.securityAdmin", "SECURITY ADMIN has been granted"),
]
def evaluar_cambio(evento, contexto):
datos = json.loads(base64.b64decode(evento["data"]).decode("utf-8"))
activo = datos.get("asset", {})
nombre = activo.get("name", "")
tipo = activo.get("assetType", "")
# 1. Creation of a service account key: ALWAYS exceptional (07-04)
if tipo == "iam.googleapis.com/ServiceAccountKey":
alertar("CRITICAL", "Service account key created", nombre, datos)
return
# 2. Dangerous grants
texto = json.dumps(activo.get("iamPolicy", {}))
for patron, mensaje in CRITICOS:
if patron in texto:
alertar("CRITICAL", mensaje, nombre, datos)
return
# 3. Firewall rule open to the whole internet on an administration port
if tipo == "compute.googleapis.com/Firewall":
r = activo.get("resource", {}).get("data", {})
rangos = r.get("sourceRanges", [])
if "0.0.0.0/0" in rangos and r.get("direction") == "INGRESS":
for permitido in r.get("allowed", []):
puertos = permitido.get("ports", [])
if any(p in ("22", "3389", "3306", "5432") for p in puertos):
alertar("CRITICAL", "Firewall open to the internet on a sensitive port",
nombre, datos)
return
def alertar(gravedad, mensaje, recurso, datos):
logging.log_struct({
"severity": gravedad,
"message": mensaje,
"recurso": recurso,
"momento": datos.get("window", {}).get("startTime"),
"alerta_gobierno": True,
})With a Cloud Monitoring alert on jsonPayload.alerta_gobierno=true and severity CRITICAL, those events reach the security channel within seconds.
This closes debt #4 from 07-04 — alerts on IAM changes and key creation — and it is the difference between finding out about a dangerous change as it happens or discovering it at the quarterly review, six weeks later.
- Cloud Audit Logs properly
They have been mentioned in 03-04, 06-06 and 07-04. Here, in full.
| Type | What it records | By default? | Cost | Can it be disabled |
|---|---|---|---|---|
| Admin activity | Configuration and permission changes | Yes | Free | No |
| Data access | Data reads and writes | No (except BigQuery) | Paid, and it can be a lot | Yes |
| System events | Google's actions on your resources | Yes | Free | No |
| Policy denials | Requests rejected by policies | Yes | Free | No |
The two facts to retain:
- Admin activity is free and cannot be disabled. Not even an attacker with organization-level permissions can erase the evidence of their configuration changes. It is the foundation of every investigation.
- Data access is disabled by default. And without it, as the 07-04 exercise demonstrated, you cannot know who read the customers' data — which turns a leaked credential into a breach of indeterminable scope.
Enabling data access where it matters
You do it with judgement, because the volume can be enormous:
# politica-auditoria.yaml — applied to the produccion FOLDER
auditConfigs:
# BigQuery: reads and writes of customer data
- service: bigquery.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
# Cloud Storage: writes and administration only.
# DATA_READ on a bucket serving images would generate millions of entries.
- service: storage.googleapis.com
auditLogConfigs:
- logType: DATA_WRITE
- logType: ADMIN_READ
# Secret Manager: EVERYTHING. Every access to a secret is relevant
- service: secretmanager.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
# Cloud KMS: every use of a key
- service: cloudkms.googleapis.com
auditLogConfigs:
- logType: DATA_READ
- logType: DATA_WRITE
# IAM: identity changes
- service: iam.googleapis.com
auditLogConfigs:
- logType: DATA_WRITEThe three decisions that make this affordable, and that turn an unacceptable cost into around €35 a month (07-05):
- Cloud Storage without
DATA_READ: the catalogue bucket serves millions of images; recording every read would cost more than the whole platform. Writes and administrative reads are recorded, which is where the risk is. - Secret Manager with everything: there are few accesses and every one matters.
- Only in the
produccionfolder: there is no real data in development.
And an essential exclusion so the volume does not explode:
gcloud logging sinks update _Default \
--add-exclusion=name=ruido-lecturas-repetitivas,\
filter='protoPayload.methodName="google.storage.objects.get"
AND protoPayload.authenticationInfo.principalEmail=~"^service-.*gserviceaccount.com$"'This excludes the reads made by Google's own service agents, which are pure noise and extremely high volume.
How to read an entry
{
"protoPayload": {
"@type": "type.googleapis.com/google.cloud.audit.AuditLog",
"authenticationInfo": {
"principalEmail": "[email protected]",
"serviceAccountDelegationInfo": [
{"firstPartyPrincipal": {"principalEmail": "sa-deploy-prod@..."}}
]
},
"requestMetadata": {
"callerIp": "88.20.145.33",
"callerSuppliedUserAgent": "google-cloud-sdk gcloud/latest"
},
"serviceName": "cloudresourcemanager.googleapis.com",
"methodName": "SetIamPolicy",
"authorizationInfo": [
{"resource": "projects/alpinashop-prod",
"permission": "resourcemanager.projects.setIamPolicy",
"granted": true}
],
"resourceName": "projects/alpinashop-prod",
"serviceData": {"policyDelta": {"bindingDeltas": [
{"action": "ADD", "role": "roles/owner", "member": "user:[email protected]"}
]}}
},
"severity": "NOTICE",
"timestamp": "2026-08-04T18:23:11.004Z"
}The fields you have to know how to read, in order of importance:
| Field | What it tells you |
|---|---|
authenticationInfo.principalEmail |
Who |
serviceAccountDelegationInfo |
Who really, if there was impersonation (03-04) |
methodName |
What |
resourceName |
On what |
requestMetadata.callerIp |
From where |
authorizationInfo.granted |
Whether it was allowed or denied |
serviceData.policyDelta |
The exact change, on IAM changes |
timestamp |
When |
serviceAccountDelegationInfo is the field almost nobody looks at and the one that matters most in an organization that uses impersonation: without it, a change made by Marta impersonating sa-deploy-prod appears attributed to the service account, and the trail back to the person is lost.
- The audit queries to have ready
Written and saved as views before you need them. An incident is no time to learn SQL.
1. Who changed an IAM policy?
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS who,
protopayload_auditlog.resourceName AS resource_name,
delta.action AS action,
delta.role AS role,
delta.member AS member,
protopayload_auditlog.requestMetadata.callerIp AS ip
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`,
UNNEST(JSON_QUERY_ARRAY(protopayload_auditlog.servicedata_v1_iam.policyDelta.bindingDeltas)) AS d,
UNNEST([STRUCT(
JSON_VALUE(d, '$.action') AS action,
JSON_VALUE(d, '$.role') AS role,
JSON_VALUE(d, '$.member') AS member)]) AS delta
WHERE protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
ORDER BY timestamp DESC2. Who accessed customer data?
SELECT
DATE(timestamp) AS day,
protopayload_auditlog.authenticationInfo.principalEmail AS who,
protopayload_auditlog.resourceName AS table_name,
COUNT(*) AS accesses
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_data_access`
WHERE protopayload_auditlog.serviceName = 'bigquery.googleapis.com'
AND protopayload_auditlog.resourceName LIKE '%pedidos%'
AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY day, who, table_name
ORDER BY day DESC, accesses DESC3. Who switched off or modified a log?
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS who,
protopayload_auditlog.methodName AS operation,
protopayload_auditlog.resourceName AS resource_name
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE (protopayload_auditlog.methodName LIKE '%UpdateSink%'
OR protopayload_auditlog.methodName LIKE '%DeleteSink%'
OR protopayload_auditlog.methodName LIKE '%UpdateBucket%'
OR protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
AND protopayload_auditlog.serviceName = 'logging.googleapis.com'
OR protopayload_auditlog.methodName LIKE '%UpdateExclusion%')
AND DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 90 DAY)
ORDER BY timestamp DESCThis third one is the most important of the three, and the one fewest people have. Tampering with the record is the first thing a competent attacker does and the first thing somebody who wants to hide a mistake does. It must have a permanent alert.
4. Out-of-hours activity.
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS who,
protopayload_auditlog.methodName AS operation,
protopayload_auditlog.requestMetadata.callerIp AS ip
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY)
AND NOT REGEXP_CONTAINS(
protopayload_auditlog.authenticationInfo.principalEmail, r'^(service-|.*gserviceaccount)')
AND (EXTRACT(HOUR FROM timestamp AT TIME ZONE 'Europe/Madrid') NOT BETWEEN 7 AND 21
OR EXTRACT(DAYOFWEEK FROM timestamp AT TIME ZONE 'Europe/Madrid') IN (1, 7))
ORDER BY timestamp DESCThe filter that excludes service accounts is essential: software works at night, and without that filter the query returns thousands of irrelevant lines.
5. Policy denials: what somebody tried and could not do.
SELECT
DATE(timestamp) AS day,
protopayload_auditlog.authenticationInfo.principalEmail AS who,
protopayload_auditlog.methodName AS operation,
protopayload_auditlog.status.message AS reason,
COUNT(*) AS attempts
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_policy`
WHERE DATE(timestamp) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY day, who, operation, reason
ORDER BY attempts DESCThis query has a dual use worth understanding: it detects malicious attempts, and it also detects badly calibrated policies getting in the team's way. If Dani turns up twenty times trying to do something legitimate, the problem is the policy, not Dani.
- Immutable retention in a separate project
Here is the piece that makes the auditing credible.
A log the administrator can delete is no use as evidence.
If Marta has permission to modify the audit logs, then faced with any investigation — internal, from a customer or from the AEPD — the answer to "could somebody have altered this?" is "yes". And that empties the whole record of value.
The solution is a separate project with permissions not even the administrators have.
flowchart LR
subgraph ORG["Organization"]
P1["alpinashop-prod"]
P2["alpinashop-dev"]
P3["alpinashop-datos"]
P4["alpinashop-red"]
end
SINK["Aggregated sink<br/>at ORGANIZATION level"]
P1 & P2 & P3 & P4 --> SINK
SINK --> B["Log bucket<br/>alpinashop-auditoria<br/>retention LOCKED 400 days"]
SINK --> BQ["BigQuery<br/>audit queries"]
SINK --> GCS["GCS bucket<br/>retention lock 7 years"]
style B fill:#e6f4ea,stroke:#34a853
style GCS fill:#e6f4ea,stroke:#34a853
Step 1: the project and its permissions
gcloud projects create alpinashop-auditoria --folder=FOLDER_SEGURIDAD_ID
gcloud services enable logging.googleapis.com bigquery.googleapis.com storage.googleapis.com \
--project=alpinashop-auditoriaAnd the split of permissions, which is the key to everything:
| Identity | Role | Can |
|---|---|---|
gcp-seguridad@ |
roles/logging.viewer, roles/bigquery.dataViewer |
Read, not write or delete |
gcp-infra@ (Marta) |
roles/logging.viewer |
Read only |
| An external auditor | Scoped, temporary roles/logging.viewer |
Read during the audit |
| Nobody | roles/logging.admin over the sinks |
— |
Marta, who administers the whole platform, can only read here. It is the separation of duties from 07-04 taken to its conclusion: whoever administers does not keep the evidence of what they administer.
Step 2: the aggregated sink at organization level
gcloud logging sinks create sumidero-auditoria-org \
logging.googleapis.com/projects/alpinashop-auditoria/locations/eu/buckets/auditoria \
--organization=ORG_ID \
--include-children \
--log-filter='logName:"cloudaudit.googleapis.com"'--include-children is what makes it aggregated: it collects the logs of all the organization's projects and folders, including the ones created in the future. Without that flag, every new project would be left out and nobody would notice.
Then you have to grant the sink's writer permission on the destination:
ESCRITOR=$(gcloud logging sinks describe sumidero-auditoria-org \
--organization=ORG_ID --format='value(writerIdentity)')
gcloud projects add-iam-policy-binding alpinashop-auditoria \
--member="$ESCRITOR" --role=roles/logging.bucketWriterThis step gets forgotten constantly and the symptom is baffling: the sink exists, it shows up in the console, and nothing arrives. With no errors.
Step 3: locked retention
# Log bucket with 400-day retention and a LOCK
gcloud logging buckets create auditoria \
--location=eu --retention-days=400 \
--project=alpinashop-auditoria
# THE IRREVERSIBLE STEP
gcloud logging buckets update auditoria \
--location=eu --locked \
--project=alpinashop-auditoria⚠️
--lockedis irreversible. From that moment on nobody, not Google, not the organization owner, not support, can reduce the retention or delete the bucket before it expires. That is exactly the point: the guarantee is that not even you can.
And for long-term preservation, with Cloud Storage retention lock:
gcloud storage buckets create gs://alpinashop-auditoria-archivo \
--location=EU --uniform-bucket-level-access \
--project=alpinashop-auditoria
gcloud storage buckets update gs://alpinashop-auditoria-archivo \
--retention-period=7y
# FINAL LOCK: no object can be deleted for 7 years
gcloud storage buckets update gs://alpinashop-auditoria-archivo --lock-retention-periodThis is what is known as WORM storage (Write Once, Read Many), and it is what many compliance frameworks require. It is also, by the way, the best defence against ransomware there is: an attacker with full permissions cannot encrypt or delete what is under a retention lock.
With a counterpart to be accepted consciously: if you lock 7 years, you pay for 7 years of storage, no exceptions. Before locking, you calculate the volume and check that the number makes sense.
- Landing zones and the order that matters
A landing zone is the organization prepared before the first workload arrives: hierarchy, policies, network, identity, auditing and billing, all deployed as code.
AlpinaShop has done it the other way round, like almost everybody: workloads first, governance afterwards. It has worked because it is small. The correct way, and the one to know for your next project, is this.
The deployment order, and why it matters
flowchart TB
A["1. Organization and Cloud Identity<br/>groups, MFA, domain"] --> B["2. Folder hierarchy"]
B --> C["3. Billing<br/>account, export, budgets"]
C --> D["4. Organization policies<br/>in DRY-RUN"]
D --> E["5. Audit project<br/>aggregated sink + retention"]
E --> F["6. Network project<br/>Shared VPC"]
F --> G["7. IAM: groups and roles"]
G --> H["8. Workload projects"]
H --> I["9. Policies in ENFORCED mode"]
I --> J["10. Workloads"]
style E fill:#e6f4ea,stroke:#34a853
style D fill:#fef7e0,stroke:#fbbc04
style I fill:#fef7e0,stroke:#fbbc04
The four reasons for this particular order and not another:
- Auditing (5) comes before the network and the workloads. If the sink is created afterwards, the logs of everything done in the meantime are not captured. Building the platform is precisely the period with the most permission changes, and it is the one you most want on the record.
- Policies in
dry-run(4) come before the workload projects (8). That way they are measured against resources as they are created, and violations get fixed as you go instead of piling up. - The network (6) before the service projects (8). A service project needs the host to exist.
- Enforced policies (9) before the workloads (10). It is the only moment when enforcing a policy breaks nothing, because there is nothing to break.
In Terraform
# modules/landing-zone/main.tf — the module structure
module "carpetas" {
source = "./modulos/carpetas"
organizacion = var.org_id
carpetas = ["produccion", "desarrollo", "compartido", "seguridad"]
}
module "auditoria" {
source = "./modulos/auditoria"
depends_on = [module.carpetas]
org_id = var.org_id
carpeta_seguridad = module.carpetas.ids["seguridad"]
retencion_dias = 400
bloquear_retencion = true # IRREVERSIBLE: done by hand the first time
}
module "politicas" {
source = "./modulos/politicas-organizacion"
depends_on = [module.carpetas]
org_id = var.org_id
dry_run = var.politicas_en_pruebas # true at first, false once measured
regiones_permitidas = ["in:europe-west1-locations",
"in:europe-southwest1-locations",
"in:eu-locations"]
}
module "red" {
source = "./modulos/vpc-compartida"
depends_on = [module.politicas]
carpeta_compartido = module.carpetas.ids["compartido"]
proyecto_host = "alpinashop-red"
subredes = var.subredes
}
module "proyectos" {
source = "./modulos/proyectos"
depends_on = [module.red]
for_each = var.proyectos
# ...
}Explicit depends_on on every module, even though there are no references between them. It is the legitimate case for depends_on that 06-07 flagged as the exception: the order here does not come from data dependencies, but from a logical sequence Terraform cannot infer.
Google publishes the foundation blueprints (Cloud Foundation Fabric, Terraform Example Foundation), which implement all of this thoughtfully. They deserve an honest warning: they are designed for large organizations and bring far more structure than a small business needs. Use them as a reference and as a source of well-thought-out decisions, not as a template to copy.
What it would cost to do it today at AlpinaShop
| Phase | Work | Duration |
|---|---|---|
| Inventory the breaches | The queries from section 8 | 2 h |
Create alpinashop-auditoria and the sink |
Terraform | 4 h |
Policies in dry-run |
Terraform | 3 h |
| Wait and analyse | — | 14 days |
| Fix breaches and exceptions | Variable | 1-2 days |
| Enforce in development | — | 1 h |
| Wait | — | 7 days |
| Enforce in production | — | 1 h |
| Feeds and alerts | Terraform + a function | 4 h |
| Total effective work | ~4 days | |
| Total elapsed time | ~4 weeks |
Four days of work spread over four weeks. That is the price of governing the platform, and it explains why the arithmetic in section 1 matters: in two years' time it would be four weeks of work and six months of elapsed time.
- Change management: reviews, lifecycle and exceptions
Technical policies are not enough. You need processes, and for a team of three they have to be light or they will not happen.
Periodic access review
| Frequency | What is reviewed | Who | Duration |
|---|---|---|---|
| Monthly | New service accounts; keys created; basic roles | Marta | 30 min |
| Quarterly | All permissions against the actual functions; Recommender recommendations | Marta + Dani | 2 h |
| Half-yearly | External accounts; policy exceptions; third-party access | Marta + management | 1 h |
| When somebody changes role or leaves | All their access | Immediately | — |
#!/usr/bin/env bash
# revision-trimestral.sh — generates the report for the meeting
echo "=== 1. Basic roles in the organization ==="
gcloud asset search-all-iam-policies --scope=organizations/$ORG_ID \
--query="policy:(roles/owner OR roles/editor)" \
--format="table(resource, policy.bindings.role, policy.bindings.members)"
echo "=== 2. External accounts ==="
gcloud asset search-all-iam-policies --scope=organizations/$ORG_ID \
--query="policy:(gmail.com OR hotmail.com OR outlook.com)" \
--format="table(resource, policy.bindings.members)"
echo "=== 3. Permissions granted and unused for 90 days ==="
for P in $PROYECTOS; do
gcloud recommender recommendations list --project="$P" --location=global \
--recommender=google.iam.policy.Recommender \
--format="table(content.overview.member, content.overview.removedRole)"
done
echo "=== 4. Service account keys ==="
gcloud asset search-all-resources --scope=organizations/$ORG_ID \
--asset-types=iam.googleapis.com/ServiceAccountKey \
--format="table(name, createTime)"
echo "=== 5. Policy exceptions in force ==="
gcloud org-policies list --organization=$ORG_ID --format="table(name, spec.rules)"Project lifecycle
| Phase | What is required |
|---|---|
| Request | Purpose, owner, environment, cost centre, review date |
| Creation | Always via Terraform, never from the console. Mandatory labels, a budget, the correct folder |
| Operation | Quarterly review of cost and permissions |
| Expiry | Every test project has an expiry date |
| Retirement | Export the data → unlink billing → wait 30 days → delete |
The mandatory expiry date on test projects is the measure that avoids the most phantom spend (07-05), because it attacks the cause rather than the symptom. It is implemented with a caduca=2026-12-31 label and a weekly job that flags the expired ones.
And the 30 days of waiting before deleting are not bureaucracy: deleting a project is irreversible once the grace period is over, and somebody always turns up who needed something from there.
The exceptions process
Every policy will end up having exceptions. Without a process, they get granted verbally and become permanent.
| Element | Rule |
|---|---|
| Request | In writing, with a technical reason and the alternatives ruled out |
| Approval | Marta + one more person. Never just one |
| Scope | The smallest possible: one resource, not a project |
| Expiry | Mandatory. 90 days maximum, renewable with justification |
| Record | In alpinashop-infra/EXCEPCIONES.md, with Terraform |
| Review | Half-yearly: is it still needed? |
# EXCEPCIONES.md
## EXC-001 — Public IP on `bastion-migracion`
- **Policy:** `constraints/compute.vmExternalIpAccess`
- **Scope:** `projects/alpinashop-dev/zones/europe-west1-b/instances/bastion-migracion`
- **Reason:** the ERP vendor requires an inbound connection from its IP for the
data migration; it does not support outbound connections or PSC.
- **Alternatives ruled out:** IAP (the vendor cannot use the tunnel),
VPN (outside the project's timescale).
- **Mitigation:** firewall restricted to the vendor's IP; the VM is switched off
outside working sessions; connection logging enabled.
- **Requested by:** Dani · **Approved by:** Marta + management
- **Granted:** 2026-08-05 · **EXPIRES: 2026-11-03**
- **Review:** when the migration finishes, the VM and the exception are removedThe expiry date is what separates an exception from a repeal. Without it, the policy is hollowed out forever and nobody remembers why.
- AlpinaShop's maturity, and what is left
Where it stands
| Domain | Status | Comment |
|---|---|---|
| Organizational structure | 🟢 Good | Hierarchy by environment, projects with a clear purpose, a separate security folder |
| Identity | 🟢 Good | Groups, no basic roles, no keys, federation, nobody owner of production |
| Network | 🟢 Good | Shared VPC, permissions per subnet, hybrid resolved, data perimeter |
| Compute | 🟢 Good | No servers to maintain, scaling to zero, canary and rollback in seconds |
| Data | 🟡 Acceptable | Classified, encrypted, with retention; the right to erasure still needs testing |
| CI/CD | 🟢 Good | Trunk-based, keyless, manual approval, verified canary |
| Observability | 🟢 Good | Metrics, logs, traces, SLOs, error budget, burn rate alerts |
| Reliability | 🟡 Acceptable | Regional HA, DR with RTO/RPO, restore tested; multi-region still missing |
| Cost | 🟢 Good | Visibility, allocation, budgets, a monthly routine, −60 % |
| Security | 🟡 Acceptable | Defence in depth; the priority debt paid; active detection still missing |
| Governance | 🟡 Under way | This lesson: policies, inventory, immutable auditing |
| Compliance | 🟠 Needs work | Documentation out of date; professional validation still missing |
What is left, honestly
Short term (this quarter):
- Finish enforcing the organization policies. They are in
dry-run; the 4 weeks from section 13 have to be got through. - Updated compliance documentation, with professional advice. It is the most backward box and the only one with legal consequences.
- Test the right to erasure procedure end to end, with a test customer.
Medium term (this year):
- Multi-region for Cloud SQL, which is the only thing preventing complete regional high availability (07-06).
- Active threat detection, once there is somebody who can deal with it (07-04).
- Temporary privilege elevation with PAM.
- An external security review over the already-hardened architecture.
What it is NOT going to do, and that is fine:
| Will not do | Why |
|---|---|
| GKE Enterprise / Google Distributed Cloud | DA-004: there are no workloads outside GCP to manage |
| A service mesh | One service. There is nothing to mesh |
| Multicloud | There is no business reason |
| Active-active | The cost is not justified with 1,200 orders/month (07-06) |
| A dedicated security team | Forty people |
| Network Connectivity Center | One VPC and one link |
That last table is the most valuable in the lesson, because a mature organization is not the one that has adopted the most technology: it is the one that can justify what it has decided not to adopt. All six rows have their decision written down, their reason, and — in the case of DA-004 — their review conditions.
The complete journey
It is worth looking back once.
AlpinaShop started module 1 with no Google Cloud account, with a shop running somewhere and three people who did not know what a region was. It finished module 2 with the application migrated and a decision outstanding. Module 3 gave it network, load balancing, identity and secrets. Module 4, a data platform. Module 5, models that predict. Module 6, continuous delivery and observability. And module 7 has resolved what was left: the hybrid question, the definitive compute, the advanced network, the reviewed security, the understood bill, the measured reliability and the applied governance.
Today AlpinaShop has a platform that one person can operate, anybody can understand by reading a repository, and can be rebuilt in its entirety with terraform apply. It is not perfect — it has a written, prioritised list of debts, which is exactly what it should have — but it is defensible in front of a customer, in front of an auditor and in front of the person who joins next month.
Common Mistakes and Tips
- Leaving governance until "when we are big". The cost grows with the exceptions you have to negotiate, not with the number of resources. It happens now or it does not happen.
- Applying policies without first inventorying what breaches them. You generate hundreds of violations nobody analyses and the
dry-runstops being useful. - Forgetting
in:eu-locationsinresourceLocations. Multi-region BigQuery and some buckets can no longer be created. allowedPolicyMemberDomainswith no exceptions for Google's service agents. Internal services fail in inexplicable ways.- Requiring CMEK without granting the service agent permissions on the key. New resources cannot be created.
- Enforcing policies on a Friday. A weekend unable to deploy.
- Only one person having
orgpolicy.policyAdmin. If they lock themselves out, the way out is Google support. - A sink without
--include-children. New projects are left out and nobody notices. - Forgetting to give
logging.bucketWriterto the sink'swriterIdentity. The sink exists and nothing arrives, with no errors. - Creating the audit project last. The logs from the build phase, which has the most changes, are lost.
- Letting the administrator write to the audit logs. Then they are no use as evidence.
- Locking the retention without calculating the volume.
--lockedis irreversible and you pay for every year you set. - Enabling
DATA_READon a bucket that serves images. Millions of entries and an absurd bill. - Not looking at
serviceAccountDelegationInfo. In an organization with impersonation, without that field the trail back to the person is lost. - Exceptions with no expiry date. They stop being exceptions and hollow out the policy.
- Copying a foundation blueprint as is. They are built for large organizations and bring structure you do not need.
- Tip: the "who touched a log" query deserves a permanent alert. It is the first thing tampered with by anybody wanting to hide something.
- Tip: give test projects a mandatory expiry date. It attacks the cause of phantom spend, not the symptom.
- Tip: use policy denials as a thermometer. If somebody on the team turns up twenty times, the policy is badly calibrated.
- Tip: write down what you decide NOT to adopt. It is what distinguishes a mature organization from one that simply never got there.
Exercises
Exercise 1 — Designing the policy set for a new company
AlpinaTech, a 25-person consultancy, is starting on Google Cloud from scratch. The facts:
- Three teams: development (12), data (5), infrastructure (3). The rest are non-technical.
- They work for external customers, and some require that their data does not leave the EU.
- Two freelance consultants with Gmail accounts collaborate on specific projects.
- One customer requires ISO 27001 certification within 18 months.
- Budget: €2,500/month of cloud.
Design: the folder and project hierarchy with its criterion justified, the complete set of organization policies with their level and their risk, how you resolve the freelancers' case without disabling allowedPolicyMemberDomains, and the deployment order of the landing zone.
Exercise 2 — Investigating a suspicious change
On a Monday morning, the monthly audit query reveals this:
timestamp quien operacion recurso 2026-08-01T23:47:12Z [email protected]... SetIamPolicy projects/alpinashop-prod 2026-08-01T23:47:44Z [email protected]... CreateServiceAccount projects/alpinashop-prod 2026-08-01T23:48:03Z [email protected]... CreateServiceAccountKey projects/alpinashop-prod 2026-08-01T23:51:20Z [email protected]... storage.buckets.update alpinashop-datalake
It was Saturday night. There was no scheduled deployment. The sa-mantenimiento account does not appear in AlpinaShop's Terraform.
Write the complete investigation: which queries you run and in what order, what you are looking for in each one, how you determine whether it was an attack or an undocumented legitimate automation, what you contain and in what order, and which governance controls would have detected or prevented this earlier.
Exercise 3 — Justifying governance to management
AlpinaShop's management asks why four days should be spent "putting in rules" when the system works perfectly and there has not been a single incident.
Write Marta's answer: what specific risk each block of work covers, what would happen without it with real examples from the course, what it costs to do it now against in two years' time, and what you ask for besides the time. One page maximum, in non-technical language.
Solutions
Solution 1 — Designing AlpinaTech's policies
The hierarchy, and the criterion that decides it.
The determining factor is not the size or the teams: it is that they work for external customers with different requirements. That makes isolation by customer more important than isolation by environment.
Organizacion alpinatech.example
├── carpeta: clientes
│ ├── carpeta: cliente-acme
│ │ ├── alpinatech-acme-prod
│ │ └── alpinatech-acme-dev
│ └── carpeta: cliente-beta
│ ├── alpinatech-beta-prod
│ └── alpinatech-beta-dev
├── carpeta: interno
│ ├── alpinatech-web-corporativa
│ └── alpinatech-herramientas
├── carpeta: compartido
│ ├── alpinatech-red
│ └── alpinatech-cicd
└── carpeta: seguridad
└── alpinatech-auditoriaWhy a folder per customer with environments inside:
| Advantage | Detail |
|---|---|
| Contractual isolation | Acme's data cannot be mixed with Beta's, not even by accident |
| Policies per customer | If Acme requires EU only and Beta does not, it is applied in each one's folder |
| Direct billing | The cost per folder is what is invoiced to the customer |
| Clean exit | When the contract ends, the whole folder is deleted |
| Permissions per project | Only the team assigned to Acme has access to Acme |
The last row matters most with freelancers involved, and it links to the next point.
The policy set.
| # | Constraint | Level | Risk | Reason |
|---|---|---|---|---|
| 1 | iam.allowedPolicyMemberDomains |
Organization | High | They work with outsiders: it is the most important and the most delicate |
| 2 | gcp.resourceLocations = EU |
clientes folder |
Medium-high | A contractual requirement; in interno it can be laxer |
| 3 | iam.disableServiceAccountKeyCreation |
Organization | Medium | The basis of ISO 27001 |
| 4 | storage.publicAccessPrevention |
Organization | Low | Customer data |
| 5 | storage.uniformBucketLevelAccess |
Organization | Low | IAM as the only truth |
| 6 | compute.vmExternalIpAccess |
Organization | Low | — |
| 7 | compute.requireOsLogin |
Organization | Low | A freelancer leaving = losing access to everything |
| 8 | iam.automaticIamGrantsForDefaultServiceAccounts |
Organization | Low | From day 1: it will never need fixing |
| 9 | sql.restrictPublicIp |
Organization | Low | — |
| 10 | compute.disableSerialPortAccess |
Organization | Low | Avoids an unaudited access path |
| 11 | Custom: cliente and centro-coste labels mandatory |
Organization | Medium | Customer billing depends on it |
| 12 | gcp.restrictNonCmekServices |
clientes folder |
High | If any customer requires it |
Note policy 2 at the clientes folder level and not at the organization. Applying it at the organization would force the corporate website and the internal tools to be in the EU, which is probably fine but is a restriction that is not needed. Policies go at the highest level where they make sense, not at the highest level possible.
The freelancers' case — the interesting part of the exercise.
Disabling allowedPolicyMemberDomains to give access to two Gmail accounts would be throwing away the most valuable policy in the set. There are four options, and the last one is the right one:
| Option | Assessment |
|---|---|
| Not applying the policy | ❌ Anybody can give access to any Google account in the world |
| A permanent exception for their two Gmail addresses | ❌ Personal accounts outside the company's control: no mandatory MFA, no centralised offboarding, no ability to revoke the mailbox |
| An exception in the specific customer's folder | ⚠️ Better, but it still rests on personal accounts |
| Cloud Identity accounts of their own for the freelancers | ✅ The correct one |
The solution: [email protected] managed by the company, with mandatory MFA, an IAM condition with an expiry date (03-04) matching the end of the contract, and permissions only on the project they work on.
resource "google_project_iam_member" "freelance_acme" {
project = "alpinatech-acme-dev"
role = "roles/editor"
member = "user:[email protected]"
condition {
title = "Acme contract until 2026-12-31"
expression = "request.time < timestamp('2027-01-01T00:00:00Z')"
}
}The access expires by itself. It does not depend on anybody remembering to revoke it on 31 December, which is exactly the kind of task that gets forgotten. And the cost of a Cloud Identity licence is trivial compared with the risk of a consultant keeping access to a customer's data for two years after finishing.
Deployment order.
- Cloud Identity, domain, groups (
at-desarrollo@,at-datos@,at-infra@,at-seguridad@), mandatory MFA from minute one. - The organization and folders.
- Billing: account, export to BigQuery, a global budget of €2,500 and one per customer folder.
alpinatech-auditoriawith an aggregated sink and locked retention. Before anything else, to capture the whole build.- Policies 1-11 in
dry-run. alpinatech-redwith Shared VPC.alpinatech-cicdwith Workload Identity Federation.- The first customer project as a template, with a reusable Terraform module.
- Policies in enforced mode.
- Workloads.
AlpinaTech's decisive advantage over AlpinaShop: it starts at step 1 with a blank slate. It will not have to negotiate a single exception, because when it enforces the policies there will be nothing breaching them. It is literally the best possible moment, and it only happens once.
On ISO 27001 within 18 months: practically everything in this lesson is direct evidence for the certification — access control, an immutable audit record, change management, periodic permission review, data classification. Doing it now means arriving at the audit with 18 months of records; doing it in a year means arriving with six. The auditor looks at the history, not at the snapshot of the day.
Solution 2 — Investigating the suspicious change
An initial reading of the indicators. Four signals, and none is conclusive on its own but together they draw a very recognisable pattern:
| Indicator | Why it is worrying |
|---|---|
| Saturday 23:47 | Outside any working hours and with no scheduled deployment |
SetIamPolicy → CreateServiceAccount → CreateServiceAccountKey in 51 seconds |
It is the canonical sequence for establishing persistence |
CreateServiceAccountKey |
Policy 4.3 ought to prevent it. A new downloadable key exists |
sa-mantenimiento is not in Terraform |
A resource created outside the process: either it is shadow IT or it is an attacker |
Step 1 — Who really acted? (5 minutes)
sa-deploy-prod is a service account; it does not act on its own. The question is who used it:
SELECT
timestamp,
protopayload_auditlog.authenticationInfo.principalEmail AS identity,
JSON_VALUE(protopayload_auditlog.authenticationInfo.serviceAccountDelegationInfo)
AS delegation,
protopayload_auditlog.requestMetadata.callerIp AS ip,
protopayload_auditlog.requestMetadata.callerSuppliedUserAgent AS agent,
protopayload_auditlog.methodName AS operation
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.authenticationInfo.principalEmail LIKE 'sa-deploy-prod%'
AND timestamp BETWEEN TIMESTAMP('2026-08-01 22:00:00')
AND TIMESTAMP('2026-08-02 02:00:00')
ORDER BY timestampThe three fields that decide the case:
| Field | If it is… | It means |
|---|---|---|
delegation |
Empty | The account was used directly, with a credential. Where did it come from? |
delegation |
A person's email address | Somebody impersonated it. Who, and why on a Saturday? |
agent |
Google-Cloud-Build |
It was the pipeline: you have to look for which build |
agent |
gcloud/... |
Somebody from a terminal |
agent |
A generic library or empty | Very suspicious |
ip |
A Google range (35.x, 34.x) |
Consistent with Cloud Build |
ip |
The office IP | Somebody on the team |
ip |
Anything else | 🚨 Incident confirmed |
Step 2 — What exactly was granted? (5 minutes)
SELECT
timestamp,
protopayload_auditlog.resourceName AS resource_name,
JSON_VALUE(d, '$.action') AS action,
JSON_VALUE(d, '$.role') AS role,
JSON_VALUE(d, '$.member') AS member
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`,
UNNEST(JSON_QUERY_ARRAY(
protopayload_auditlog.servicedata_v1_iam.policyDelta.bindingDeltas)) AS d
WHERE protopayload_auditlog.methodName LIKE '%SetIamPolicy%'
AND DATE(timestamp) = '2026-08-01'If the result includes ADD roles/owner or ADD roles/editor to sa-mantenimiento, or any grant to an external identity, it is an incident and the 07-04 script is activated.
Step 3 — Was the key that was created used? (10 minutes)
This is the question that determines the scope:
SELECT
timestamp,
protopayload_auditlog.methodName AS operation,
protopayload_auditlog.resourceName AS resource_name,
protopayload_auditlog.requestMetadata.callerIp AS ip
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.authenticationInfo.principalEmail LIKE 'sa-mantenimiento%'
ORDER BY timestampAnd the change to the data lake bucket, which is the obvious target:
SELECT timestamp, protopayload_auditlog.methodName, protopayload_auditlog.request
FROM `alpinashop-auditoria.auditoria.cloudaudit_googleapis_com_activity`
WHERE protopayload_auditlog.resourceName LIKE '%alpinashop-datalake%'
AND DATE(timestamp) BETWEEN '2026-08-01' AND CURRENT_DATE()
ORDER BY timestampIf buckets.update added allUsers, the data lake was public. That turns the case into a personal data breach with notification to the AEPD within 72 hours.
Step 4 — Attack or undocumented automation? (15 minutes)
| Signal | Legitimate automation | Attack |
|---|---|---|
| IP | A Google or office range | External, or from another cloud provider |
| User agent | Google-Cloud-Build, terraform |
Generic, empty, python-requests |
| Delegation | Somebody on the team | Empty or unknown |
| Associated build | It exists in Cloud Build | It does not exist |
| Role granted | Scoped and coherent | owner, editor |
| Key created | None: the pipeline does not need them | Yes |
| Resource name | Consistent with the convention | Generic and plausible: sa-mantenimiento |
| Subsequent activity | Consistent with the task | Enumeration, mass reads |
The indicator that weighs most is the key creation. AlpinaShop's pipeline has used Workload Identity Federation since 06-02 and neither needs nor has ever created a JSON key. A new key in production, on a Saturday at 23:48, is almost certainly an attacker's persistence. And the name sa-mantenimiento is exactly the kind of plausible name chosen to go unnoticed in a list.
Step 5 — Containment, in this order.
# 1. DELETE the key: it is the exfiltrated credential
gcloud iam service-accounts keys delete KEY_ID \
--iam-account=sa-mantenimiento@alpinashop-prod.iam.gserviceaccount.com
# 2. DISABLE (do not delete) the account: the evidence is preserved
gcloud iam service-accounts disable \
[email protected]
# 3. Revert the improper IAM grants
gcloud projects remove-iam-policy-binding alpinashop-prod \
--member="serviceAccount:[email protected]" \
--role="roles/owner"
# 4. Close the bucket if it was left open
gcloud storage buckets update gs://alpinashop-datalake --no-public-access-prevention=false
# 5. Rotate sa-deploy-prod's credentials and review the pipeline
# (if the compromise came from there, everything it touches is in doubt)
# 6. Freeze the evidence outside the affected project
gcloud logging read 'timestamp>="2026-07-01T00:00:00Z"' --project=alpinashop-prod \
--format=json > /tmp/evidencia.json
gcloud storage cp /tmp/evidencia.json gs://alpinashop-evidencia-forense/
# 7. Look for MORE persistence: there is never only one
gcloud asset search-all-resources --scope=organizations/ORG_ID \
--query="createTime>2026-07-25" --format="table(name, assetType, createTime)"The order matters: the credential first (step 1), because while it exists the attacker is still inside; then the account; then the permissions. And step 7 is never skipped: whoever establishes persistence rarely does it in only one place.
Which governance controls would have detected or prevented it.
| Control | Effect | Section |
|---|---|---|
iam.disableServiceAccountKeyCreation |
It would have PREVENTED it. Without a key, there is no persistence | 4.3 |
A Cloud Asset Inventory feed on ServiceAccountKey |
An alert in seconds, not 36 hours later | 9 |
A feed on owner grants |
An immediate alert on the SetIamPolicy |
9 |
storage.publicAccessPrevention |
It would have stopped the bucket being opened | 4.5 |
| The out-of-hours activity query | Detection the next day, not at the monthly review | 11 |
| Terraform as the only route to creation + drift detection | A plan would have shown the undeclared account |
06-07 |
| Immutable log retention | It guarantees the evidence was not altered | 12 |
And the conclusion that makes sense of the whole lesson: of the seven measures, the first would have prevented the attack entirely and the second would have detected it within seconds. Both are from this lesson, both cost less than an hour of configuration, and neither was in place on Saturday night.
A preventive control that costs an hour is worth more than the most brilliant investigation.
Solution 3 — Justifying governance to management
Why we are spending four days putting rules in the cloud Marta Ruiz, infrastructure lead
It is true that the system works and that we have not had a single serious incident. It is also true that it works because the three of us on the team remember to do things properly, not because anything prevents otherwise. Today, any of us — or anybody who joins next month — can, without meaning to and without any alarm going off, leave a data store open to the internet, create a permanent password nobody knows exists, or put customer data on a server outside Europe.
This is not a hypothesis. We have had two cases this year. We found a data access password that had been lost for fourteen months and still worked. And we discovered two services switched on that nobody was using and that had cost us €1,500. Both were resolved, but we found both of them by chance, months later.
What each block of work covers:
Automatic rules (day 1). They technically prevent what today we only avoid out of habit: no servers exposed to the internet, no data outside the EU, and — the most important — nobody can create permanent passwords. Without this, the lost password case can happen again tomorrow.
A protected record (day 2). We keep, in a separate place and behind a padlock not even I can open, the record of everything done on the platform. It serves three purposes: if something happens, we know exactly what and who; if a customer or the Data Protection Agency asks us for it, we can prove it; and because not even the administrator can modify it, it counts as evidence. A record I could delete would be no use at all.
Warnings as they happen (day 3). Today, if somebody grants themselves administrator permissions on a Saturday night, we would find out at the following month's review. With this we find out within seconds.
Inventory and reviews (day 4). Knowing at all times what exists, who has access to what, and what is costing money without delivering anything.
Why now and not later. It is a matter of arithmetic, and it is the most important argument in this note. Today we have five projects and around a hundred and twenty resources: putting the rules in affects zero existing things, because we already do everything properly. In two years, with fifteen projects and six hundred resources, putting those same rules in will mean finding everything that does not comply, talking to every team and accepting permanent exceptions that would leave the rules almost empty. Four days now are four weeks in two years' time, and by then it probably will not get done at all.
And there is a commercial reason worth bearing in mind too: when a large customer asks us for a security questionnaire — and they will — the difference between answering "yes, and here is the record for the last eighteen months" and answering "we trust our team" can be the difference between signing the contract or not.
What I am asking for besides the time:
- That a second person holds the master keys. Today I am the only one who can lift these rules. If I make a mistake or am unavailable, the team is left blocked.
- Half an hour a quarter to review together who has access to what. It is quick and it stops permissions accumulating on their own, which is what always ends up happening.
- Accepting that for two weeks we will go a bit more slowly, while we measure the impact of the rules before switching them on. We are not going to enable anything without testing it first: blocking the team out of haste would be the worst possible outcome.
The four communication decisions:
- Acknowledging they are right before countering. "It is true that it works" disarms the objection instead of confronting it. What gets corrected is why it works, not the fact.
- Two real incidents instead of hypothetical risks. "It could happen" gets dismissed; "it happened to us in March and we found it in July" does not. And both cases were resolved, which makes it possible to tell them without seeming alarmist or incompetent.
- The arithmetic argument as the backbone. Four days now against four weeks in two years is reasoning a finance director understands immediately and cannot argue with, because it does not depend on judging the risk: it depends on counting.
- The commercial angle at the end. It turns governance from a cost into a revenue enabler. It is the argument that carries the most weight with a board and that is why it comes after the others, not before: put first it would look like an excuse; put last, it clinches it.
And a fifth, visible in the requests: it explicitly asks to go more slowly for two weeks. Announcing the cost before it is felt stops the first friction being read as a planning error.
Conclusion
Module 7 ends where it had to end: turning into controls what until now were agreements.
You know what governance is and you answer its four questions with concrete mechanisms: what can be created (organization policies), what exists (Asset Inventory), who did what (Audit Logs) and who pays (structure and labels). And you have the argument that decides when to put it in: the cost does not grow with the resources, it grows with the exceptions you have to negotiate. Policies go in when they bother nobody; after that they never go in at all.
You know how to structure an organization with the three criteria — environment, team, application — and, more importantly, with their consequences for permissions, inherited policies, billing, quotas and blast radius. You understand why AlpinaShop structures by environment and why AlpinaTech, with external customers, must do it by customer.
You have mastered organization policies: what distinguishes them from IAM — they are not permissions, and not even an owner bypasses them — their three types, and inheritance with its key rule: a deny at any level always wins. You have AlpinaShop's nine policies with their reason, their level and their risk, including the two that close critical debts from 07-04.
You know how to apply them without blocking the team: the dry-run pattern for the fourth time in the course, the eight-step procedure that starts by inventorying what already breaches them, the five real ways to lock yourself out, and the safeguard to prepare before anything else: two people with orgpolicy.policyAdmin.
You know how to write custom constraints with CEL and to distinguish them from Policy Controller, which acts on Kubernetes and does not compete but complements. And you know that quotas prevent where budgets only warn.
You can handle Cloud Asset Inventory to know what exists, search resources and IAM policies, export to BigQuery and build the history that answers "what was there on 3 June?". And you set up feeds that warn within seconds about an owner grant, a public bucket or — most important of all — the creation of a service account key.
You know the Cloud Audit Logs properly: the four types, with the two facts to retain — admin activity is free and cannot be disabled; data access is off by default and without it a breach cannot be bounded — and the three decisions that make its cost bearable. You know how to read an entry field by field, including serviceAccountDelegationInfo, which almost nobody looks at and without which the trail back to the person behind an impersonation is lost.
You have the five audit queries saved before you need them, with the "who touched a log" one singled out as the most important and the least common, and with policy denials used as a thermometer for whether your rules are properly calibrated.
And you have set up the piece that makes everything else credible: immutable retention in a separate project, with an aggregated sink at organization level and --include-children so no future project is left out, with logging.bucketWriter granted to the writerIdentity — the step that is always forgotten — and with the irreversible --locked. Where Marta, who administers the whole platform, can only read. Because a log the administrator can delete is no use as evidence.
You know what a landing zone is and — what matters — why the order is what it is: auditing before the network and the workloads, so as not to lose the logs from the period with the most changes in the platform's whole life. With Terraform's explicit depends_on as the legitimate case, and with Google's blueprints as a reference and not a template.
And you have the change management processes sized for three people: reviews with their frequency and their script, a project lifecycle with a mandatory expiry date on test ones, and an exceptions process where the expiry date is the only thing separating an exception from a permanent repeal.
And here module 7 ends. Look at where AlpinaShop is.
It started this module with an application that built, deployed and observed itself, and with a list of deferred conversations. Today the list is empty. DA-004 decided not to adopt the hybrid platform and to connect Sabadell with a tunnel, with its review conditions written down. DA-001, promised in module 2 and carried for five, is fulfilled: the catalogue runs on Cloud Run, the MIG is switched off, and there is not a single virtual machine left in the serving path. The network is a Shared VPC with permissions per subnet, real hybrid connectivity and a perimeter that stops the data leaving. Security has been reviewed end to end layer by layer, with a checklist of 43 controls and a prioritised debt whose critical part is already paid. The bill has come down 60 % and — more importantly — it is understood, attributed and tracked by unit cost. Reliability has numbers: four SLIs, four SLOs, an error budget with a policy, a recovery plan with RTO and RPO, and a tested restore that found five problems nobody suspected. And governance turns all of that into controls that do not depend on anybody remembering.
Debt remains, written down and prioritised. And a list remains of things AlpinaShop has decided not to do, with its reasons — which is what really distinguishes a mature organization from one that simply never got there.
You have travelled seven modules following Marta, Dani and Lucía. You have seen an account and a hierarchy created, an application migrated, a choice made between five compute services, networks, identities and secrets built, a data platform constructed, models trained and served, delivery automated, the system observed, an incident resolved end to end, infrastructure written as code, and now it secured, measured, made cheaper and governed. You have seen decisions taken and also undone: App Engine was ruled out, GKE was retired, Anthos was rejected, an endpoint was switched off. You have seen four architecture decisions documented, reviewed and — some of them — fulfilled years after being written.
What you have not done yet is decide for yourself.
Module 8 is your project. There is no longer an AlpinaShop to follow: there is a company, a problem and a set of constraints you will have to resolve from start to finish. You are going to gather requirements and translate them into decisions (08-01), design the complete architecture justifying every choice just as DA-001 was justified (08-02), implement it with infrastructure as code and continuous delivery (08-03), test it and deploy it with a canary, SLOs and a recovery plan (08-04), and present it and defend it to whoever asks you why you chose what you chose (08-05). And you will finish by looking ahead, towards the Google Cloud certifications and towards what comes after this course (08-06).
Everything you need is in the seven modules you have just been through. Now it is your turn.
Google Cloud Platform (GCP) Course
Module 1: Introduction to Google Cloud Platform
- What is Google Cloud Platform?
- Setting Up Your GCP Account
- A Tour of the GCP Console
- Projects, Resource Hierarchy and Billing
- Regions, Zones and the Shared Responsibility Model
- Cloud Shell and the gcloud CLI
Module 2: Core GCP Services
- Compute Engine: Virtual Machines on Google Cloud
- Cloud Storage: Object Storage
- Cloud SQL: Managed Relational Databases
- App Engine: Platform as a Service
- Google Kubernetes Engine (GKE)
- NoSQL Databases: Firestore, Bigtable and Spanner
- How to Choose the Right Compute Service
Module 3: Networking and Security
- VPC Networks
- Cloud Load Balancing
- Cloud CDN
- Identity and Access Management (IAM)
- Cloud Armor
- Secrets and Encryption: Secret Manager and Cloud KMS
- Cloud DNS, TLS Certificates and Publishing Services Securely
Module 4: Data and Analytics
- BigQuery: The Analytical Data Warehouse
- Cloud Dataflow: Batch and Streaming Data Processing
- Cloud Dataproc: Managed Spark and Hadoop
- Cloud Pub/Sub: Asynchronous Messaging
- Cloud Data Fusion: Code-Free Data Integration
- Orchestrating Pipelines with Cloud Composer and Workflows
- Data Governance and Dashboards with Dataplex and Looker Studio
Module 5: Machine Learning and AI
- Vertex AI: The Machine Learning Platform on GCP
- AutoML: Custom Models Without Writing Code
- TensorFlow on GCP: Training and Serving Models
- Natural Language API
- Vision API
- Generative AI on Vertex AI: Gemini Models and Embeddings
- MLOps: From Model to Product with Vertex AI Pipelines
Module 6: DevOps and Monitoring
- Cloud Build: Continuous Integration on GCP
- Cloud Source Repositories and Source Code Management
- Cloud Functions: Serverless Functions
- Cloud Monitoring (formerly Stackdriver): Metrics, Dashboards and Alerts
- Cloud Deployment Manager and Native Infrastructure as Code
- Cloud Logging and Cloud Trace: Logs, Traces and Diagnostics
- Terraform on GCP: Infrastructure as Code in Practice
Module 7: Advanced GCP Topics
- Hybrid and Multicloud with Anthos
- Serverless Computing with Cloud Run
- Advanced Networking: Shared VPC, Peering and Hybrid Connectivity
- Security Best Practices
- Cost Management and Optimization
- Reliability: SLOs, High Availability and Disaster Recovery
- Governance at Scale: Organization, Policies and Auditing
