Lección 4 — Construire les modules

Stockage, journaux et réseau

7 min de lectura

Este contenido aún no está traducido. Se muestra la versión original.

L'auditeur RGPD demande les journaux d'appels du service de tri des six derniers mois. La rétention par défaut du bucket _Default de Cloud Logging est de 30 jours. Un sink d'archive avait bien été créé, il y a un an ; il n'a jamais rien écrit, parce que son identité n'avait pas le droit d'écrire dans le bucket de destination. Aucune erreur n'a jamais été affichée. Cette leçon construit trois modules dont les défauts sont silencieux : buckets, journalisation, réseau.

Le mécanisme

flowchart LR
  CR["Cloud Run<br/>lab-pia-dev-triage"] -- "stdout JSON" --> CL["Cloud Logging"]
  CL -- "filtre service + severity>=ERROR" --> MET["Métrique lab-pia-dev/errors"]
  CL -- "filtre service" --> SINK["Sink lab-pia-dev-archive"]
  SINK -- "writer_identity : objectCreator" --> BK[("Bucket archive<br/>privé, rétention N jours")]
  MET --> AL["Alerte (hors module)"]
  • Le service écrit une ligne JSON par événement sur la sortie standard ; Cloud Run la transmet à Cloud Logging, qui lit severity et message.
  • Une métrique basée sur les journaux compte les lignes qui correspondent à un filtre, à l'ingestion ; c'est elle qu'une alerte surveille.
  • Un sink copie les journaux filtrés vers une destination. Avec unique_writer_identity = true, il écrit sous une identité propre (writer_identity), qu'il faut autoriser sur la destination.

La mise en œuvre

Buckets : privés par construction

files/infra/modules/buckets/main.tf (extrait)
resource "google_storage_bucket" "this" {
  for_each                    = var.buckets
  name                        = "${var.prefix}-${each.key}-${random_id.suffix.hex}"
  location                    = var.region
  uniform_bucket_level_access = true
  public_access_prevention    = "enforced"
  force_destroy               = var.force_destroy
  deletion_policy             = var.force_destroy ? "DELETE" : "PREVENT"
  # ... versioning, lifecycle_rule si retention_days
}

public_access_prevention = "enforced" rend impossible toute ouverture publique, même par un allUsers ajouté plus tard à la main. uniform_bucket_level_access supprime les ACL par objet : un seul endroit à auditer, l'IAM du bucket. Le suffixe aléatoire évite les collisions : les noms de buckets sont globaux, tous clients confondus.

deletion_policy est un argument générique du provider google 8.x : PREVENT fait échouer tout destroy qui supprimerait la ressource. Le module le déduit de force_destroy : un bucket qu'on accepte de vider est un bucket qu'on accepte de supprimer.

Journalisation : le droit d'écrire fait partie du module

files/infra/modules/logging/main.tf (extrait)
resource "google_logging_project_sink" "archive" {
  name                   = "${var.prefix}-archive"
  destination            = "storage.googleapis.com/${var.archive_bucket}"
  filter                 = local.service_filter
  unique_writer_identity = true
}

# Sans ce droit, le sink est créé sans erreur… et n'écrit jamais rien.
resource "google_storage_bucket_iam_member" "sink_writer" {
  bucket = var.archive_bucket
  role   = "roles/storage.objectCreator"
  member = google_logging_project_sink.archive.writer_identity
}

Le filtre s'appuie sur le nom du service, connu avant que le service existe : la métrique et le sink se créent à la première application, avant même l'image.

Réseau : seulement en prod

Le dev de Logi-Express n'a pas de VPC : Cloud Run appelle Vertex AI par les API publiques de Google, authentifié par son compte de service. La prod en a un, pour joindre des systèmes internes (WMS, base de suivi) par des adresses privées, avec l'egress VPC direct de Cloud Run (sans connecteur à payer) :

files/infra/modules/network/main.tf (extrait)
resource "google_compute_firewall" "deny_ingress" {
  name          = "${var.prefix}-deny-all-ingress"
  network       = google_compute_network.this.id
  direction     = "INGRESS"
  priority      = 65534
  source_ranges = ["0.0.0.0/0"]
  deny { protocol = "all" }
}

resource "google_compute_subnetwork" "this" {
  ip_cidr_range            = var.cidr
  private_ip_google_access = true # API Google sans IP publique
  log_config {
    aggregation_interval = "INTERVAL_10_MIN"
    flow_sampling        = 0.1
  }
}

