Skip to content

Глава 22. Мультиоблачная архитектура

«Мультиоблако — это не выбор технологий, а выбор абстракций.»

Большинство организаций уже работают в нескольких облаках: вычисления в AWS, данные в GCP BigQuery, идентичность в Azure Entra ID. Согласно Flexera State of the Cloud 2025, 89% организаций используют мультиоблачную стратегию. Проблема: каждый облачный провайдер предлагает собственный набор Zero Trust-инструментов, несовместимых между собой. Без единого слоя абстракции каждая пара «облако ↔ облако» порождает ad hoc-интеграции, а количество таких пар растёт как O(n²).


22.1 Зачем: проблема фрагментации

Три измерения фрагментации

ИзмерениеAWSGCPAzureПроблема
ИдентичностьIRSA, Pod Identity, IAM Roles AnywhereWorkload Identity FederationManaged Identity, Entra WIFРазные форматы токенов, разные API
ПолитикиIAM JSON, CedarIAM allow-onlyRBAC + Conditional AccessРазные языки, разная семантика
СетьVPC Lattice, Security GroupsFirewall Policies, Secure TagsNSG, ASG, Private LinkРазные модели сегментации

Подробное сравнение облачных механизмов идентичности рабочих нагрузок — в Главе 7, раздел «Идентичность рабочих нагрузок в облаках».

Реальная цена фрагментации

Uber (5 000+ микросервисов, 4 облака: GCP, OCI, AWS, on-prem) обнаружили, что управляют 150 000 секретами в 25 хранилищах, потому что каждое облако требовало собственных credentials. Решение: унификация через SPIRE + переход к secretless-архитектуре. К 2025 году платформа автоматически ротирует ~20 000 секретов в месяц и движется к полному отказу от статических credentials через workload identity federation.

Источники: Uber Blog: Multi-Cloud Secrets Management, Uber Blog: SPIFFE/SPIRE at Scale

Подход: три открытых слоя абстракции

Принцип: один открытый стандарт на каждый слой — SPIFFE/SPIRE для идентичности (CNCF Graduated, сентябрь 2022), OPA для политик (CNCF Graduated, январь 2021), Terraform/OpenTofu для инфраструктуры. Каждый слой работает поверх облачных API, не заменяя их.


22.2 Единая идентичность: SPIRE federation

Основы федерации SPIRE (протокол обмена trust bundles, профили https_web и https_spiffe, конфигурация federates_with) рассмотрены в Главе 7, раздел «Федерация между доменами доверия». Интеграция SPIRE с Istio service mesh — в Главе 12. Здесь мы сосредоточимся на практической архитектуре для трёх облаков.

Архитектура: один trust domain на облако

Почему отдельные trust domains, а не вложенный SPIRE? Отдельные домены обеспечивают организационную автономию: команды каждого облака управляют своим SPIRE Server независимо. Вложенный (nested) SPIRE — единый trust domain с иерархией CA — лучше подходит для single-org multi-cluster в одном облаке (см. документацию SPIRE по nested topology).

Выбор Node Attestor по облаку

ОблакоРекомендуемый attestorАльтернативаКлючевые selectors
AWS EKSaws_iidk8s_psataws_iid:iamrole, aws_iid:tag:env:prod, aws_iid:sg:id
GCP GKEgcp_iitk8s_psatgcp_iit:project-id, gcp_iit:sa, gcp_iit:label:env:prod
Azure AKSazure_msik8s_psat, azure_imds (v1.14.0+)azure_msi:subscription-id, azure_msi:vm-name:rg:name
Любой K8sk8s_psatk8s_psat:cluster, k8s_psat:agent_ns, k8s_psat:agent_sa

k8s_psat — единственный облако-агностичный attestor, работающий одинаково на всех платформах. Используйте его, когда облачные selectors не нужны. Начиная с SPIRE v1.12.x, устаревший k8s_sat удалён.

Начиная с SPIRE v1.13.1, aws_iid поддерживает валидацию принадлежности к EKS-кластеру — сервер проверяет, что нода принадлежит указанному EKS-кластеру через Auto Scaling Group.

