Saltar para o conteúdo
BeryxTechBeryxTech
Voltar às respostas para RFP

Documentação do BeryX

Claude via Amazon Bedrock na conta AWS do cliente — onboarding IAM

BYOK 3 · ADR 0002, opção C · plano Enterprise (FEATURE_LLM_BYOK_CLOUD)

Clientes GCP: a mesma rota existe no Vertex AI, com impersonação de SA e sem chave JSON — docs/BYOK_VERTEX.md. Um workspace tem uma rota cloud (Bedrock ou Vertex).

Com esta rota, relatórios, sugestões, sinais, assistente, base de conhecimento e varredura de engenharia do BeryX correm no Claude da conta AWS da organização, numa região da UE. Os dados e a fatura dos tokens ficam na conta do cliente. A BeryxTech não guarda segredos: guarda o ARN de um role IAM e um ExternalId gerado por workspace, e obtém credenciais temporárias (15 min, só em memória) com sts:AssumeRole.

Como funciona

BeryX (runtime da BeryxTech)                    Conta AWS do cliente (eu-*)
───────────────────────────                   ───────────────────────────────────────────
ID token Google da SA do Cloud Run
  │ sts:AssumeRoleWithWebIdentity
  ▼
role broker da BeryxTech      ── sts:AssumeRole ─▶ role DataVaultBedrock
(LLM_BEDROCK_BROKER_PRINCIPAL)  + ExternalId      trust: principal da BeryxTech + ExternalId
                                                  policy: bedrock:InvokeModel* em eu.anthropic.*
credenciais temporárias    ── SigV4 ──────────▶ bedrock-runtime.<eu-região>.amazonaws.com
(só em memória, ≤15 min)                           inference profile eu.anthropic.claude-…
  • Residência: só são aceites regiões eu-* e ids de modelo eu.anthropic.* (inference profiles da UE). A política é verificada no domínio e na infra (providers/bedrock.py), por isso uma configuração fora dela nunca chega a assumir o role.
  • Confused deputy: o ExternalId é aleatório e único por workspace (dv-…). Sem ele na trust policy, o AssumeRole é recusado, mesmo que alguém saiba o ARN.
  • Falha: se o role deixar de funcionar (trust policy alterada, modelo sem acesso, quota), a IA degrada para os modos offline (heurísticas e templates) e o evento fica como llm.byok_error. Nunca caímos para a conta da BeryxTech contra a escolha do cliente.
  • Precedência: uma rota Bedrock ativa substitui a chave Anthropic própria (BYOK 1), se existir.
  • Model policy (BYOK 2): «IA desligada» e as restrições de modelos por tarefa continuam a aplicar-se. A policy pode recusar o modelo Bedrock numa tarefa, mas nunca o troca por outro.

Passo a passo

Em Equipa & Acessos → Claude via Amazon Bedrock (só administradores):

  1. Gerar ExternalId. A UI mostra o ExternalId do workspace e o principal da BeryxTech (o ARN que vai na trust policy).
  2. Criar o role na conta AWS (Terraform abaixo) e colar na UI o ARN do role, a região e o modelo. Ao guardar, fazemos um health-check: AssumeRole + um pedido de 1 token ao modelo (o Bedrock não tem Models API, e esta é a única prova de que o role pode invocá-lo nessa região). O custo é de ~10 tokens, na conta do cliente. Se falhar, nada é guardado e a mensagem diz o que corrigir (trust policy, permissões, acesso ao modelo, região).
  3. Correr o golden set. O golden set das sugestões (13 casos, GOLDEN v1) corre no modelo Bedrock escolhido, no worker (fila llm.golden.requested). A rota só fica ativa se passar (limiar DATAVAULT_LLM_CLOUD_GOLDEN_THRESHOLD, por omissão 100%). Reprovado, fica em golden_failed, com os casos que falharam, e a IA continua como estava. Os tokens do golden set são faturados na conta AWS e aparecem no FinOps (kind=golden). Com DATAVAULT_LLM_CLOUD_GOLDEN_JUDGE_MIN_SCORE > 0, o gate junta o juiz LLM do GOLDEN v2: 5 sugestões amostradas avaliadas pela cadeia dos relatórios, com a chave da BeryxTech, e a rota só ativa se a média das notas chegar a esse mínimo (sem fornecedor, reprova). O custo do juiz fica no FinOps como da BeryxTech (kind=golden, provider=anthropic).

Mudar o role, a região ou o modelo invalida a aprovação: o golden set tem de passar outra vez nesse modelo. Remover a rota é sempre possível, mesmo depois de um downgrade de plano.

Terraform (conta do cliente)

