Sägetstrasse 18, 3123 Belp, Switzerland +41 79 173 36 84 info@ict.technology

    Terraform @ Scale - Parte 5b: API Gateway

    Nel precedente articolo 5a abbiamo visto quanto rapidamente i grandi rollout con Terraform possano scontrarsi con i limiti API, ad esempio quando i test di disaster recovery creano centinaia di risorse in parallelo e gli errori 429 scatenano una valanga di retry. Questa continuazione riprende da lì e mostra come, con l’API Gateway di Oracle Cloud Infrastructure e Amazon API Gateway, sia possibile gestire consapevolmente i limiti, osservarli in maniera pulita e renderli operativamente solidi tramite “Policy as Code”. 

    API Gateway: l’ultima ratio?

    Gli API Gateway aiutano a rendere pianificabili i limiti API. Se usati correttamente, aggregano le chiamate API, applicano quote e throttling, forniscono materiale di osservabilità coerente e creano un punto centrale per operation e governance.

    Per noi è soprattutto rilevante un aspetto: un Gateway non si limita a spostare il problema dei rate limit, ma ne consente la gestione attiva per team, per deployment e per route.

    In Oracle Cloud Infrastructure si impostano dei binari tecnici con Usage Plan ed Entitlement. Questi agiscono direttamente sui deployment dell’API Gateway, ad esempio imponendo un limite rigido al numero di richieste al secondo nonché quote per minuto o per mese. Per enforcement e trasparenza sono disponibili metriche specifiche del servizio, come HttpResponses con le dimensioni deploymentId e httpStatusCode, che si possono allarmare in modo pulito. (Oracle Documentation).

    Le categorie di log di servizio access ed execution sono i canali previsti dal servizio; vengono direttamente assegnate al deployment dell’API e sono da preferire rispetto alla log-archiviazione legacy su bucket. (Oracle Documentation)

    Ecco un esempio per OCI (un esempio per AWS segue più avanti):


    # Terraform >= 1.10, OCI Provider 7.14.0
    terraform {
      required_version = ">= 1.10"
      required_providers {
        oci = { source = "oracle/oci", version >= "7.14.0" }
      }
    }
    
    provider "oci" {
      region = var.region
    }
    
    variable "region" {
      type        = string
      description = "OCI region, e.g., eu-frankfurt-1"
      validation {
        condition     = can(regex("^[a-z]+-[a-z0-9]+-[0-9]+$", var.region))
        error_message = "Region must match a pattern like 'eu-frankfurt-1'."
      }
    }
    
    variable "compartment_id" {
      type        = string
      description = "Compartment OCID used for gateway, logs, and alarms"
    }
    
    # Optional: Many organizations manage the API deployment separately.
    # We intentionally reference it via a variable to keep the example focused.
    variable "api_deployment_id" {
      type        = string
      description = "OCID of the API Gateway deployment"
      validation {
        condition     = can(regex("^ocid1\\..+", var.api_deployment_id))
        error_message = "api_deployment_id must be a valid OCID."
      }
    }
    
    # Enable service logs for 'access' and 'execution'
    resource "oci_logging_log_group" "apigw" {
      compartment_id = var.compartment_id
      display_name   = "apigw-logs"
    }
    
    resource "oci_logging_log" "apigw_access" {
      log_group_id = oci_logging_log_group.apigw.id
      display_name = "apigateway-access"
      log_type     = "SERVICE"
      is_enabled   = true
    
      configuration {
        source {
          category = "access"
          resource = var.api_deployment_id
          service  = "apigateway"
        }
      }
    }
    
    resource "oci_logging_log" "apigw_execution" {
      log_group_id = oci_logging_log_group.apigw.id
      display_name = "apigateway-execution"
      log_type     = "SERVICE"
      is_enabled   = true
    
      configuration {
        source {
          category = "execution"
          resource = var.api_deployment_id
          service  = "apigateway"
        }
      }
    }
    
    # Usage plan with rate limit & minute quota
    resource "oci_apigateway_usage_plan" "team_plan" {
      compartment_id = var.compartment_id
      display_name   = "team-standard-plan"
    
      entitlements {
        name        = "default"
        description = "Standard quota for CI runs"
    
        rate_limit {
          unit  = "SECOND"
          value = 50
        }
    
        quota {
          unit                 = "MINUTE"
          value                = 2000
          reset_policy         = "CALENDAR"
          operation_on_breach  = "REJECT"
        }
    
        targets {
          deployment_id = var.api_deployment_id
        }
      }
    
      lifecycle {
        prevent_destroy = true
      }
    }

    In Amazon API Gateway si combinano tre leve: Stage- e Method-Throttling, Usage Plan con API Key e regole basate sul rate in AWS WAF per aggregazioni IP. Le metriche di CloudWatch 4XXError e 5XXError forniscono un sistema di allerta precoce e robusto a livello di Stage.

    Importante: AWS WAFv2 oggi può essere associato solo a Stage REST-API, non alle HTTP API. (AWS Documentation, Terraform Registry)


    # Amazon API Gateway (REST) – stage throttling, usage plan, WAF
    terraform {
      required_version = ">= 1.10"
      required_providers {
        aws = { source = "hashicorp/aws", version = ">= 5.0" }
      }
    }
    
    provider "aws" {
      region = var.aws_region
    }
    
    data "aws_region" "current" {}
    
    resource "aws_api_gateway_rest_api" "tf_api" {
      name = "terraform-at-scale"
    }
    
    resource "aws_api_gateway_resource" "status" {
      rest_api_id = aws_api_gateway_rest_api.tf_api.id
      parent_id   = aws_api_gateway_rest_api.tf_api.root_resource_id
      path_part   = "status"
    }
    
    resource "aws_api_gateway_method" "get_status" {
      rest_api_id   = aws_api_gateway_rest_api.tf_api.id
      resource_id   = aws_api_gateway_resource.status.id
      http_method   = "GET"
      authorization = "NONE"
    }
    
    resource "aws_api_gateway_integration" "get_status_mock" {
      rest_api_id = aws_api_gateway_rest_api.tf_api.id
      resource_id = aws_api_gateway_resource.status.id
      http_method = aws_api_gateway_method.get_status.http_method
      type        = "MOCK"
    }
    
    resource "aws_api_gateway_deployment" "this" {
      rest_api_id = aws_api_gateway_rest_api.tf_api.id
      depends_on  = [aws_api_gateway_integration.get_status_mock]
    }
    
    resource "aws_api_gateway_stage" "prod" {
      rest_api_id   = aws_api_gateway_rest_api.tf_api.id
      deployment_id = aws_api_gateway_deployment.this.id
      stage_name    = "prod"
    
      method_settings {
        resource_path           = "/*"
        http_method             = "*"
        metrics_enabled         = true
        logging_level           = "INFO"
        data_trace_enabled      = false
        throttling_burst_limit  = 100
        throttling_rate_limit   = 50
      }
    }
    
    resource "aws_api_gateway_usage_plan" "plan" {
      name = "team-standard-plan"
    
      api_stages {
        api_id = aws_api_gateway_rest_api.tf_api.id
        stage  = aws_api_gateway_stage.prod.stage_name
      }
    
      throttle_settings {
        burst_limit = 100
        rate_limit  = 50
      }
    
      quota_settings {
        limit  = 2000
        period = "DAY"
      }
    }
    
    resource "aws_api_gateway_api_key" "ci_key" {
      name    = "ci-runs"
      enabled = true
      # If 'value' is omitted, the service generates a secure key automatically.
    }
    
    resource "aws_api_gateway_usage_plan_key" "ci_key_bind" {
      key_id        = aws_api_gateway_api_key.ci_key.id
      key_type      = "API_KEY"
      usage_plan_id = aws_api_gateway_usage_plan.plan.id
    }
    
    # WAFv2 rate-based rule (REGIONAL) – only for REST API stages, not HTTP APIs
    resource "aws_wafv2_web_acl" "apigw_waf" {
      name        = "apigw-waf"
      description = "Rate limit per source IP"
      scope       = "REGIONAL"
    
      default_action { allow {} }
    
      rule {
        name     = "rate-limit"
        priority = 1
        action { block {} }
    
        statement {
          rate_based_statement {
            limit              = 500
            aggregate_key_type = "IP"
          }
        }
    
        visibility_config {
          cloudwatch_metrics_enabled = true
          metric_name                = "apigw-waf"
          sampled_requests_enabled   = true
        }
      }
    
      visibility_config {
        cloudwatch_metrics_enabled = true
        metric_name                = "apigw-waf"
        sampled_requests_enabled   = true
      }
    }
    
    resource "aws_wafv2_web_acl_association" "stage_assoc" {
      resource_arn = "arn:aws:apigateway:${data.aws_region.current.name}::/restapis/${aws_api_gateway_rest_api.tf_api.id}/stages/${aws_api_gateway_stage.prod.stage_name}"
      web_acl_arn  = aws_wafv2_web_acl.apigw_waf.arn
    }
    

    I throttling a livello di Stage, i Usage Plan e l’associazione con la WAF sono i pilastri fondamentali sul lato AWS. CloudWatch mette a disposizione, tra le altre, le metriche 4XXError con le dimensioni ApiName e Stage, il che semplifica l’attivazione di allarmi specifici per Stage. (AWS Documentation)

    Testing e validazione con Terraform 1.10+

    Per disporre di reti di sicurezza rapide e riproducibili si raccomanda il framework di testing nativo di Terraform. I Mock-Provider incapsulano le dipendenze esterne, mentre le Assertions verificano regole specifiche di progetto come le dimensioni massime dei batch o il comportamento in caso di limiti troppo bassi.

    Pro-Tip: Utilizzi test brevi e significativi, che rafforzano i Suoi moduli contro configurazioni errate. (HashiCorp Developer)


    # tests/api_limits.tftest.hcl
    
    test {
      # optional name and timeouts can be added here
    }
    
    variables {
      # Global default variables for all runs in this test file
      max_batch_size = 50
    }
    
    # Example: The plan must never try to create more than 50 new resources
    run "enforce_small_batches" {
      command = plan
    
      assert {
        condition = length([for rc in run.plan.resource_changes : rc if contains(rc.change.actions, "create")]) <= var.max_batch_size
        error_message = "Too many new resources in a single run – split the deployment into smaller batches."
      }
    }
    
    # Example: We expect a failure of a named precondition
    # (Preconditions are defined in your modules/resources)
    run "expect_precondition_failure" {
      command = plan
      expect_failures = [
        precondition.api_limits_reasonable
      ]
    }

    Note dalla pratica:

    • Le Assertions devono essere su una sola riga,
    • expect_failures si riferisce a Preconditions nominate, non a generici errori di tipo,
    • Le risorse effimere, ad oggi (Terraform 1.12.0), sono utili soprattutto per token temporanei e query, ma non come sostituto universale dei Mock.

    Monitoring + Alerting

    L’osservabilità è la spina dorsale operativa della Sua strategia sugli API-Limit.

    Su OCI il metodo più affidabile è lavorare direttamente con le metriche di servizio dell’API Gateway in combinazione con gli allarmi della piattaforma di Monitoring. Le dimensioni deploymentId e httpStatusCode consentono un filtraggio univoco sulle risposte 429. La sintassi in MQL è la seguente, presti attenzione ai nomi corretti delle dimensioni: (Oracle Documentation)


    # OCI: Alarm on sustained HTTP 429 responses at deployment level
    resource "oci_ons_notification_topic" "ops" {
      compartment_id = var.compartment_id
      name           = "ops-alerts"
    }
    
    resource "oci_ons_subscription" "ops_mail" {
      compartment_id = var.compartment_id
      topic_id       = oci_ons_notification_topic.ops.id
      protocol       = "EMAIL"
      endpoint       = var.alert_email
    }
    
    resource "oci_monitoring_alarm" "apigw_429" {
      compartment_id        = var.compartment_id
      metric_compartment_id = var.compartment_id
      display_name          = "APIGW 429 bursts"
      is_enabled            = true
      severity              = "CRITICAL"
      destinations          = [oci_ons_notification_topic.ops.id]
      message_format        = "ONS_OPTIMIZED"
      pending_duration      = "PT1M"  # 1 minute
      resolution            = "1m"
    
      # Correct dimensions according to API Gateway metrics: deploymentId, httpStatusCode
      query = <<-EOT
        HttpResponses[1m]{deploymentId="${var.api_deployment_id}", httpStatusCode="429"}.sum() > 5
      EOT
    
      body = "Increased rate of HTTP 429 on API Gateway deployment: {{triggerValue}}/min"
    }

    Su AWS si definiscono allarmi semplici e affidabili su 4XXError e 5XXError, integrati da uno Stage-wide Throttling. Nella pratica, gli allarmi su 4XXError segnalano in anticipo e in maniera ampia, mentre i WAF-Rate-Limit intercettano i picchi di carico. (AWS Documentation)


    # AWS: CloudWatch alarm on 4XX errors (stage-wide)
    resource "aws_cloudwatch_metric_alarm" "api_4xx_spike" {
      alarm_name          = "apigw-prod-4xx-spike"
      comparison_operator = "GreaterThanThreshold"
      evaluation_periods  = 1
      period              = 60
      statistic           = "Sum"
      threshold           = 50
      namespace           = "AWS/ApiGateway"
      metric_name         = "4XXError"
    
      dimensions = {
        ApiName = aws_api_gateway_rest_api.tf_api.name
        Stage   = aws_api_gateway_stage.prod.stage_name
      }
    
      alarm_description = "Elevated client errors on 'prod' stage"
    }

    Best Practices per l’esercizio in produzione

    Pianificazione prima dell’ottimizzazione

    Gli API Gateway devono adattarsi al Suo modello architetturale e operativo, non il contrario. Le seguenti pratiche si sono dimostrate efficaci e si basano sull’articolo 5a di questa serie:

    Deployments scaglionati: Separi Foundation, Platform e Application Workloads, in modo che i singoli run restino piccoli e le quote non vengano superate cumulativamente.

    Circuit-Breaker per IaC: Implementi Preconditions e Check che interrompono i run non appena i tassi di errore aumentano. In questo modo non consuma le quote di altri team.

    Sfruttare finestre temporali: I rollout di grandi dimensioni dovrebbero avvenire al di fuori dei periodi di carico principale. Le pianificazioni CI sono strumenti operativi, non cosmetica.

    Timeout e Retry dei Provider: Estenda i timeout solo in modo mirato, invece di gonfiarli globalmente. Per le risorse OCI può impostare limiti temporali per singola risorsa, ad esempio durante il deployment:


    resource "oci_apigateway_deployment" "depl" {
      # ... your configuration ...
      timeouts {
        create = "30m"
        update = "30m"
        delete = "30m"
      }
    }
    

    Gestione consapevole della parallelità: In Terraform Enterprise utilizzi TFE_PARALLELISM per Workspace, invece di fissare ovunque i flag -parallelism nelle linee di comando. Questo evita picchi di carico incontrollati ed è tracciabile.

    Graceful Degradation: Costruisca percorsi opzionali che, in caso di limiti, ricadono su modalità operative più semplici invece di far fallire l’intero run.

    Quote documentate: Quote centralizzate e mantenute per provider e servizio sono indispensabili. Solo chi conosce le quote può effettuare deployment limitati.

    Policy as Code con Sentinel

    Le Policy proteggono la qualità della piattaforma. La seguente Sentinel-Policy limita il numero massimo di nuove risorse per run. Può essere configurata come guardrail Must-Have in Terraform Enterprise e genera un avviso significativo per volumi elevati invece di un Hard Fail.


    # sentinel/policies/api_limit_guard.sentinel
    import "tfplan/v2" as tfplan
    
    max_resources_per_run = 50
    
    resources_to_create = filter tfplan.resource_changes as _, rc {
      rc.change.actions contains "create"
    }
    
    main = rule {
      length(resources_to_create) <= max_resources_per_run
    }
    
    warn_high_resource_count = rule when length(resources_to_create) > 30 {
      print("WARNING: High resource volume detected.")
      print("Consider reducing parallelism or splitting the deployment.")
      true
    }

    Integrazione con Terraform Enterprise

    Molte delle misure discusse nell’articolo 5a sprigionano il loro effetto solo all’interno della pipeline.

    Terraform Enterprise consente di codificare Parallelismo, impostazioni di runtime e configurazioni client Gateway come standard organizzativi. Per i clienti nell’UE con esigenze di sovranità dei dati, TFE è (attualmente) l’unico strumento disponibile.


    terraform {
      required_version = ">= 1.10"
      required_providers {
        tfe = { source = "hashicorp/tfe", version = ">= 0.65.0" }
      }
    }
    
    provider "tfe" {
      hostname = var.tfe_hostname   # e.g., tfe.example.eu
      token    = var.tfe_token
    }
    
    resource "tfe_workspace" "prod" {
      name              = "production-infra"
      organization      = var.tfe_org
      queue_all_runs    = true    # Consider 'false' if your maturity model requires manual gates
      terraform_version = "1.10.5"
      working_directory = "live/prod"
    }
    
    resource "tfe_variable_set" "api_limits" {
      name         = "api-limit-controls"
      description  = "Controls for parallelism and API client defaults"
      organization = var.tfe_org
    }
    
    # Control Terraform parallelism via TFE_PARALLELISM
    resource "tfe_variable" "parallelism" {
      key             = "TFE_PARALLELISM"
      value           = "5"
      category        = "env"
      description     = "Terraform parallelism for API limit control"
      variable_set_id = tfe_variable_set.api_limits.id
    }
    
    # Example of passing a client header for downstream API gateway policies
    resource "tfe_variable" "client_header" {
      key             = "TF_VAR_apigw_client_header"
      value           = "X-CI-Run: ${timestamp()}"
      category        = "env"
      description     = "Example header for downstream API gateway policies"
      variable_set_id = tfe_variable_set.api_limits.id
    }

    Il controllo tramite TFE_PARALLELISM è documentato e validato nella pratica. Mantenga i valori conservativi e misuri l’impatto sulla durata di Plan e Apply.

    Attenzione: Un incremento indiscriminato porta spesso a prestazioni peggiori a causa di un aumento delle risposte 429/5xx.

    Conclusione: rispetto per l’API

    Gli API-Limit sono spesso percepiti come un ostacolo, ma in realtà rappresentano una sorta di contratto operativo tra il Suo codice e la piattaforma. Un approccio centrato su Terraform con Rate Limit chiari, Quote e allarmi a livello di Gateway porta pianificabilità nelle pipeline CI, protegge le risorse trasversali ai team e aumenta sensibilmente la percentuale di successo dei Suoi run.

    Le misure discusse nell’articolo 5a restano il primo strumento. Ulteriori API Gateway approfondiscono il controllo, armonizzano l’Observability e ancorano centralmente le Sue regole.

    Nota: Chi rispetta i limiti, effettua deployment più sostenibili e robusti.