Источники: aws_iid plugin, gcp_iit plugin, azure_msi plugin

Конфигурация federation через Helm

Используем SPIRE Helm charts hardened v0.28.1 (январь 2026), CRD API: spire.spiffe.io/v1alpha1.

Кластер A (EKS, trust domain aws.example.com):

yaml
# values-eks.yaml
global:
  spire:
    clusterName: eks-production
    trustDomain: aws.example.com

spire-server:
  nodeAttestor:
    aws_iid:
      enabled: true
      plugin_data:
        validate_eks_cluster_membership:
          eks_cluster_names: ["eks-production"]

  federation:
    enabled: true
    ingress:
      enabled: true  # Expose bundle endpoint

  controllerManager:
    identities:
      clusterFederatedTrustDomains:
        gcp-domain:
          trustDomain: gcp.example.com
          bundleEndpointURL: https://spire-federation.gcp.example.com
          bundleEndpointProfile:
            type: https_web  # WebPKI — проще для начала
        azure-domain:
          trustDomain: azure.example.com
          bundleEndpointURL: https://spire-federation.azure.example.com
          bundleEndpointProfile:
            type: https_web

      clusterSPIFFEIDs:
        default:
          federatesWith:
          - gcp.example.com
          - azure.example.com

Кластер B (GKE, trust domain gcp.example.com):

yaml
# values-gke.yaml
global:
  spire:
    clusterName: gke-production
    trustDomain: gcp.example.com

spire-server:
  nodeAttestor:
    gcp_iit:
      enabled: true
      plugin_data:
        projectid_allow_list: ["my-gcp-project"]
        use_instance_metadata: true

  federation:
    enabled: true
    ingress:
      enabled: true

  controllerManager:
    identities:
      clusterFederatedTrustDomains:
        aws-domain:
          trustDomain: aws.example.com
          bundleEndpointURL: https://spire-federation.aws.example.com
          bundleEndpointProfile:
            type: https_web
        azure-domain:
          trustDomain: azure.example.com
          bundleEndpointURL: https://spire-federation.azure.example.com
          bundleEndpointProfile:
            type: https_web

      clusterSPIFFEIDs:
        default:
          federatesWith:
          - aws.example.com
          - azure.example.com

Кластер C (AKS) конфигурируется аналогично с azure_msi node attestor и federates_with двух других доменов.

Источники: SPIRE Helm Charts Federation, SPIRE Helm Chart Repo

OIDC Discovery Provider: мост к облачным API

SPIRE JWT-SVID сам по себе не авторизует доступ к AWS S3 или GCP BigQuery. Для этого нужен OIDC Discovery Provider — компонент, который публикует JWKS-endpoint, понятный облачным STS.

Настройка для каждого облака:

ОблакоКонфигурацияAudience
AWSIAM OIDC Identity Provider → AssumeRoleWithWebIdentitysts.amazonaws.com
GCPWorkload Identity Pool + OIDC Provider → STS exchangehttps://iam.googleapis.com/...
AzureEntra Federated Identity Credentials → token exchangeapi://AzureADTokenExchange

OIDC Discovery Provider развёртывается Helm chart'ом автоматически при установке SPIRE. Требуется публичный DNS для JWKS-endpoint (или приватный DNS с Cloud DNS peering / Route53 PHZ sharing).

Источники: SPIRE OIDC Discovery Provider, AWS OIDC Federation with SPIRE


22.3 Единая политика: OPA bundles across clouds

Основы OPA, Rego v1 и интеграция с Kubernetes (Gatekeeper, Kyverno) рассмотрены в Главе 19. Здесь — дистрибуция политик в мультиоблачной среде.

Проблема: политики живут в каждом облаке отдельно

Без единого слоя:

  • AWS: IAM Policies (JSON) + Security Group rules
  • GCP: IAM allow policies (YAML) + Firewall rules
  • Azure: RBAC assignments + NSG rules + Conditional Access

Результат: одна и та же логика (например, «только production workloads могут читать PII») реализована тремя разными способами, три разных места для аудита, три вектора ошибки.

Решение: OPA как единый policy engine

