Study guide
Kubernetes, from zero to production.
What Kubernetes is, the problem it solves, the configuration files and their security options, small hands-on labs with Docker on your machine, and how to run it securely on AWS.
O que é o Kubernetes e que problema ele resolveWhat Kubernetes is and the problem it solves
Kubernetes (K8s: o 8 são as letras entre K e s) é um sistema open source, criado no Google e hoje mantido pela CNCF, que roda e cuida de contêineres em várias máquinas como se fossem uma só. O nome vem do grego e significa “timoneiro”: quem conduz o navio. Pense num maestro: cada músico (contêiner) sabe tocar o seu trecho, mas alguém precisa dizer quem toca, quando, e substituir quem faltar.Kubernetes (K8s: the 8 stands for the letters between K and s) is an open source system, created at Google and now maintained by the CNCF, that runs and looks after containers across many machines as if they were one. The name is Greek for “helmsman”: the one who steers the ship. Think of a conductor: each musician (container) knows their part, but someone has to say who plays, when, and replace whoever is missing.
Pré-requisito: saber o que é um contêiner. Uma imagem é o pacote pronto (programa + bibliotecas); um contêiner é essa imagem em execução, isolada do resto da máquina. O Docker constrói e roda contêineres em uma máquina. O Kubernetes entra quando são muitos contêineres em muitas máquinas.Prerequisite: knowing what a container is. An image is the ready-made package (program + libraries); a container is that image running, isolated from the rest of the machine. Docker builds and runs containers on one machine. Kubernetes enters when there are many containers on many machines.
O problemaThe problem
Rodar um contêiner com docker run é fácil. Operar dezenas deles em produção traz perguntas que o Docker sozinho não responde:Running one container with docker run is easy. Operating dozens in production raises questions Docker alone does not answer:
- O contêiner caiu às 3h da manhã. Quem sobe de novo?The container died at 3 a.m. Who starts it again?
- A máquina inteira morreu. Quem move os contêineres para outra?The whole machine died. Who moves the containers to another one?
- Chegou mais tráfego. Como ter 10 cópias em vez de 2, e dividir os acessos entre elas?More traffic arrived. How do you get 10 copies instead of 2, and split the requests among them?
- Versão nova. Como trocar sem derrubar o site, e voltar atrás se der errado?New version. How do you swap without downtime, and go back if it fails?
- Configuração e segredos. Como entregar senhas e variáveis sem colocá-las na imagem?Configuration and secrets. How do you deliver passwords and variables without baking them into the image?
- Quem pode fazer o quê? E quem pode falar com quem dentro do sistema?Who may do what? And who may talk to whom inside the system?
A ideia central: declarativoThe core idea: declarative
Você não manda “suba o contêiner A na máquina 2”. Você escreve um arquivo YAML dizendo como quer que fique (“3 cópias do app A, na versão 1.27”) e o Kubernetes trabalha sem parar para que a realidade fique igual ao arquivo. É a mesma ideia do Terraform, só que para aplicações em execução.You do not say “start container A on machine 2”. You write a YAML file saying how you want things to be (“3 copies of app A, version 1.27”) and Kubernetes works non-stop to make reality match the file. It is the same idea as Terraform, but for running applications.
Como funciona por dentroHow it works inside
Um cluster é um conjunto de máquinas (chamadas nós). Um grupo delas, o control plane, decide; as outras, os workers, executam. Você só conversa com o control plane, pelo kubectl (a linha de comando) que chama o API server.A cluster is a set of machines (called nodes). Some of them, the control plane, decide; the others, the workers, execute. You only talk to the control plane, through kubectl (the command line) calling the API server.
| PeçaPiece | OndeWhere | O que fazWhat it does |
|---|---|---|
| API serverAPI server | control planecontrol plane | Única porta de entrada: autentica, autoriza, valida e grava. kubectl, nós e controllers só falam com ele.The only entry point: authenticates, authorizes, validates and stores. kubectl, nodes and controllers only talk to it. |
| etcdetcd | control planecontrol plane | Banco chave-valor com todo o estado do cluster, inclusive os Secrets. Perdeu o etcd, perdeu o cluster.Key-value database with all cluster state, including Secrets. Lose etcd, lose the cluster. |
| schedulerscheduler | control planecontrol plane | Para cada Pod novo, escolhe o nó que tem espaço (CPU, memória, regras).For each new Pod, picks the node with room (CPU, memory, rules). |
| controller managercontroller manager | control planecontrol plane | Conjunto de “vigias”: cada um compara o estado desejado com o real e corrige.A set of “watchers”: each compares desired state with actual state and fixes it. |
| kubeletkubelet | cada workereach worker | Agente no nó: recebe a lista de Pods do API server e manda o runtime executá-los; reporta a saúde.Agent on the node: receives the Pod list from the API server and tells the runtime to run them; reports health. |
| kube-proxykube-proxy | cada workereach worker | Programa as regras de rede para que um Service chegue aos Pods certos.Programs the network rules so a Service reaches the right Pods. |
| container runtimecontainer runtime | cada workereach worker | Quem de fato roda os contêineres (containerd ou CRI-O).What actually runs the containers (containerd or CRI-O). |
O laço de reconciliaçãoThe reconciliation loop
Tudo no Kubernetes segue o mesmo padrão: cada controller olha o estado desejado (o que você escreveu) e o estado atual (o que existe) e age sobre a diferença, para sempre. Por isso um Pod apagado volta, e por isso você nunca “inicia” coisas à mão.Everything in Kubernetes follows the same pattern: each controller looks at the desired state (what you wrote) and the current state (what exists) and acts on the difference, forever. That is why a deleted Pod comes back, and why you never “start” things by hand.
Os objetos que você vai usarThe objects you will use
Tudo que você cria é um objeto descrito em YAML. Estes são os que cobrem 80% do dia a dia:Everything you create is an object described in YAML. These cover 80% of daily work:
| ObjetoObject | Em uma fraseIn one sentence | AnalogiaAnalogy |
|---|---|---|
| PodPod | A menor unidade: 1 ou mais contêineres que dividem rede e disco. É descartável: morreu, é substituído por outro (com IP novo).The smallest unit: 1 or more containers sharing network and disk. Disposable: when it dies it is replaced by another (with a new IP). | um funcionárioone employee |
| ReplicaSetReplicaSet | Garante que existam N Pods iguais. Raramente você o cria; o Deployment o faz.Ensures N identical Pods exist. You rarely create it; the Deployment does. | o RH que mantém o quadroHR keeping headcount |
| DeploymentDeployment | Descreve o app: imagem, cópias, atualização gradual e rollback. É o objeto mais usado.Describes the app: image, copies, gradual update and rollback. The most-used object. | o gerente do timethe team manager |
| ServiceService | Dá um nome e IP fixos a um grupo de Pods e divide o tráfego entre eles. Os Pods mudam; o Service não.Gives a fixed name and IP to a group of Pods and splits traffic among them. Pods change; the Service does not. | o telefone da empresathe company phone number |
| Ingress / GatewayIngress / Gateway | Regras HTTP(S) de entrada: domínio e caminho para qual Service, com TLS.Inbound HTTP(S) rules: domain and path to which Service, with TLS. | a recepçãothe front desk |
| ConfigMap / SecretConfigMap / Secret | Configuração e dados sensíveis, fora da imagem. O Secret é “aviso de cuidado”, não cofre (veja segurança).Configuration and sensitive data, outside the image. A Secret is a “handle with care” label, not a vault (see security). | o manual / o cofrethe manual / the safe |
| NamespaceNamespace | Divide o cluster em “pastas” isoladas (dev, prod, times), com regras e cotas próprias.Splits the cluster into isolated “folders” (dev, prod, teams), with their own rules and quotas. | departamentosdepartments |
| PersistentVolumeClaimPersistentVolumeClaim | Pedido de disco que sobrevive ao Pod (bancos de dados, uploads).A request for disk that outlives the Pod (databases, uploads). | um armário trancadoa locker |
| Job / CronJobJob / CronJob | Tarefa que roda até terminar (uma vez ou agendada).A task that runs to completion (once or scheduled). | uma tarefa avulsaa one-off task |
| ServiceAccount, Role, RoleBindingServiceAccount, Role, RoleBinding | Identidade e permissões (RBAC) de Pods e pessoas dentro do cluster.Identity and permissions (RBAC) for Pods and people inside the cluster. | crachá e catracabadge and turnstile |
| NetworkPolicyNetworkPolicy | Firewall entre Pods: quem pode falar com quem.Firewall between Pods: who may talk to whom. | portas internas trancadaslocked internal doors |
selector: {app: web}; o Deployment acha os dele pelo mesmo mecanismo. Rótulo diferente entre os dois é o erro nº 1 de iniciante: o Service existe, mas aponta para ninguém.The glue between objects is labels. A Service finds its Pods by selector: {app: web}; the Deployment finds its own the same way. A label mismatch between the two is the #1 beginner error: the Service exists but points at nobody.Os arquivos de configuraçãoThe configuration files
Você trabalha com manifestos: arquivos YAML, um ou vários objetos por arquivo (separados por ---), que vão no Git como código. O comando é sempre kubectl apply -f arquivo.yaml.You work with manifests: YAML files, one or several objects per file (separated by ---), kept in Git as code. The command is always kubectl apply -f file.yaml.
Anatomia: quatro campos em todo objetoAnatomy: four fields in every object
apiVersion: apps/v1 # which API group/version defines this kind
kind: Deployment # the type of object
metadata: # identity
name: web # unique name inside the namespace
namespace: default # optional; defaults to "default"
labels: # free key/values other objects use to find this one
app: web
spec: # the DESIRED state: this is what you write
replicas: 3
# ...
# status: # the ACTUAL state: filled in by the cluster, never by you
Não decore os campos: pergunte ao próprio cluster. kubectl explain deployment.spec.template.spec.securityContext mostra a documentação oficial de qualquer campo, e kubectl get deploy web -o yaml mostra o objeto completo como o cluster o enxerga.Do not memorise fields: ask the cluster itself. kubectl explain deployment.spec.template.spec.securityContext shows the official documentation of any field, and kubectl get deploy web -o yaml shows the whole object as the cluster sees it.
Mapa dos arquivosFile map
| Arquivo (kind)File (kind) | Para quêWhat for | Campos de segurança que importamSecurity fields that matter |
|---|---|---|
DeploymentDeployment | o app: imagem, cópias, atualizaçãothe app: image, copies, rollout | securityContext, resources, image (tag/digest), serviceAccountName, automountServiceAccountTokensecurityContext, resources, image (tag/digest), serviceAccountName, automountServiceAccountToken |
ServiceService | endereço estável para os Podsstable address for the Pods | type: prefira ClusterIP; NodePort/LoadBalancer expõem ao mundotype: prefer ClusterIP; NodePort/LoadBalancer expose to the world |
IngressIngress | entrada HTTP(S) por domínio/caminhoHTTP(S) entry by domain/path | spec.tls (HTTPS), anotações do controller (WAF, restrição de IP)spec.tls (HTTPS), controller annotations (WAF, IP restriction) |
ConfigMapConfigMap | configuração não sensívelnon-sensitive configuration | nada de senha aquino passwords here |
SecretSecret | dados sensíveissensitive data | só base64 (não cifrado); nunca no Git; RBAC restritobase64 only (not encrypted); never in Git; tight RBAC |
NamespaceNamespace | isolamento lógicological isolation | rótulos pod-security.kubernetes.io/*pod-security.kubernetes.io/* labels |
ServiceAccountServiceAccount | identidade do Podthe Pod's identity | automountServiceAccountToken: falseautomountServiceAccountToken: false |
Role / ClusterRole + RoleBindingRole / ClusterRole + RoleBinding | permissões (RBAC)permissions (RBAC) | verbs, resources: mínimo necessário, sem *verbs, resources: the bare minimum, no * |
NetworkPolicyNetworkPolicy | firewall entre Podsfirewall between Pods | podSelector, ingress, egresspodSelector, ingress, egress |
ResourceQuota / LimitRangeResourceQuota / LimitRange | tetos por namespace e por contêinercaps per namespace and per container | impedem um time ou um invasor de consumir o cluster todostop a team or an intruder from eating the whole cluster |
PodDisruptionBudget, HorizontalPodAutoscalerPodDisruptionBudget, HorizontalPodAutoscaler | disponibilidade e escalaavailability and scaling | disponibilidade também é segurançaavailability is security too |
kubeconfig (~/.kube/config)kubeconfig (~/.kube/config) | seu acesso: clusters, usuários, contextosyour access: clusters, users, contexts | equivale a uma senha de administrador; não vai no Gitequals an admin password; never goes in Git |
kustomization.yaml / Helm values.yamlkustomization.yaml / Helm values.yaml | reaproveitar manifestos entre ambientesreuse manifests across environments | valores sensíveis fora do Gitkeep sensitive values out of Git |
Kustomize e Helm: o mesmo app em dev e prodKustomize and Helm: the same app in dev and prod
Copiar YAML de ambiente em ambiente vira bagunça. O Kustomize (já dentro do kubectl) parte de uma base e aplica remendos por ambiente. O Helm usa modelos com values.yaml e é o formato da maioria dos pacotes prontos (Argo CD, ingress, cert-manager).Copying YAML from environment to environment gets messy. Kustomize (built into kubectl) starts from a base and applies per-environment patches. Helm uses templates with values.yaml and is the format of most ready-made packages (Argo CD, ingress, cert-manager).
# overlays/prod/kustomization.yaml
resources:
- ../../base # the shared Deployment + Service
replicas:
- name: web
count: 5 # prod runs 5 copies; dev keeps the base value
images:
- name: nginxinc/nginx-unprivileged
newTag: "1.27" # change the version in ONE place
# kubectl apply -k overlays/prod
kubeconfig: sua chave de acessokubeconfig: your access key
kubectl config get-contexts # which clusters you can reach (* = current)
kubectl config use-context kind-lab # switch cluster
kubectl config current-context # ALWAYS check before apply/delete: wrong cluster = incident
Segurança nos arquivosSecurity in the files
O Kubernetes vem aberto por padrão: um Pod roda como root se você não disser o contrário, fala com qualquer outro Pod, e recebe um token para falar com o API server. A boa notícia: quase toda proteção é uma linha de YAML. Pense em camadas:Kubernetes is open by default: a Pod runs as root unless you say otherwise, talks to any other Pod, and gets a token to talk to the API server. The good news: almost every protection is one line of YAML. Think in layers:
Camada 4 e 3: o securityContext e o PodLayers 4 and 3: the securityContext and the Pod
spec:
serviceAccountName: web
automountServiceAccountToken: false # the app does not call the K8s API: give it no token
securityContext: # Pod level
runAsNonRoot: true # refuses to start if the image wants root
runAsUser: 101
seccompProfile:
type: RuntimeDefault # blocks dangerous system calls
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.27 # fixed tag (better: @sha256:digest)
securityContext: # container level
allowPrivilegeEscalation: false # no setuid tricks to become root
readOnlyRootFilesystem: true # attacker cannot write or drop tools
capabilities:
drop: ["ALL"] # no special Linux powers
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 200m, memory: 128Mi } # a crypto-miner or a leak cannot eat the node
| CampoField | O que impedeWhat it stops |
|---|---|
runAsNonRoot: truerunAsNonRoot: true | Se o invasor sair do app, ele não é root dentro do contêiner; fugir para o nó fica bem mais difícil.If the attacker breaks out of the app, they are not root inside the container; escaping to the node gets much harder. |
allowPrivilegeEscalation: falseallowPrivilegeEscalation: false | Programas com setuid ganhando mais privilégio do que o processo pai.setuid programs gaining more privilege than their parent process. |
readOnlyRootFilesystem: truereadOnlyRootFilesystem: true | Gravar malware ou editar arquivos do sistema. Dê um emptyDir só para as pastas que precisam de escrita (/tmp).Writing malware or editing system files. Give an emptyDir only to folders that need writes (/tmp). |
capabilities.drop: [ALL]capabilities.drop: [ALL] | Poderes de root fatiados do Linux (abrir portas baixas, mudar rede, montar discos). Adicione de volta só o indispensável.Root powers sliced up by Linux (low ports, network changes, mounting disks). Add back only what is indispensable. |
seccompProfile: RuntimeDefaultseccompProfile: RuntimeDefault | Chamadas de sistema perigosas usadas em exploits de kernel.Dangerous system calls used in kernel exploits. |
resources.limitsresources.limits | Um Pod comprometido (ou com bug) esgotar CPU/memória do nó e derrubar os vizinhos.A compromised (or buggy) Pod exhausting the node's CPU/memory and taking neighbours down. |
automountServiceAccountToken: falseautomountServiceAccountToken: false | O token em /var/run/secrets/… ser roubado e usado contra o API server.The token in /var/run/secrets/… being stolen and used against the API server. |
image: repo:1.27 ou @sha256:…image: repo:1.27 or @sha256:… | :latest muda sem aviso. O digest garante os mesmos bytes que você escaneou. Use registro privado e imagePullSecrets.:latest changes silently. A digest guarantees the same bytes you scanned. Use a private registry and imagePullSecrets. |
privileged: true, hostNetwork, hostPID, hostPathprivileged: true, hostNetwork, hostPID, hostPath | Evite. Cada um derruba o isolamento entre o contêiner e o nó. É o primeiro item de qualquer auditoria.Avoid. Each one tears down the isolation between container and node. The first item of any audit. |
Camada 2: o namespace diz o que é permitido (Pod Security)Layer 2: the namespace says what is allowed (Pod Security)
O Pod Security Admission vem ligado no cluster e é controlado por rótulos no Namespace. Há três perfis: privileged (sem limites), baseline (bloqueia o pior) e restricted (as boas práticas acima, obrigatórias). Três modos: enforce (recusa), audit (registra) e warn (avisa o usuário). Ele substituiu o antigo PodSecurityPolicy, removido no Kubernetes 1.25.Pod Security Admission is built into the cluster and controlled by labels on the Namespace. There are three profiles: privileged (no limits), baseline (blocks the worst) and restricted (the best practices above, mandatory). Three modes: enforce (rejects), audit (logs) and warn (warns the user). It replaced the old PodSecurityPolicy, removed in Kubernetes 1.25.
apiVersion: v1
kind: Namespace
metadata:
name: secure
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
Camada 2: RBAC, o mínimo de permissãoLayer 2: RBAC, least privilege
RBAC liga quem (ServiceAccount, usuário, grupo) a o que pode fazer (verbos sobre recursos). Um Role vale em um namespace; um ClusterRole no cluster todo. Regras de ouro: nada de cluster-admin para apps, nada de verbs: ["*"], e secrets só para quem realmente precisa.RBAC ties who (ServiceAccount, user, group) to what they may do (verbs on resources). A Role applies in one namespace; a ClusterRole cluster-wide. Golden rules: no cluster-admin for apps, no verbs: ["*"], and secrets only for those who truly need them.
kind: Role
metadata: { name: pod-reader, namespace: secure }
rules:
- apiGroups: [""] # "" = the core API group
resources: ["pods"]
verbs: ["get", "list", "watch"] # read only
---
kind: RoleBinding # ties the Role to a subject
metadata: { name: pod-reader, namespace: secure }
subjects:
- { kind: ServiceAccount, name: pod-reader, namespace: secure }
roleRef: { kind: Role, name: pod-reader, apiGroup: rbac.authorization.k8s.io }
Camada 2: NetworkPolicy, o firewall internoLayer 2: NetworkPolicy, the internal firewall
Sem nenhuma política, todo Pod fala com todo Pod, de qualquer namespace. Um invasor que entra em um Pod passeia pelo cluster inteiro. O padrão seguro é negar tudo e liberar só o necessário. (O plugin de rede precisa suportar NetworkPolicy; o do EKS, o Calico e o Cilium suportam.)With no policy, every Pod talks to every Pod, from any namespace. An intruder who gets into one Pod roams the whole cluster. The safe default is to deny everything and allow only what is needed. (The network plugin must support NetworkPolicy; EKS's, Calico and Cilium do.)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: secure }
spec:
podSelector: {} # {} = every Pod in the namespace
policyTypes: ["Ingress"] # no ingress rules listed = nothing may enter
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-client-to-web, namespace: secure }
spec:
podSelector: { matchLabels: { app: web } } # applies to the web Pods
policyTypes: ["Ingress"]
ingress:
- from:
- podSelector: { matchLabels: { role: client } }
ports:
- port: 8080
Secrets: o que o base64 não fazSecrets: what base64 does not do
Secret guarda o valor em base64, que é só codificação, não criptografia: echo czNjcjN0 | base64 -d devolve a senha. Quem pode ler o Secret via API (ou ler o etcd) lê a senha. O que realmente protege: RBAC apertado, criptografia do etcd em repouso, e nunca versionar o YAML com o valor.A Secret stores the value in base64, which is just encoding, not encryption: echo czNjcjN0 | base64 -d returns the password. Whoever can read the Secret via the API (or read etcd) reads the password. What really protects it: tight RBAC, etcd encryption at rest, and never committing the YAML with the value.- Monte como arquivo (
volumeMountscomreadOnly: true) em vez de variável de ambiente: variáveis aparecem em dumps de erro, em/proce em logs de depuração.Mount as a file (volumeMountswithreadOnly: true) instead of an environment variable: variables show up in crash dumps, in/procand in debug logs. - Em produção, mantenha o segredo fora do cluster (AWS Secrets Manager, Vault) e sincronize com o External Secrets Operator ou o Secrets Store CSI Driver. No Git, use Sealed Secrets ou SOPS (o valor vai cifrado).In production, keep the secret outside the cluster (AWS Secrets Manager, Vault) and sync it with the External Secrets Operator or the Secrets Store CSI Driver. In Git, use Sealed Secrets or SOPS (the value goes encrypted).
- Rotacione: um segredo que muda é um segredo que, se vazar, expira.Rotate: a secret that changes is one that, if leaked, expires.
Camada 1: o cluster em siLayer 1: the cluster itself
- API server não exposto à internet (ou restrito por IP), autenticação por identidade da nuvem, nada de usuário com senha estática.API server not exposed to the internet (or restricted by IP), authentication through cloud identity, no static-password users.
- Logs de auditoria ligados: quem fez o quê, quando.Audit logs on: who did what, when.
- Nós e Kubernetes atualizados: o projeto lança uma versão menor a cada ~4 meses e dá correções por cerca de 14 meses.Nodes and Kubernetes up to date: the project ships a minor version about every 4 months and backports fixes for roughly 14 months.
- Imagens escaneadas (Trivy, ECR scan) e, idealmente, assinadas (cosign); políticas de admissão (Kyverno, OPA Gatekeeper ou
ValidatingAdmissionPolicy) para barrar manifestos fora do padrão.Images scanned (Trivy, ECR scan) and ideally signed (cosign); admission policies (Kyverno, OPA Gatekeeper orValidatingAdmissionPolicy) to reject manifests that break the standard.
kubescape scan e trivy config . (manifestos), kube-bench (CIS Benchmark do cluster) e kubeconform (valida o YAML). Rode no CI, como o plan do Terraform.Tools to audit what you wrote: kubescape scan and trivy config . (manifests), kube-bench (the cluster's CIS Benchmark) and kubeconform (validates the YAML). Run them in CI, like Terraform's plan.Praticar na sua máquina com DockerPractise on your machine with Docker
Você vai usar o kind (“Kubernetes IN Docker”): ele cria um cluster real em que cada nó é um contêiner Docker. Sem nuvem, sem custo, e se quebrar basta apagar e criar de novo. As atividades são pequenas e cada uma se apoia na anterior. Todos os comandos desta seção foram executados de ponta a ponta num cluster kind.You will use kind (“Kubernetes IN Docker”): it creates a real cluster in which every node is a Docker container. No cloud, no cost, and if it breaks you just delete and recreate it. The activities are small and each builds on the previous one. Every command in this section was run end to end on a kind cluster.
| ## | AtividadeActivity | Você aprendeYou learn |
|---|---|---|
| 00 | Instalar e criar o clusterInstall and create the cluster | nós, kubectl, contextonodes, kubectl, context |
| 11 | Seu primeiro PodYour first Pod | Pod, logs, exec, e por que Pod solto não se curaPod, logs, exec, and why a bare Pod does not heal |
| 22 | Deployment e ServiceDeployment and Service | cópias, autocura, escala, acesso pelo navegadorcopies, self-healing, scaling, browser access |
| 33 | ConfigMap e SecretConfigMap and Secret | configuração fora da imagem; base64 não é cifraconfiguration outside the image; base64 is not encryption |
| 44 | Atualização gradual e rollbackRolling update and rollback | trocar versão sem parar; voltar quando erraswap versions without stopping; go back when it fails |
| 55 | Endurecer o PodHarden the Pod | Pod Security, securityContext, limites, probesPod Security, securityContext, limits, probes |
| 66 | RBACRBAC | permissão mínima e can-ileast privilege and can-i |
| 77 | NetworkPolicyNetworkPolicy | negar tudo e liberar só o necessáriodeny everything and allow only what is needed |
| 88 | LimparClean up | apagar tudodelete everything |
Atividade 0: instalar e criar o clusterActivity 0: install and create the cluster
Pré-requisito: Docker funcionando (docker ps responde). Instale o kind e o kubectl (no macOS: brew install kind kubectl; Linux, abaixo; Windows: winget ou use o WSL2).Prerequisite: Docker working (docker ps answers). Install kind and kubectl (on macOS: brew install kind kubectl; Linux below; Windows: winget or use WSL2).
# Linux (amd64)
curl -Lo kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64
curl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kind kubectl && sudo mv kind kubectl /usr/local/bin/
mkdir k8s-lab && cd k8s-lab
Crie o arquivo kind.yaml: um nó de control plane e dois workers, e uma porta do seu computador (8088) ligada à porta 30080 do cluster, para abrir o app no navegador.Create the file kind.yaml: one control-plane node and two workers, and one port of your computer (8088) wired to the cluster's port 30080, so you can open the app in the browser.
# kind.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: lab
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 30080
hostPort: 8088
listenAddress: "127.0.0.1" # only your machine, not the whole network
- role: worker
- role: worker
kind create cluster --config kind.yaml
kubectl get nodes
Resultado esperado: três nós Ready (a versão depende do kind instalado).Expected result: three Ready nodes (the version depends on your kind).
NAME STATUS ROLES AGE VERSION
lab-control-plane Ready control-plane 34s v1.37.0
lab-worker Ready <none> 19s v1.37.0
lab-worker2 Ready <none> 20s v1.37.0
Curiosidade: docker ps mostra três contêineres, um por nó, e kubectl get pods -n kube-system mostra as peças da seção “Como funciona por dentro” rodando como Pods (etcd, kube-apiserver, kube-scheduler…).Curiosity: docker ps shows three containers, one per node, and kubectl get pods -n kube-system shows the pieces from “How it works inside” running as Pods (etcd, kube-apiserver, kube-scheduler…).
8088 já estiver em uso, o kind falha com “port is already allocated”. Troque o hostPort por outro número e use o mesmo nas atividades seguintes.If port 8088 is already in use, kind fails with “port is already allocated”. Change hostPort to another number and use it in the following activities.Atividade 1: seu primeiro PodActivity 1: your first Pod
kubectl run hello --image=nginx:1.27
kubectl get pods -o wide # which node it landed on, and its IP
kubectl logs hello # the container's output
kubectl exec hello -- nginx -v # run a command inside it
kubectl delete pod hello
kubectl get pods # gone for good
Resultado esperado: o Pod passa a Running, o exec mostra a versão do nginx, e depois de apagado ele não volta.Expected result: the Pod becomes Running, exec prints the nginx version, and once deleted it does not come back.
NAME READY STATUS RESTARTS AGE IP NODE
hello 1/1 Running 0 32s 10.244.2.2 lab-worker
nginx version: nginx/1.27.5
pod "hello" deleted from default namespace
No resources found in default namespace.
Lição: um Pod solto não tem ninguém cuidando dele. Para a autocura você precisa de um controller, o Deployment.Lesson: a bare Pod has nobody looking after it. For self-healing you need a controller, the Deployment.
Atividade 2: Deployment e ServiceActivity 2: Deployment and Service
Usaremos a imagem nginxinc/nginx-unprivileged, que já roda sem root e escuta na porta 8080 (a atividade 5 depende disso). Crie web.yaml:We will use the nginxinc/nginx-unprivileged image, which already runs without root and listens on port 8080 (activity 5 depends on that). Create web.yaml:
# web.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 3 # desired state: 3 copies
selector:
matchLabels:
app: web # "my Pods are the ones labelled app=web"
template: # the Pod template
metadata:
labels:
app: web # MUST match the selector above
spec:
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.27
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
type: NodePort # lab only: opens a port on the nodes
selector:
app: web # sends traffic to Pods labelled app=web
ports:
- port: 80 # the Service's port
targetPort: 8080 # the container's port
nodePort: 30080 # the port kind forwards to localhost:8088
kubectl apply -f web.yaml
kubectl rollout status deploy/web
kubectl get deploy,rs,pods
curl -s -o /dev/null -w "%{http_code}\n" localhost:8088 # or open http://localhost:8088
Resultado esperado: 3/3 prontos, um ReplicaSet e três Pods, e 200 do curl.Expected result: 3/3 ready, one ReplicaSet and three Pods, and 200 from curl.
NAME READY UP-TO-DATE AVAILABLE
deployment.apps/web 3/3 3 3
NAME READY STATUS RESTARTS AGE
pod/web-7bf55664b5-6qznz 1/1 Running 0 39s
pod/web-7bf55664b5-mcxwf 1/1 Running 0 39s
pod/web-7bf55664b5-vp2ws 1/1 Running 0 39s
200
Agora a autocura. Apague um Pod (use um nome da sua lista) e olhe de novo, rápido:Now self-healing. Delete a Pod (use a name from your list) and look again, quickly:
kubectl delete pod web-7bf55664b5-6qznz
kubectl get pods # a new Pod is already being created
kubectl scale deploy/web --replicas=5 # scale up
kubectl get pods # 5 Pods
kubectl scale deploy/web --replicas=3
Resultado esperado: assim que um Pod entra em Terminating, outro nasce em ContainerCreating: o laço de reconciliação em ação.Expected result: as soon as one Pod goes Terminating, another is born in ContainerCreating: the reconciliation loop in action.
NAME READY STATUS RESTARTS AGE
web-7bf55664b5-6qznz 1/1 Terminating 0 41s
web-7bf55664b5-7tq5l 0/1 ContainerCreating 0 1s
web-7bf55664b5-mcxwf 1/1 Running 0 41s
web-7bf55664b5-vp2ws 1/1 Running 0 41s
Desafio: troque o rótulo do Service para app: errado, aplique e rode kubectl get endpoints web. A lista fica vazia e o curl falha: é o erro nº 1 de rótulos que vimos antes. Desfaça com kubectl apply -f web.yaml.Challenge: change the Service's label to app: wrong, apply and run kubectl get endpoints web. The list is empty and curl fails: the #1 label error we saw earlier. Undo with kubectl apply -f web.yaml.
Atividade 3: ConfigMap e SecretActivity 3: ConfigMap and Secret
kubectl create configmap web-config --from-literal=GREETING="hello from k8s"
kubectl create secret generic web-secret --from-literal=API_KEY=s3cr3t
# inject both as environment variables in the Deployment (this triggers a new rollout)
kubectl set env deploy/web --from=configmap/web-config
kubectl set env deploy/web --from=secret/web-secret
kubectl rollout status deploy/web
kubectl exec deploy/web -- printenv GREETING API_KEY
kubectl get secret web-secret -o jsonpath='{.data.API_KEY}'; echo
kubectl get secret web-secret -o jsonpath='{.data.API_KEY}' | base64 -d; echo
Resultado esperado: o Pod enxerga os dois valores; o Secret guardado é só base64, e qualquer um com acesso de leitura recupera a senha.Expected result: the Pod sees both values; the stored Secret is just base64, and anyone with read access recovers the password.
hello from k8s
s3cr3t
czNjcjN0
s3cr3t
Lição: a configuração ficou fora da imagem (a mesma imagem serve a dev e prod), mas o Secret não está protegido por ser Secret. Quem protege é o RBAC (atividade 6) e, em produção, o cofre externo.Lesson: the configuration lives outside the image (the same image serves dev and prod), but the Secret is not protected just for being a Secret. RBAC (activity 6) and, in production, an external vault protect it.
Atividade 4: atualização gradual e rollbackActivity 4: rolling update and rollback
kubectl set image deploy/web web=nginxinc/nginx-unprivileged:1.28
kubectl rollout status deploy/web # old Pods leave as new ones become ready
kubectl rollout history deploy/web
# now ship a broken version on purpose
kubectl set image deploy/web web=nginxinc/nginx-unprivileged:nao-existe
kubectl get pods # one new Pod in ErrImagePull; the 3 old ones still serve
curl -s -o /dev/null -w "%{http_code}\n" localhost:8088 # still 200
kubectl rollout undo deploy/web # go back to the last good version
kubectl rollout status deploy/web
Resultado esperado: a versão quebrada nunca derruba o site, porque o Kubernetes só tira um Pod antigo quando o novo fica pronto. O undo restaura a versão boa.Expected result: the broken version never takes the site down, because Kubernetes only removes an old Pod once the new one is ready. undo restores the good version.
NAME READY STATUS RESTARTS AGE
web-6b6f99c975-86zgm 0/1 ErrImagePull 0 16s
web-7db97c8d9-2blnr 1/1 Running 0 65s
web-7db97c8d9-nn5t6 1/1 Running 0 41s
web-7db97c8d9-t9256 1/1 Running 0 18s
Em equipe, o undo manual é o plano B: o normal é reverter o commit no Git e deixar o pipeline reaplicar.On a team, the manual undo is plan B: normally you revert the commit in Git and let the pipeline reapply.
Atividade 5: endurecer o PodActivity 5: harden the Pod
Crie um namespace que só aceita Pods que seguem o perfil restricted e tente subir um Pod comum:Create a namespace that only accepts Pods following the restricted profile and try to start an ordinary Pod:
# secure-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
name: secure
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
kubectl apply -f secure-ns.yaml
kubectl run bad --image=nginx:1.27 -n secure
Resultado esperado: o cluster recusa e diz exatamente o que faltou, o que é um roteiro do que escrever.Expected result: the cluster refuses and says exactly what is missing, which is a to-do list of what to write.
Error from server (Forbidden): pods "bad" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "bad" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "bad" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "bad" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "bad" must set securityContext.seccompProfile.type to
"RuntimeDefault" or "Localhost")
Agora a versão endurecida, web-secure.yaml, com os campos da seção de segurança mais probes (o Kubernetes só manda tráfego quando o readinessProbe responde, e reinicia o contêiner se o livenessProbe falhar):Now the hardened version, web-secure.yaml, with the fields from the security section plus probes (Kubernetes only sends traffic once the readinessProbe answers, and restarts the container if the livenessProbe fails):
# web-secure.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: web
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
labels:
app: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
serviceAccountName: web
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 101
runAsGroup: 101
seccompProfile:
type: RuntimeDefault
containers:
- name: web
image: nginxinc/nginx-unprivileged:1.27
ports:
- containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
readinessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /
port: 8080
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: tmp
mountPath: /tmp # the only writable folder
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: web
spec:
selector:
app: web
ports:
- port: 80
targetPort: 8080 # ClusterIP (default): reachable only inside the cluster
kubectl apply -n secure -f web-secure.yaml
kubectl rollout status deploy/web -n secure
kubectl exec -n secure deploy/web -- sh -c 'id; touch /etc/x; touch /tmp/ok && echo tmp-ok; ls /var/run/secrets'
Resultado esperado: o Pod passa pelo namespace restricted; roda como usuário 101 (não root); não consegue escrever fora de /tmp; e não tem token do Kubernetes.Expected result: the Pod passes the restricted namespace; runs as user 101 (not root); cannot write outside /tmp; and has no Kubernetes token.
uid=101(nginx) gid=101(nginx) groups=101(nginx)
touch: cannot touch '/etc/x': Read-only file system
tmp-ok
ls: cannot access '/var/run/secrets': No such file or directory
command terminated with exit code 2 # the last error is expected: that folder does not exist
Deployment inseguro num namespace restrito, o Deployment é criado, mas os Pods são recusados. Se um Deployment mostra 0/3, rode kubectl describe rs ou kubectl get events para ver o motivo.Tip: when you apply an insecure Deployment to a restricted namespace, the Deployment is created but its Pods are rejected. If a Deployment shows 0/3, run kubectl describe rs or kubectl get events to see why.Atividade 6: RBACActivity 6: RBAC
# rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: pod-reader
namespace: secure
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: secure
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: pod-reader
namespace: secure
subjects:
- kind: ServiceAccount
name: pod-reader
namespace: secure
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
kubectl apply -f rbac.yaml
SA=system:serviceaccount:secure:pod-reader
kubectl auth can-i list pods -n secure --as=$SA
kubectl auth can-i delete pods -n secure --as=$SA
kubectl auth can-i get secrets -n secure --as=$SA
kubectl auth can-i list pods -n default --as=$SA
Resultado esperado: só a ação liberada, no namespace liberado, responde yes.Expected result: only the allowed action, in the allowed namespace, answers yes.
yes
no
no
no
kubectl auth can-i --list --as=$SA -n secure mostra tudo o que uma identidade pode fazer. É o primeiro comando de uma revisão de permissões.kubectl auth can-i --list --as=$SA -n secure shows everything an identity can do. It is the first command of a permissions review.
Atividade 7: NetworkPolicyActivity 7: NetworkPolicy
Suba dois Pods de teste no namespace secure: um com o rótulo role=client e outro sem. Como o namespace é restricted, eles também precisam do securityContext. Crie clients.yaml:Start two test Pods in the secure namespace: one with the label role=client and one without. Since the namespace is restricted, they also need the securityContext. Create clients.yaml:
# clients.yaml
apiVersion: v1
kind: Pod
metadata:
name: client
namespace: secure
labels:
role: client
spec:
securityContext: { runAsNonRoot: true, runAsUser: 1000, seccompProfile: { type: RuntimeDefault } }
containers:
- name: client
image: busybox:1.37
command: ["sleep", "3600"]
securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } }
---
apiVersion: v1
kind: Pod
metadata:
name: other
namespace: secure
spec:
securityContext: { runAsNonRoot: true, runAsUser: 1000, seccompProfile: { type: RuntimeDefault } }
containers:
- name: other
image: busybox:1.37
command: ["sleep", "3600"]
securityContext: { allowPrivilegeEscalation: false, capabilities: { drop: ["ALL"] } }
kubectl apply -f clients.yaml
kubectl wait -n secure --for=condition=Ready pod/client pod/other
# BEFORE any policy: both reach web
kubectl exec -n secure client -- wget -qO- -T 3 http://web | head -2
kubectl exec -n secure other -- wget -qO- -T 3 http://web | head -2
Salve as duas políticas da seção de segurança em netpol.yaml (a default-deny e a allow-client-to-web) e aplique:Save the two policies from the security section as netpol.yaml (default-deny and allow-client-to-web) and apply them:
kubectl apply -f netpol.yaml
kubectl exec -n secure client -- wget -qO- -T 3 http://web | head -2 # allowed
kubectl exec -n secure other -- wget -qO- -T 3 http://web | head -2 # blocked
Resultado esperado: antes, os dois recebem o HTML; depois, só o client recebe, e o other fica sem resposta até o tempo esgotar.Expected result: before, both get the HTML; after, only client does, and other gets no answer until it times out.
# client (allowed)
<!DOCTYPE html>
<html>
# other (blocked)
wget: download timed out
command terminated with exit code 1
Bônus: fechar também a saída (egress)Bonus: also close the outbound side (egress)
Negar a saída impede um Pod comprometido de “ligar para casa” (exfiltrar dados, baixar ferramentas). Mas ao negar tudo você também bloqueia o DNS, e nada mais resolve nomes (o erro é bad address 'web'). Por isso liberamos o DNS e depois cada destino necessário:Denying outbound traffic stops a compromised Pod from “phoning home” (exfiltrating data, downloading tools). But denying everything also blocks DNS, and nothing resolves names any more (the error is bad address 'web'). So we allow DNS first and then each needed destination:
# egress.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny-egress, namespace: secure }
spec:
podSelector: {}
policyTypes: ["Egress"]
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: allow-dns, namespace: secure }
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { port: 53, protocol: UDP }
- { port: 53, protocol: TCP }
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: client-egress-to-web, namespace: secure }
spec:
podSelector: { matchLabels: { role: client } }
policyTypes: ["Egress"]
egress:
- to:
- podSelector: { matchLabels: { app: web } }
ports:
- port: 8080
kubectl apply -f egress.yaml
kubectl exec -n secure client -- wget -qO- -T 3 http://web | head -2 # still works
kubectl exec -n secure client -- wget -qO- -T 3 http://example.com | head -2 # blocked
Atividade 8: limparActivity 8: clean up
kind delete cluster --name lab # removes the three node containers; nothing else is left behind
E depois? Um roteiro de estudoWhat next? A study path
- Repita as atividades sem copiar, escrevendo os YAMLs com a ajuda de
kubectl explainekubectl create ... --dry-run=client -o yaml.Repeat the activities without copying, writing the YAMLs with the help ofkubectl explainandkubectl create ... --dry-run=client -o yaml. - Quebre de propósito: imagem inexistente, rótulo errado, limite de memória baixo demais (
OOMKilled), probe errado. Aprenda a lerkubectl describe,logseget events.Break things on purpose: missing image, wrong label, memory limit too low (OOMKilled), wrong probe. Learn to readkubectl describe,logsandget events. - Suba um app seu: um Deployment, Service, ConfigMap e um PersistentVolumeClaim para o banco (use o Postgres da outra nota).Deploy an app of your own: a Deployment, Service, ConfigMap and a PersistentVolumeClaim for the database (use the Postgres from the other note).
- Instale um Ingress/Gateway e um
cert-manager; empacote o app com Kustomize ou Helm.Install an Ingress/Gateway andcert-manager; package the app with Kustomize or Helm. - Passe os manifestos por
kubescapeoutrivy confige corrija os achados.Run the manifests throughkubescapeortrivy configand fix the findings. - Preparação formal: as provas CKA, CKAD e CKS (segurança) da Linux Foundation, e o simulador killer.sh, são o melhor treino prático.Formal preparation: the Linux Foundation's CKA, CKAD and CKS (security) exams, and the killer.sh simulator, are the best hands-on practice.
Colocar em produção com segurança na AWS (EKS)Running it in production, securely, on AWS (EKS)
O serviço gerenciado da AWS é o EKS (Elastic Kubernetes Service): a AWS opera o control plane (alta disponibilidade em várias zonas, atualizações, backup do etcd) e você cuida dos nós e dos apps. O modelo de responsabilidade compartilhada vale aqui: a AWS protege o control plane; a configuração do seu cluster e dos seus Pods é com você.AWS's managed service is EKS (Elastic Kubernetes Service): AWS runs the control plane (multi-zone high availability, upgrades, etcd backups) and you look after the nodes and the apps. The shared-responsibility model applies: AWS protects the control plane; your cluster's and Pods' configuration is on you.
eksctl delete cluster). Defina um alerta de orçamento (AWS Budgets) antes de começar.Cost. EKS charges for the control plane by the hour (about US$ 0.10/h, roughly US$ 73 a month, check the current price list), plus the EC2 nodes, the NAT gateway and the ALB. Create the study cluster, practise and delete it the same day (eksctl delete cluster). Set a budget alert (AWS Budgets) before you begin.Passo 1: o cluster, já com os controles de segurançaStep 1: the cluster, with security controls from day one
O jeito mais curto de aprender é o eksctl, que lê um arquivo e cria VPC, control plane e nós. Em produção real, descreva a mesma coisa em Terraform (módulos terraform-aws-modules/eks). Os comentários mostram o que cada linha protege:The shortest way to learn is eksctl, which reads a file and creates the VPC, control plane and nodes. In real production, describe the same thing in Terraform (the terraform-aws-modules/eks modules). The comments show what each line protects:
# cluster.yaml
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: demo
region: us-east-1
version: "1.35" # use a version currently in standard support
accessConfig:
authenticationMode: API # access via IAM "access entries"; no legacy aws-auth ConfigMap
vpc:
clusterEndpoints:
privateAccess: true
publicAccess: true # best for production: false + VPN/bastion
publicAccessCIDRs: ["203.0.113.10/32"] # replace with YOUR IP: only it can reach the API server
secretsEncryption:
keyARN: arn:aws:kms:us-east-1:111122223333:key/REPLACE-ME # your own KMS key for Secrets
cloudWatch:
clusterLogging:
enableTypes: ["api", "audit", "authenticator", "controllerManager", "scheduler"]
iam:
withOIDC: true # lets Pods assume IAM roles (IRSA)
addons:
- name: vpc-cni
configurationValues: '{"enableNetworkPolicy": "true"}' # NetworkPolicy enforcement
- name: eks-pod-identity-agent # IAM roles for Pods (Pod Identity)
managedNodeGroups:
- name: ng-private
instanceType: t3.medium
desiredCapacity: 2
minSize: 2
maxSize: 4
privateNetworking: true # nodes in private subnets, no public IP
volumeEncrypted: true # encrypted disks
eksctl create cluster -f cluster.yaml # ~15-20 minutes
aws eks update-kubeconfig --name demo --region us-east-1 # writes your kubeconfig (short-lived tokens via IAM)
kubectl get nodes
kubectl do EKS não usa senha: o kubeconfig chama aws eks get-token e o acesso segue as suas credenciais da AWS (SSO, de preferência). Quem entra no cluster é decidido por access entries do IAM, com políticas como AmazonEKSViewPolicy; evite dar cluster-admin a todo mundo.EKS's kubectl uses no password: the kubeconfig calls aws eks get-token and access follows your AWS credentials (SSO, ideally). Who gets into the cluster is decided by IAM access entries, with policies such as AmazonEKSViewPolicy; avoid giving cluster-admin to everyone.Passo 2: imagens no ECRStep 2: images in ECR
aws ecr create-repository --repository-name web \
--image-scanning-configuration scanOnPush=true \
--image-tag-mutability IMMUTABLE \
--encryption-configuration encryptionType=KMS
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 111122223333.dkr.ecr.us-east-1.amazonaws.com
docker build -t 111122223333.dkr.ecr.us-east-1.amazonaws.com/web:1.0.0 .
docker push 111122223333.dkr.ecr.us-east-1.amazonaws.com/web:1.0.0
Tags imutáveis impedem que a 1.0.0 seja sobrescrita; o scan na publicação aponta CVEs conhecidas. No manifesto, referencie a imagem pelo digest (web@sha256:…). Em CI, o próprio pipeline faz o push, com OIDC (veja a nota GitHub to AWS with OIDC), sem chave guardada.Immutable tags stop 1.0.0 from being overwritten; scan-on-push flags known CVEs. In the manifest, reference the image by digest (web@sha256:…). In CI, the pipeline itself does the push, with OIDC (see the GitHub to AWS with OIDC note), with no stored key.
Passo 3: o app com identidade própria na AWSStep 3: the app with its own AWS identity
Um Pod que precisa ler um bucket S3 ou um segredo não deve usar as credenciais do nó (que serviriam a todos os Pods) nem chaves fixas. Crie uma IAM role só com o que ele precisa e ligue-a à ServiceAccount:A Pod that must read an S3 bucket or a secret should not use the node's credentials (which would serve every Pod) nor static keys. Create an IAM role with only what it needs and tie it to the ServiceAccount:
# IRSA: the ServiceAccount carries the role to assume
apiVersion: v1
kind: ServiceAccount
metadata:
name: web
namespace: prod
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/web-s3-read
automountServiceAccountToken: true # IRSA needs the projected token; set false only for Pods with no AWS access
A alternativa mais nova, o EKS Pod Identity, dispensa a anotação: aws eks create-pod-identity-association --cluster-name demo --namespace prod --service-account web --role-arn …. Nos dois casos a role aceita só aquela ServiceAccount daquele namespace.The newer alternative, EKS Pod Identity, drops the annotation: aws eks create-pod-identity-association --cluster-name demo --namespace prod --service-account web --role-arn …. In both cases the role accepts only that ServiceAccount in that namespace.
Passo 4: expor com HTTPSStep 4: expose it with HTTPS
Instale o AWS Load Balancer Controller (Helm). Ele transforma um Ingress em um ALB. Certificado no ACM, HTTPS e, se quiser, WAF:Install the AWS Load Balancer Controller (Helm). It turns an Ingress into an ALB. Certificate in ACM, HTTPS and, if you want, WAF:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: prod
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:us-east-1:111122223333:certificate/REPLACE-ME
alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS13-1-2-2021-06
# alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:... # optional WAF
spec:
ingressClassName: alb
rules:
- host: app.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service: { name: web, port: { number: 80 } }
Mantenha os Services como ClusterIP e as subnets dos nós privadas: só o ALB fica na internet. Aponte seu domínio para o ALB (Route 53 ou Cloudflare).Keep Services as ClusterIP and the node subnets private: only the ALB faces the internet. Point your domain at the ALB (Route 53 or Cloudflare).
Passo 5: entregar com pipelineStep 5: deliver through a pipeline
O ideal é GitOps: o Git é a fonte da verdade e uma ferramenta dentro do cluster (Argo CD ou Flux) aplica o que está lá. Ninguém precisa de kubectl com permissão de escrita em produção, e todo deploy vira um commit auditável.The ideal is GitOps: Git is the source of truth and a tool inside the cluster (Argo CD or Flux) applies what is there. Nobody needs write-enabled kubectl in production, and every deploy becomes an auditable commit.
Lista de verificação de produçãoProduction checklist
| ÁreaArea | O que fazerWhat to do |
|---|---|
| Acesso ao clusterCluster access | Endpoint do API server privado ou restrito por IP; access entries do IAM com menor privilégio; SSO; nenhum usuário com chave fixa.Private or IP-restricted API server endpoint; least-privilege IAM access entries; SSO; no static-key users. |
| RedeNetwork | Nós em subnets privadas, em 2+ zonas; só o ALB público; Security Groups mínimos; NetworkPolicy default deny em cada namespace; VPC endpoints para ECR/S3/STS.Nodes in private subnets, in 2+ zones; only the ALB public; minimal Security Groups; default deny NetworkPolicy in every namespace; VPC endpoints for ECR/S3/STS. |
| NósNodes | Managed node groups com AMI atualizada; IMDSv2 obrigatório e hop limit = 1 (Pod não alcança as credenciais do nó); discos cifrados; sem SSH (use SSM).Managed node groups with a current AMI; IMDSv2 required and hop limit = 1 (Pods cannot reach the node's credentials); encrypted disks; no SSH (use SSM). |
| PodsPods | Pod Security restricted como enforce nos namespaces de apps; securityContext, limites e probes em todo Deployment; PodDisruptionBudget e spread entre zonas.Pod Security restricted as enforce on app namespaces; securityContext, limits and probes on every Deployment; PodDisruptionBudget and zone spread. |
| Identidade dos PodsPod identity | Uma ServiceAccount e uma IAM role por app (Pod Identity/IRSA), com política mínima; nunca chaves de acesso em Secret.One ServiceAccount and one IAM role per app (Pod Identity/IRSA), minimal policy; never access keys in a Secret. |
| SegredosSecrets | Valores no Secrets Manager/Parameter Store, sincronizados por External Secrets ou CSI Driver; envelope encryption do EKS com sua chave KMS; RBAC sem get secrets amplo.Values in Secrets Manager/Parameter Store, synced by External Secrets or the CSI Driver; EKS envelope encryption with your KMS key; RBAC without broad get secrets. |
| ImagensImages | ECR privado, tags imutáveis, scan na publicação (e contínuo com Inspector), digest nos manifestos, imagens mínimas (distroless/alpine), assinatura com cosign + política de admissão.Private ECR, immutable tags, scan on push (and continuous with Inspector), digests in manifests, minimal images (distroless/alpine), cosign signing + an admission policy. |
| PolíticasPolicies | Kyverno, Gatekeeper ou ValidatingAdmissionPolicy barrando :latest, privileged, hostPath e imagens de fora do seu ECR.Kyverno, Gatekeeper or ValidatingAdmissionPolicy blocking :latest, privileged, hostPath and images from outside your ECR. |
| Observabilidade e detecçãoObservability and detection | Logs do control plane (principalmente audit) no CloudWatch; GuardDuty com proteção para EKS; métricas e alertas (Prometheus/CloudWatch Container Insights).Control-plane logs (especially audit) in CloudWatch; GuardDuty with EKS protection; metrics and alerts (Prometheus/CloudWatch Container Insights). |
| AtualizaçõesUpgrades | Acompanhe o calendário de versões do EKS; atualize o control plane e depois os nós, em dev antes de prod; versões fora de suporte padrão custam mais e deixam de receber correções.Follow the EKS version calendar; upgrade the control plane and then the nodes, dev before prod; versions out of standard support cost more and stop getting fixes. |
| Dados e recuperaçãoData and recovery | Manifestos no Git (recria o cluster); Velero ou AWS Backup para volumes; bancos preferencialmente em RDS, fora do cluster; teste a restauração.Manifests in Git (recreates the cluster); Velero or AWS Backup for volumes; databases preferably on RDS, outside the cluster; test the restore. |
| CustoCost | Tags de custo, requests/limits realistas, autoscaling (Karpenter ou Cluster Autoscaler + HPA), AWS Budgets.Cost tags, realistic requests/limits, autoscaling (Karpenter or Cluster Autoscaler + HPA), AWS Budgets. |
LoadBalancer exposto sem querer, e um Pod root com token de ServiceAccount poderoso. As três linhas que mais valem: NetworkPolicy default deny, Pod Security restricted e RBAC mínimo.The most common production failure is not a sophisticated attack: it is a dashboard, API server or LoadBalancer Service exposed by accident, and a root Pod with a powerful ServiceAccount token. The three lines that pay off most: default-deny NetworkPolicy, restricted Pod Security and minimal RBAC.# When you are done studying: remove everything (control plane, nodes, VPC, NAT) to stop the billing
eksctl delete cluster -f cluster.yaml
Cola e revisãoCheat sheet and review
Comandos do dia a diaEveryday commands
| Quero…I want… | UsoI use |
|---|---|
| aplicar um arquivo / uma pastaapply a file / a folder | kubectl apply -f arquivo.yaml / -f pasta/ / -k overlay/kubectl apply -f file.yaml / -f folder/ / -k overlay/ |
| ver recursossee resources | kubectl get pods,svc,deploy -n NS (-A = todos os namespaces, -o wide, -o yaml)kubectl get pods,svc,deploy -n NS (-A = all namespaces, -o wide, -o yaml) |
| entender por que não sobeunderstand why it does not start | kubectl describe pod X · kubectl get events --sort-by=.lastTimestampkubectl describe pod X · kubectl get events --sort-by=.lastTimestamp |
| ver logssee logs | kubectl logs X · -f (seguir) · --previous (contêiner que reiniciou)kubectl logs X · -f (follow) · --previous (the container that restarted) |
| entrar no contêinerget inside the container | kubectl exec -it X -- shkubectl exec -it X -- sh |
| testar sem exportest without exposing | kubectl port-forward svc/web 8080:80kubectl port-forward svc/web 8080:80 |
| escalar / atualizar / voltarscale / update / roll back | kubectl scale · set image · rollout status|history|undokubectl scale · set image · rollout status|history|undo |
| gerar um YAML-basegenerate a starter YAML | kubectl create deploy web --image=nginx --dry-run=client -o yamlkubectl create deploy web --image=nginx --dry-run=client -o yaml |
| ver a diferença antes de aplicarsee the diff before applying | kubectl diff -f arquivo.yamlkubectl diff -f file.yaml |
| consultar a documentação de um campolook up a field's docs | kubectl explain pod.spec.containers.securityContextkubectl explain pod.spec.containers.securityContext |
| checar permissõescheck permissions | kubectl auth can-i VERBO RECURSO --as=…kubectl auth can-i VERB RESOURCE --as=… |
estado de erro comum: CrashLoopBackOffcommon error: CrashLoopBackOff | o app morre ao iniciar: logs --previousthe app dies on start: logs --previous |
ImagePullBackOff / Pending / OOMKilledImagePullBackOff / Pending / OOMKilled | imagem/credencial errada · sem recurso ou sem nó que sirva · limite de memória baixowrong image/credential · no resources or no fitting node · memory limit too low |
Perguntas de revisãoReview questions
- Que problema o Kubernetes resolve que o
docker runsozinho não resolve?What problem does Kubernetes solve thatdocker runalone does not? - Por que dizemos que ele é declarativo e o que é o laço de reconciliação?Why do we say it is declarative, and what is the reconciliation loop?
- Qual a diferença entre Pod, Deployment e Service? Por que um Pod solto não se cura?What is the difference between a Pod, a Deployment and a Service? Why does a bare Pod not heal?
- O que acontece se o
selectordo Service não casa com os rótulos dos Pods?What happens if the Service'sselectordoes not match the Pods' labels? - Por que um Secret não é seguro só por ser Secret? O que de fato o protege?Why is a Secret not safe just for being a Secret? What actually protects it?
- Cite cinco campos de
securityContext/Pod e o ataque que cada um dificulta.Name fivesecurityContext/Pod fields and the attack each one hinders. - O que o Pod Security Admission faz e quais são seus três perfis?What does Pod Security Admission do and what are its three profiles?
- Por que usar NetworkPolicy default deny, e por que ao negar a saída é preciso liberar o DNS?Why use a default deny NetworkPolicy, and why must DNS be allowed when egress is denied?
- No EKS, como um Pod acessa o S3 sem chave de acesso guardada?On EKS, how does a Pod reach S3 without a stored access key?
- Por que os nós ficam em subnets privadas e só o ALB é público?Why do the nodes sit in private subnets while only the ALB is public?
- Kubernetes docs: Overview and concepts
- Kubernetes docs: Cluster architecture
- Kubernetes docs: Security concepts
- Kubernetes docs: Pod Security Standards
- Kubernetes docs: Configure a Security Context
- Kubernetes docs: RBAC
- Kubernetes docs: Network Policies
- Kubernetes docs: Secrets (good practices)
- kind: Quick start
- AWS: Amazon EKS Best Practices Guide
- AWS: Amazon EKS User Guide
- eksctl
- NSA/CISA: Kubernetes Hardening Guidance
- CIS Kubernetes Benchmark