Un VPC personnalisé refuse déjà l'entrant implicitement ; la règle explicite rend ce refus visible et journalisable. L'échantillonnage des flow logs à 10 % limite leur volume (facturé à l'ingestion) tout en gardant de quoi enquêter.

L'API Compute n'est pas activée dans le projet du lab, et l'activer crée en général un réseau default avec ses règles de pare-feu. Le module network n'est donc pas appliqué ici : il est validé, planifié pour la prod (leçon 6) et testé avec un provider simulé dans le projet suivant.

Résultat obtenu (2026-10-15)

Extrait du plan de prod réel (make plan-prod), valeurs résolues :

module.platform.module.network[0].google_compute_firewall.deny_ingress
  name = "lab-pia-prod-deny-all-ingress", priority = 65534, direction = "INGRESS", deny = [{ protocol = "all" }]
module.platform.module.network[0].google_compute_subnetwork.this
  private_ip_google_access = true, ip_cidr_range = "10.20.0.0/24", flow_sampling = 0.1
module.platform.module.buckets.google_storage_bucket.this["archive"]
  force_destroy = false, deletion_policy = "PREVENT", lifecycle_rule = [{ Delete, age = 365 }]
module.platform.module.logging.google_logging_project_sink.archive
  filter = "resource.type=\"cloud_run_revision\" AND resource.labels.service_name=\"lab-pia-prod-triage\""

En dev (appliqué réellement), le premier appel au service a produit des lignes ERROR ; la métrique lab-pia-dev/errors les compte, et gcloud logging read les retrouve par le même filtre (leçon 7).

Ce qui casse en production

  • Sink muet. Créé sans le droit objectCreator de son writer_identity : aucune erreur, aucun objet. Diagnostic : gcloud logging sinks describe donne l'identité, gcloud storage buckets get-iam-policy montre qu'elle manque. Parade : le droit fait partie du module.
  • Les objets arrivent par lots. Un sink vers Cloud Storage écrit des fichiers regroupés par heure ; un bucket vide dix minutes après le premier appel n'est pas une panne. Ne testez pas un sink GCS avec une attente de quelques secondes.
  • destroy bloqué en fin de lab. Error: ... deletion_policy is set to PREVENT ou « bucket not empty ». C'est voulu en prod ; en dev, force_destroy = true (le module le déduit de l'environnement).
  • Plage IP qui chevauche le réseau d'entreprise. 10.0.0.0/8 choisi « par défaut », puis impossible d'appairer avec le VPN du siège. Parade : demander la plage à l'équipe réseau, la passer en variable validée (cidrnetmask).

La règle de décision

Tout bucket : privé forcé, accès uniforme, rétention explicite. Tout sink : son droit d'écriture dans le même module, et un test qui vérifie l'arrivée d'objets une heure plus tard. Un VPC seulement quand le service doit joindre des adresses privées ; sinon, l'identité IAM suffit et le réseau ajoute de la surface sans protection supplémentaire.

Pratique

Un sink d'archive vers GCS existe depuis six mois, le bucket est vide. Aucune erreur n'a été signalée. Que vérifiez-vous en premier ?

La sécurité demande « un VPC pour le service de tri en dev aussi, c'est plus sûr ». Le service n'appelle que Vertex AI et Secret Manager. Que répondez-vous ?

Exercice. Ajoutez au module logging une alerte Cloud Monitoring qui se déclenche si plus de 5 erreurs arrivent en 5 minutes, envoyée à un canal de notification passé en variable.

Voir la solution
variable "notification_channels" {
  type    = list(string)
  default = []
}

resource "google_monitoring_alert_policy" "errors" {
  project      = var.project_id
  display_name = "${var.prefix} : erreurs du service"
  combiner     = "OR"
  conditions {
    display_name = "plus de 5 erreurs en 5 min"
    condition_threshold {
      filter          = "metric.type=\"logging.googleapis.com/user/${google_logging_metric.errors.name}\" AND resource.type=\"cloud_run_revision\""
      comparison      = "COMPARISON_GT"
      threshold_value = 5
      duration        = "0s"
      aggregations {
        alignment_period   = "300s"
        per_series_aligner = "ALIGN_SUM"
      }
    }
  }
  notification_channels = var.notification_channels
}

ALIGN_SUM sur 300 s additionne les erreurs de la fenêtre ; duration = "0s" déclenche dès la première fenêtre au-dessus du seuil. Vérifiez avec terraform validate puis un plan.

  • Je crée des buckets privés par construction, avec rétention et protection contre la suppression selon l'environnement.
  • Je câble un sink avec le droit d'écriture de son identité, et je sais diagnostiquer un sink muet.
  • Je crée une métrique basée sur les journaux à partir d'un filtre de service.
  • Je sais quand un VPC apporte quelque chose à un service serverless.
Siguiente