OPA v1.13.1 (январь 2026) — текущая стабильная версия. Начиная с OPA 1.0 (декабрь 2024), Rego v1 обязателен: ключевые слова if и contains используются во всех правилах.

Bundle: единица дистрибуции политик

OPA Bundle — gzip-архив, содержащий .rego-файлы, data.json/data.yaml и .manifest. Bundles загружаются по HTTP/S из облачного хранилища или OCI-реестра.

Конфигурация OPA для AWS (S3):

yaml
services:
  s3:
    url: https://zt-policy-bundles.s3.eu-central-1.amazonaws.com
    credentials:
      s3_signing:
        environment_credentials: {}

bundles:
  authz:
    service: s3
    resource: bundles/authz/bundle.tar.gz
    polling:
      min_delay_seconds: 30
      max_delay_seconds: 120

Конфигурация OPA для GCP (GCS):

yaml
services:
  gcs:
    url: https://storage.googleapis.com/storage/v1/b/zt-policy-bundles/o
    credentials:
      gcp_metadata:
        scopes:
          - https://www.googleapis.com/auth/devstorage.read_only

bundles:
  authz:
    service: gcs
    resource: "bundles/authz/bundle.tar.gz?alt=media"
    polling:
      min_delay_seconds: 30
      max_delay_seconds: 120

Конфигурация OPA для Azure (Blob Storage):

yaml
services:
  blob:
    url: https://ztpolicybundles.blob.core.windows.net
    credentials:
      oauth2:
        token_url: "https://login.microsoftonline.com/${TENANT_ID}/oauth2/v2.0/token"
        client_id: "${CLIENT_ID}"
        client_secret: "${CLIENT_SECRET}"
        scopes:
          - "https://storage.azure.com/.default"

bundles:
  authz:
    service: blob
    resource: policy-container/bundles/authz/bundle.tar.gz
    polling:
      min_delay_seconds: 30
      max_delay_seconds: 120

OCI-реестры как универсальная альтернатива

Начиная с OPA v0.40.0, bundles можно хранить в OCI-реестрах (ECR, GAR, ACR, GHCR). Преимущество: единый формат дистрибуции для всех облаков, версионирование через теги, подпись через Cosign.

bash
# Сборка и публикация bundle
opa build -b policy/ -o bundle.tar.gz
oras push ghcr.io/myorg/zt-policy:v1.2.0 \
  --artifact-type application/vnd.oci.image.layer.v1.tar+gzip \
  bundle.tar.gz

CI pipeline для мультиоблачных политик

yaml
# .github/workflows/policy-ci.yaml
name: Policy CI
on:
  push:
    paths: ['policy/**']

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Install OPA
      run: |
        curl -L -o opa https://openpolicyagent.org/downloads/v1.13.1/opa_linux_amd64_static
        chmod +x opa && sudo mv opa /usr/local/bin/
    - name: Rego format check
      run: opa fmt --check policy/
    - name: Run tests
      run: opa test policy/ -v --coverage --format=json > coverage.json
    - name: Build bundle
      run: opa build -b policy/ -o bundle.tar.gz

  distribute:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
    - uses: actions/checkout@v4
    # AWS
    - uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789012:role/policy-deployer
        aws-region: eu-central-1
    - run: aws s3 cp bundle.tar.gz s3://zt-policy-bundles/bundles/authz/
    # GCP
    - uses: google-github-actions/auth@v3
      with:
        workload_identity_provider: projects/123/locations/global/...
    - run: gsutil cp bundle.tar.gz gs://zt-policy-bundles/bundles/authz/

Источники: OPA Bundles, OPA Releases


22.4 Terraform: единая инфраструктура

Multi-provider паттерн

Terraform v1.14.4 (январь 2026, BSL 1.1) и OpenTofu v1.11.4 (январь 2026, MPL 2.0, CNCF Sandbox) поддерживают multi-provider конфигурации. Ключевой принцип: один модуль на ресурс, отдельные state-файлы на облако.