variable "datavault_principal_arn" {
  description = "Principal AWS da BeryxTech (mostrado na UI, passo 1)"
  type        = string
}

variable "datavault_external_id" {
  description = "ExternalId do workspace BeryX (mostrado na UI, passo 1)"
  type        = string
}

variable "region" {
  description = "Região da UE onde o BeryX invoca o Claude"
  type        = string
  default     = "eu-central-1"
}

data "aws_caller_identity" "current" {}

data "aws_iam_policy_document" "datavault_trust" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "AWS"
      identifiers = [var.datavault_principal_arn]
    }
    condition {
      test     = "StringEquals"
      variable = "sts:ExternalId"
      values   = [var.datavault_external_id]
    }
  }
}

resource "aws_iam_role" "datavault_bedrock" {
  name                 = "DataVaultBedrock"
  assume_role_policy   = data.aws_iam_policy_document.datavault_trust.json
  max_session_duration = 3600
}

# Mínimo: invocar os inference profiles Claude da UE (e os modelos de base para
# onde o profile encaminha, nas regiões da UE). Nada de listagens nem de gestão.
data "aws_iam_policy_document" "datavault_invoke" {
  statement {
    sid     = "InvokeClaudeEuInferenceProfiles"
    actions = ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"]
    resources = [
      "arn:aws:bedrock:${var.region}:${data.aws_caller_identity.current.account_id}:inference-profile/eu.anthropic.claude-*",
      "arn:aws:bedrock:eu-*::foundation-model/anthropic.claude-*",
    ]
  }
}

resource "aws_iam_role_policy" "datavault_invoke" {
  name   = "DataVaultBedrockInvoke"
  role   = aws_iam_role.datavault_bedrock.id
  policy = data.aws_iam_policy_document.datavault_invoke.json
}

output "datavault_role_arn" {
  value = aws_iam_role.datavault_bedrock.arn # colar na UI, passo 2
}

Antes de aplicar, garantir no console do Bedrock (região escolhida) que o acesso ao modelo Claude está concedido. Sem isso, o health-check devolve «O role não pode invocar este modelo». Para restringir mais, trocar eu.anthropic.claude-* pelo id exato do modelo escolhido. O CloudTrail do cliente regista cada sessão como datavault-<workspace_id>.

Modelos disponíveis

A allowlist é mantida pela BeryxTech (DEFAULT_BEDROCK_MODELS em backend/app/infra/llm/providers/bedrock.py). Por omissão tem Claude Haiku 4.5, Sonnet 5 e Opus 4.8, cada um com o seu inference profile eu.anthropic.…. Os ids mudam com as versões no Bedrock, e por isso podem ser substituídos sem deploy com LLM_BEDROCK_MODELS (JSON {"família": "eu.anthropic.…"}). Entradas fora de eu.anthropic.* são ignoradas. Confirmar os ids no console (Cross-region inference) antes de oferecer um modelo novo. Um modelo retirado da lista desliga a rota dos workspaces que o usavam (offline), até o admin escolher outro e o golden set passar.

Operação (BeryxTech)

  • Imagem: o extra [bedrock] (boto3) entra nas builds (CONNECTOR_EXTRAS), e os casos do golden set são copiados para /app/golden. Sem boto3, o health-check devolve sdk_unavailable (503 na API). O broker usa também o google-auth, que o backend/requirements.lock da imagem já traz.

  • Identidade de origem (role broker): o Cloud Run não tem identidade AWS e não põe o token da service account num ficheiro, por isso o AWS_WEB_IDENTITY_TOKEN_FILE do boto3 não serve. A api e o worker fazem a federação no código (backend/app/infra/llm/providers/bedrock_broker.py):

    1. pedem ao servidor de metadados um ID token Google da SA do serviço (datavault-api ou datavault-worker), com a audiência LLM_BEDROCK_BROKER_AUDIENCE;
    2. trocam-no por credenciais de 1 h do role broker da BeryxTech, com sts:AssumeRoleWithWebIdentity no STS de eu-central-1 (uma região ativa por omissão, mesmo que o cliente use uma região opt-in). Ficam só em memória e renovam-se 20 min antes de expirarem;
    3. assinam com elas o sts:AssumeRole com ExternalId no role do cliente, que dá as credenciais de 15 min (backend/app/infra/llm/providers/bedrock.py).

    A trust policy do broker só aceita accounts.google.com, com essa audiência e o uniqueId das duas SAs de runtime. O role só pode fazer sts:AssumeRole em roles de outras contas e com um ExternalId do BeryX (dv-*). Não há chaves de acesso AWS em lado nenhum, nem por workspace. O ARN do broker vai em LLM_BEDROCK_BROKER_PRINCIPAL e é o principal que os clientes põem na trust policy (passo 1). Um broker que recuse deixa a rota em unavailable (503) e nunca em assume_role_denied, porque o erro está do lado da BeryxTech e não na trust policy do cliente. Sem LLM_BEDROCK_BROKER_AUDIENCE (dev) não há federação, e o STS usa a cadeia por omissão do boto3 (AWS_PROFILE).

  • Custo: o preço Bedrock depende da região e do contrato do cliente, e por isso o FinOps mostra os tokens com custo — (nunca um número inventado) e remete para a fatura AWS.

  • Rotulagem art. 50.º: os outputs saem com «Claude …, Anthropic via Amazon Bedrock».

  • Sem Message Batches: o Bedrock não tem a Batches API. Os relatórios agendados desta rota correm pelo pedido síncrono.

