Um pod que precisa ler um bucket S3 ou publicar numa fila SQS precisa de credenciais da AWS. Colocar uma access key num Secret funciona, mas cria uma chave de longa duração que alguém vai esquecer de girar. Com EKS Pod Identity, o pod recebe credenciais temporárias de uma IAM role, ligada à service account dele, sem nenhuma chave guardada no cluster.
Este é o quarto artigo da série artigos-eks. O anterior definiu quem acessa o cluster. Agora o assunto é o contrário: como os pods acessam serviços da AWS. O exemplo é o pod catalogo, do namespace loja, lendo arquivos de um bucket S3 com a menor permissão possível.
Segurança: com EKS Pod Identity não há access key no cluster. Se a sua aplicação ainda lê AWS_ACCESS_KEY_ID de um Secret, remova o Secret depois da migração e desative a chave no IAM.
Sumário
- Como o EKS Pod Identity funciona
- EKS Pod Identity × IRSA
- Add-on eks-pod-identity-agent
- Política mínima para o bucket S3
- Role e associação do EKS Pod Identity com Terraform
- Usando a role no pod
- terraform plan
- Outputs do Terraform
- Conclusão
- Referências oficiais
Como o EKS Pod Identity funciona
São três peças:
- Uma IAM role com trust policy no serviço
pods.eks.amazonaws.com. É a mesma trust policy para qualquer cluster. - Uma Pod Identity association, recurso da API do EKS, que liga a role a uma service account de um namespace.
- O add-on
eks-pod-identity-agent, que roda em cada nó e entrega as credenciais temporárias aos pods.
Quando um pod é criado com a service account associada, o EKS injeta nele as variáveis de ambiente que apontam para o agent. Os SDKs da AWS, em versões recentes, buscam as credenciais ali sozinhos, sem nenhuma mudança no código da aplicação.
As variáveis são injetadas na criação do pod. Pods que já estavam rodando antes da associação precisam ser recriados, por exemplo com kubectl rollout restart.
EKS Pod Identity × IRSA
Antes do EKS Pod Identity, a forma padrão era o IRSA (IAM Roles for Service Accounts). Ele continua funcionando, e muita gente ainda usa. A diferença está em quem guarda a ligação entre pod e role:
- IRSA: cada cluster precisa de um provedor OIDC registrado no IAM. A trust policy da role cita o OIDC daquele cluster, o namespace e a service account. A service account recebe a anotação
eks.amazonaws.com/role-arn. Usar a mesma role em outro cluster exige editar a trust policy. - EKS Pod Identity: sem OIDC e sem anotação. A trust policy é genérica, e a ligação fica na association, criada pela API do EKS. A mesma role serve para vários clusters, cada um com a sua association.
Outra vantagem do EKS Pod Identity: a sessão assumida leva tags com o nome do cluster, o namespace e a service account. Dá para usar essas tags em políticas (ABAC) e para identificar no CloudTrail qual pod fez cada chamada.
O IRSA ainda é o caminho para pods no Fargate, que não suportam EKS Pod Identity. No módulo do cluster, o provedor OIDC é opcional (enable_irsa). Neste ambiente ele fica desligado, porque não é usado:
module "eks" {
source = "../../terraform-eks-modules"
name = var.cluster_name
kubernetes_version = var.kubernetes_version
subnet_ids = module.vpc.private_subnet_ids
enable_irsa = false
public_access_cidrs = var.public_access_cidrs
}
Add-on eks-pod-identity-agent
O agent já vem do primeiro artigo da série, no módulo de add-ons. Sem ele, a association existe, mas o pod não recebe credencial nenhuma:
module "addons" {
source = "../../terraform-eks-addon-modules"
cluster_name = module.eks.name
kubernetes_version = module.eks.kubernetes_version
addons = {
vpc-cni = {}
coredns = {}
kube-proxy = {}
eks-pod-identity-agent = {}
}
depends_on = [module.ng_default]
}
Depois do apply, confira que o agent está rodando em todos os nós:
kubectl get daemonset eks-pod-identity-agent -n kube-system
Política mínima para o bucket S3
O pod catalogo só precisa listar e ler o prefixo produtos/ do bucket. Nada de s3:* nem de AmazonS3ReadOnlyAccess, que daria leitura em todos os buckets da conta:
data "aws_iam_policy_document" "catalogo_s3" {
statement {
sid = "ListarProdutos"
actions = ["s3:ListBucket"]
resources = ["arn:aws:s3:::${var.catalog_bucket_name}"]
condition {
test = "StringLike"
variable = "s3:prefix"
values = ["produtos/*"]
}
}
statement {
sid = "LerProdutos"
actions = ["s3:GetObject"]
resources = ["arn:aws:s3:::${var.catalog_bucket_name}/produtos/*"]
}
}
O bucket não é criado aqui: o nome dele entra pela variável catalog_bucket_name, para usar um bucket que você já tenha.
Role e associação do EKS Pod Identity com Terraform
module "pi_catalogo" {
source = "../../terraform-eks-pod-identity-modules"
cluster_name = module.eks.name
role_name = "${var.cluster_name}-catalogo-s3"
service_accounts = [
{ namespace = "loja", name = "catalogo" },
]
inline_policy_json = data.aws_iam_policy_document.catalogo_s3.json
depends_on = [module.addons]
}
Dentro do módulo estão a trust policy genérica e a association. Repare que a trust policy não cita cluster, namespace nem service account. Quem faz essa ligação é a association:
data "aws_iam_policy_document" "assume" {
statement {
actions = ["sts:AssumeRole", "sts:TagSession"]
principals {
type = "Service"
identifiers = ["pods.eks.amazonaws.com"]
}
}
}
resource "aws_eks_pod_identity_association" "this" {
for_each = { for sa in var.service_accounts : "${sa.namespace}/${sa.name}" => sa }
cluster_name = var.cluster_name
namespace = each.value.namespace
service_account = each.value.name
role_arn = aws_iam_role.this.arn
tags = var.tags
}
A ação sts:TagSession é obrigatória na trust policy: é ela que permite ao EKS colocar as tags de cluster, namespace e service account na sessão.
Usando a role no pod
No Kubernetes, basta criar a service account com o mesmo nome e namespace da association. Não há anotação. Salve como catalogo.yaml e troque o nome do bucket:
apiVersion: v1
kind: Namespace
metadata:
name: loja
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: catalogo
namespace: loja
---
apiVersion: v1
kind: Pod
metadata:
name: catalogo-teste
namespace: loja
spec:
serviceAccountName: catalogo
restartPolicy: Never
containers:
- name: aws-cli
image: public.ecr.aws/aws-cli/aws-cli:latest
command: ["aws", "s3", "ls", "s3://SEU-BUCKET/produtos/"]
kubectl apply -f catalogo.yaml
kubectl logs -n loja catalogo-teste
kubectl get pod catalogo-teste -n loja -o yaml | grep AWS_CONTAINER
O último comando deve mostrar as variáveis AWS_CONTAINER_CREDENTIALS_FULL_URI e AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE, injetadas pelo EKS. Se elas não aparecerem, a association não bate com o namespace e a service account do pod, ou o pod foi criado antes dela.
O mesmo padrão vale para add-ons que precisam de AWS, como o driver CSI do Mountpoint para S3 do post Addon mountpoint aws s3 csi driver: em vez de dar a permissão à role dos nós, que vale para todos os pods do nó, dê a permissão só à service account que precisa dela.
terraform plan
terraform init
terraform validate
terraform plan \
-var 'public_access_cidrs=["SEU_IP/32"]' \
-var 'catalog_bucket_name=SEU-BUCKET' \
-out=tfplan
O código foi validado com terraform validate e terraform plan contra uma conta AWS real, sem apply. Na validação, o nome de bucket usado foi o fictício loja-catalogo-exemplo. São 31 recursos: os 28 do cluster base mais a role, a política inline e a association.
# module.pi_catalogo.aws_eks_pod_identity_association.this["loja/catalogo"] will be created
# module.pi_catalogo.aws_iam_role.this will be created
# module.pi_catalogo.aws_iam_role_policy.inline[0] will be created
...
Plan: 31 to add, 0 to change, 0 to destroy.
No plan, a política inline sai exatamente com as duas permissões:
+ Statement = [
+ {
+ Action = "s3:ListBucket"
+ Condition = {
+ StringLike = {
+ "s3:prefix" = "produtos/*"
}
}
+ Effect = "Allow"
+ Resource = "arn:aws:s3:::loja-catalogo-exemplo"
+ Sid = "ListarProdutos"
},
+ {
+ Action = "s3:GetObject"
+ Effect = "Allow"
+ Resource = "arn:aws:s3:::loja-catalogo-exemplo/produtos/*"
+ Sid = "LerProdutos"
},
]
Outputs do Terraform
O ambiente tem quatro outputs. Como não houve apply, os valores abaixo vêm do bloco Changes to Outputs do plan:
Changes to Outputs:
+ catalogo_role_arn = (known after apply)
+ cluster_name = "eks-blog-castilho"
+ kubeconfig_command = "aws eks update-kubeconfig --region us-east-1 --name eks-blog-castilho"
+ pod_identity_associations = {
+ "loja/catalogo" = (known after apply)
}
cluster_name: nome do cluster EKS, para os comandosaws eks.catalogo_role_arn: ARN da role assumida pelo pod. Depois do apply ele terá o formatoarn:aws:iam::111122223333:role/eks-blog-castilho-catalogo-s3(conta de exemplo). Use para procurar as chamadas do pod no CloudTrail.pod_identity_associations: o ID de cada association, por namespace e service account. Serve para consultar a association comaws eks describe-pod-identity-association --cluster-name eks-blog-castilho --association-id ID.kubeconfig_command: o comando pronto para configurar okubectl. Rode com$(terraform output -raw kubeconfig_command).
O EKS Pod Identity não tem custo próprio. O lab custa o mesmo do cluster base, então destrua com terraform destroy ao terminar.
Conclusão
Com EKS Pod Identity, cada pod recebe só a permissão de que precisa, por credenciais temporárias, sem access key e sem provedor OIDC por cluster. A trust policy é genérica, a ligação fica numa association versionada no Terraform, e a política mínima é revisada no plan antes de chegar à conta.
Esse mesmo mecanismo vai aparecer de novo na série, agora para dar permissões aos add-ons que precisam conversar com a AWS.
Referências oficiais
- Learn how EKS Pod Identity grants pods access to AWS services — AWS Documentation
- Set up the Amazon EKS Pod Identity Agent — AWS Documentation
- IAM roles for service accounts — AWS Documentation
- aws_eks_pod_identity_association — Terraform Registry
Interessado em saber mais sobre esse assunto?