infrastructure/
├── modules/
│   ├── spire-eks/          # AWS EKS + SPIRE
│   ├── spire-gke/          # GCP GKE + SPIRE
│   └── spire-aks/          # Azure AKS + SPIRE
├── environments/
│   ├── production/
│   │   ├── aws/
│   │   │   ├── main.tf     # module "spire-eks" { ... }
│   │   │   └── backend.tf  # S3 + DynamoDB
│   │   ├── gcp/
│   │   │   ├── main.tf     # module "spire-gke" { ... }
│   │   │   └── backend.tf  # GCS
│   │   └── azure/
│   │       ├── main.tf     # module "spire-aks" { ... }
│   │       └── backend.tf  # Azure Storage
│   └── staging/
└── policy/                  # OPA policies for Terraform plans

State management: отдельный state на облако

BackendLockingШифрованиеПримечание
S3DynamoDB tableKMSСамый зрелый; encrypt = true
GCSBuilt-in (object metadata)Customer-Managed Encryption KeysНе требует отдельного lock-ресурса
Azure StorageNative blob leaseStorage Service EncryptionИнтеграция с Azure RBAC

Не используйте единый state для нескольких облаков. Blast radius единого state-файла слишком велик: ошибка в GCP-модуле может заблокировать деплой в AWS из-за lock contention.

Ephemeral resources (Terraform 1.10+)

Для Zero Trust критически важно: секреты не должны сохраняться в state-файле. Ephemeral resources решают эту проблему:

hcl
# Секрет запрашивается при apply и НЕ сохраняется в state
ephemeral "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "production/db/password"
}

resource "aws_db_instance" "main" {
  # ...
  password = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
}

OPA-валидация Terraform-планов

Перед применением каждого плана — проверка OPA-политиками (подробнее в Главе 19):

bash
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
conftest test tfplan.json --policy policy/

Пример политики для мультиоблака — запрет публичных endpoints:

hcl
package terraform.multicloud

import rego.v1

# Запретить публичные S3 бакеты
deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "aws_s3_bucket_public_access_block"
    resource.change.after.block_public_acls == false
    msg := sprintf("S3 bucket '%s': публичный доступ запрещён", [resource.name])
}

# Запретить GCS бакеты с allUsers
deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "google_storage_bucket_iam_member"
    resource.change.after.member == "allUsers"
    msg := sprintf("GCS bucket '%s': allUsers запрещён", [resource.name])
}

# Запретить Azure Storage без private endpoint
deny contains msg if {
    resource := input.resource_changes[_]
    resource.type == "azurerm_storage_account"
    resource.change.after.public_network_access_enabled == true
    msg := sprintf("Storage account '%s': публичный доступ запрещён", [resource.name])
}

Источники: Terraform Releases, OpenTofu Releases, OPA Terraform Integration


22.5 Cloud-native vs Open Source: матрица выбора

Сравнительная таблица

СлойAWSGCPAzureOpen Source
Идентичность (workload)IRSA, Pod IdentityWorkload Identity FederationManaged Identity, WIFSPIFFE/SPIRE
Привилегированный доступSecrets ManagerSecret ManagerKey VaultVault (BSL 1.1)
ПолитикиIAM JSON, CedarIAM allow-onlyRBAC + Conditional AccessOPA/Rego
МикросегментацияSecurity Groups, VPC LatticeFirewall PoliciesNSG + ASGCilium, Calico
Service meshApp Mesh (EOL 30.09.2026) → VPC LatticeCloud Service MeshAKS Istio add-onIstio, Linkerd
IaCCloudFormationDeployment ManagerBicep/ARMTerraform / OpenTofu
Lock-inВысокийВысокийВысокийНет
Multi-cloudНетНетНетДа

Когда cloud-native, когда open source

Cloud-native (один провайдер):

  • Проще настройка, managed operations
  • Глубокая интеграция с облачными сервисами
  • Подходит для single-cloud организаций
  • Пример: EKS Pod Identity → S3 доступ в 3 строки YAML

Open source (мультиоблако):

  • Единый набор инструментов для всех платформ
  • Переносимость между облаками и on-prem
  • Инвестиции в экспертизу переиспользуются
  • Пример: SPIRE → любое облако + on-prem через один API