Ligar o broker em produção (owner, uma vez)

Com as credenciais AWS do owner na conta da BeryxTech (o script não guarda nenhuma) e o gcloud com acesso ao projeto de produção:

AWS_PROFILE=<perfil do owner> ./deploy/aws_broker_bootstrap.sh --project <projeto de produção>
LLM_BEDROCK_BROKER_PRINCIPAL=<ARN que o script imprime> ./deploy/gcp_deploy.sh
bash deploy/validate_prod.sh
  1. O deploy/aws_broker_bootstrap.sh lê o uniqueId das SAs datavault-api e datavault-worker (gcloud iam service-accounts describe), cria ou atualiza o role beryx-bedrock-broker (MaxSessionDuration de 1 h), confere-o e imprime o ARN. Com --check só lê, e sai ≠ 0 se faltar alguma coisa ou se o role tiver outras permissões. Não cria OIDC provider, porque a AWS já conhece o accounts.google.com.
  2. O gcp_deploy.sh põe LLM_BEDROCK_BROKER_PRINCIPAL e LLM_BEDROCK_BROKER_AUDIENCE no env da api e do worker. A audiência é beryx-bedrock-broker por omissão, a mesma que o bootstrap põe na trust policy. Como o deploy repõe o env inteiro, a variável tem de vir em cada execução, tal como os price ids do Stripe.
  3. O check «Bedrock: broker AWS» do validate_prod.sh lê o ARN e a audiência do env do datavault-worker e executa com python -m app.infra.llm.providers.bedrock_broker o job datavault-llm-models, que corre com a SA do worker: o job obtém o ID token e assume o broker. Não chama o Bedrock nem a conta de nenhum cliente. Sem o broker no worker, o check dá SKIP «broker por configurar». Configurado, um STS que recuse dá FAIL de postura.
  4. Prova ponta a ponta, numa conta AWS de teste do owner. Com o acesso ao Claude pedido no Bedrock numa região eu-*, cria o role do Terraform com o ARN do broker como principal e o ExternalId que a UI mostra. Depois, num workspace Enterprise, segue o passo a passo até guardar a rota: o health-check (AssumeRole assinado pelo broker e 1 token ao modelo, ~10 tokens na conta de teste) tem de passar. O validate_prod.sh não faz esta prova, porque precisava de uma conta de cliente.

A trust policy segue as fontes primárias lidas a 19/10/2026. O ID token de uma service account traz o azp, e por isso a AWS compara accounts.google.com:aud com o azp (o uniqueId da SA), accounts.google.com:oaud com o aud (a audiência) e accounts.google.com:sub com o sub (o uniqueId): chaves de condição do IAM e AWS Security Blog. O role broker nunca se apaga. As trust policies dos clientes guardam o id interno do role, e um role recriado com o mesmo nome já não lhes serve (elemento Principal do IAM).

API (admin)

Método Caminho O quê
GET /api/v1/workspace/llm-bedrock Estado, ExternalId, principal da BeryxTech, allowlist, último golden
POST /api/v1/workspace/llm-bedrock/prepare Gera o ExternalId (idempotente)
PUT /api/v1/workspace/llm-bedrock {role_arn, region, model}: grava se o health-check passar
POST /api/v1/workspace/llm-bedrock/golden Pede o golden set (fila; inline sem mensageria)
DELETE /api/v1/workspace/llm-bedrock Remove a rota (sem gate de plano)

Estados: draft → pending_golden → golden_running → active | golden_failed. Erros com code estável: region_not_allowed, invalid_role_arn, model_not_allowed (422), assume_role_denied, model_denied, model_not_found (422), unreachable (502), unavailable (503), not_prepared, not_configured, golden_running, already_active (409), e 402 fora do Enterprise. Tudo fica em admin-events (llm_byok_bedrock_*, categoria sensível byok), com a conta AWS, a região e o modelo, mas nunca o ExternalId.