Гибридный подход (рекомендуемый):

  • Open source для кросс-облачных слоёв (идентичность, политики)
  • Cloud-native для специфичных сервисов (VPC Lattice для AWS-internal traffic, Cloud Service Mesh для GCP-internal)
  • SPIRE OIDC Discovery Provider как мост между SPIFFE-идентичностями и облачными IAM

Service mesh: текущее состояние

Облачные провайдеры переосмысливают managed mesh:

ПровайдерТекущий статусНаправление
AWSApp Mesh EOL 30.09.2026 (новые клиенты не принимаются с 24.09.2024)Миграция на VPC Lattice (для EKS) или ECS Service Connect
GCPCloud Service Mesh (Anthos SM + Traffic Director объединены)Единый глобальный control plane, автоматические обновления
AzureAKS Istio add-on (managed sidecar mode)Ambient mode на roadmap, без сроков

Для мультиоблачного service mesh: самостоятельный Istio с SPIRE CA обеспечивает единообразие. SPIRE выдаёт сертификаты для workloads, Istio использует их через SDS API. Подробная конфигурация SPIRE+Istio — в Главе 12.

Источники: AWS App Mesh EOL, GCP Cloud Service Mesh, AKS Istio add-on


22.6 Lab: SPIRE federation между двумя кластерами

В этой лабораторной работе мы создадим два kind-кластера, имитирующих разные облака с разными trust domains, настроим SPIRE federation между ними и продемонстрируем cross-domain mTLS.

Предварительные требования

Шаг 1: Создание двух кластеров

bash
# Кластер A — «AWS EKS»
cat <<EOF | kind create cluster --name cluster-aws --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  podSubnet: "10.10.0.0/16"
  serviceSubnet: "10.11.0.0/16"
nodes:
- role: control-plane
- role: worker
EOF

# Кластер B — «GCP GKE»
cat <<EOF | kind create cluster --name cluster-gcp --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
  podSubnet: "10.20.0.0/16"
  serviceSubnet: "10.21.0.0/16"
nodes:
- role: control-plane
- role: worker
EOF

Шаг 2: Установка SPIRE на оба кластера

bash
# Установка CRD (одинаково для обоих)
for ctx in kind-cluster-aws kind-cluster-gcp; do
  kubectl --context $ctx create namespace spire-system 2>/dev/null || true
  helm upgrade --install --kube-context $ctx -n spire-system \
    spire-crds spire-crds \
    --repo https://spiffe.github.io/helm-charts-hardened/
done

# Кластер A (trust domain: aws.lab.example)
helm upgrade --install --kube-context kind-cluster-aws \
  -n spire-system spire spire \
  --repo https://spiffe.github.io/helm-charts-hardened/ \
  --set global.spire.clusterName=cluster-aws \
  --set global.spire.trustDomain=aws.lab.example \
  --set spire-server.federation.enabled=true \
  --wait

# Кластер B (trust domain: gcp.lab.example)
helm upgrade --install --kube-context kind-cluster-gcp \
  -n spire-system spire spire \
  --repo https://spiffe.github.io/helm-charts-hardened/ \
  --set global.spire.clusterName=cluster-gcp \
  --set global.spire.trustDomain=gcp.lab.example \
  --set spire-server.federation.enabled=true \
  --wait

Шаг 3: Обмен trust bundles

bash
# Экспорт bundle из кластера A
kubectl --context kind-cluster-aws -n spire-system exec \
  spire-server-0 -- \
  /opt/spire/bin/spire-server bundle show -format spiffe \
  > /tmp/aws-bundle.json

# Экспорт bundle из кластера B
kubectl --context kind-cluster-gcp -n spire-system exec \
  spire-server-0 -- \
  /opt/spire/bin/spire-server bundle show -format spiffe \
  > /tmp/gcp-bundle.json

# Импорт bundle B в кластер A
kubectl --context kind-cluster-aws -n spire-system \
  cp /tmp/gcp-bundle.json spire-server-0:/tmp/gcp-bundle.json
kubectl --context kind-cluster-aws -n spire-system exec \
  spire-server-0 -- \
  /opt/spire/bin/spire-server bundle set \
    -format spiffe \
    -id spiffe://gcp.lab.example \
    -path /tmp/gcp-bundle.json

# Импорт bundle A в кластер B
kubectl --context kind-cluster-gcp -n spire-system \
  cp /tmp/aws-bundle.json spire-server-0:/tmp/aws-bundle.json
kubectl --context kind-cluster-gcp -n spire-system exec \
  spire-server-0 -- \
  /opt/spire/bin/spire-server bundle set \
    -format spiffe \
    -id spiffe://aws.lab.example \
    -path /tmp/aws-bundle.json

Шаг 4: Регистрация рабочих нагрузок с федерацией

bash
# В кластере A: зарегистрировать workload, которому доверяет кластер B
kubectl --context kind-cluster-aws -n spire-system exec \
  spire-server-0 -- \
  /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://aws.lab.example/ns/demo/sa/frontend \
    -parentID spiffe://aws.lab.example/spire/agent/k8s_psat/cluster-aws \
    -selector k8s:ns:demo \
    -selector k8s:sa:frontend \
    -federatesWith spiffe://gcp.lab.example

# В кластере B: зарегистрировать workload
kubectl --context kind-cluster-gcp -n spire-system exec \
  spire-server-0 -- \
  /opt/spire/bin/spire-server entry create \
    -spiffeID spiffe://gcp.lab.example/ns/demo/sa/backend \
    -parentID spiffe://gcp.lab.example/spire/agent/k8s_psat/cluster-gcp \
    -selector k8s:ns:demo \
    -selector k8s:sa:backend \
    -federatesWith spiffe://aws.lab.example

Шаг 5: Проверка федерации

bash
# В кластере A: проверить, что workload получает оба trust bundles
kubectl --context kind-cluster-aws -n demo exec \
  deploy/frontend -- \
  /opt/spire/bin/spire-agent api fetch x509 \
    -socketPath /spiffe-workload-api/spire-agent.sock

# Ожидаемый вывод:
# SVID: spiffe://aws.lab.example/ns/demo/sa/frontend
# Bundles:
#   spiffe://aws.lab.example (N certificates)
#   spiffe://gcp.lab.example (N certificates)  ← федерированный bundle

Наличие gcp.lab.example в списке bundles подтверждает, что рабочая нагрузка в кластере A может верифицировать SVID-сертификаты из кластера B — кросс-доменный mTLS готов.

Шаг 6: Очистка

bash
kind delete cluster --name cluster-aws
kind delete cluster --name cluster-gcp

Связь с другими главами

ТемаГлаваСвязь
SPIFFE/SPIRE основыГл. 7Идентичность рабочих нагрузок, федерация, облачные WIF
Service Mesh + SPIREГл. 12Istio + SPIRE CA, cross-cluster mTLS
Kubernetes deep diveГл. 14RBAC, Vault CSI, SPIRE на K8s
Политика как кодГл. 19OPA/Rego, Conftest, GitOps
Легаси и гибридныеГл. 23ZT proxy для legacy apps, AD→Entra миграция
Новые горизонтыГл. 24PQC для мультиоблачного mTLS, AI-агенты
Эталонная архитектураГл. 25Greenfield/brownfield сценарии, ADR

Итоги

  • SPIRE federation обеспечивает единую идентичность рабочих нагрузок через все облака: каждый кластер — свой trust domain, обмен trust bundles через federation API
  • OIDC Discovery Provider — мост между SPIFFE-идентичностями и облачными STS (AWS, GCP, Azure), позволяющий workloads получать временные облачные credentials без статических секретов
  • OPA bundles обеспечивают единые политики: один Rego-код в Git, дистрибуция в S3/GCS/Azure Blob или OCI-реестры
  • Terraform multi-provider + отдельные state-файлы + OPA-валидация планов = безопасная инфраструктура для всех облаков
  • Гибридный подход оптимален: open source для кросс-облачных слоёв (SPIRE, OPA), cloud-native для внутриоблачных сервисов (VPC Lattice, Cloud Service Mesh)
  • Начните с двух кластеров и https_web-профиля — это минимальный happy-path, который можно расширить до трёх и более облаков

Опубликовано под лицензией CC BY-SA 4.0