O Projeto final consiste na resolução do desafio PICK 2024_01 proposto pelo @Badtuxx. O desafio consiste em um projeto prático de DevOps focado na aplicação de gestão de senhas Giropops-Senhas. O projeto exige Stacks com ferramentas de containerização, orquestração, segurança e monitoramento, com foco em ambientes seguros, CI/CD, automatizados e observáveis com alta disponibilidade.
Objetivos principais:
Docker: Criação e publicação da imagem da aplicação.
Cosign: Assinatura e verificação de imagens Docker.
Helm: Deploy automatizado da aplicação em cluster Kubernetes.
Kubernetes: Estrutura de namespaces para ambientes (dev, staging, prod).
GitHub Actions (Self-Hosted): CI/CD 100% automatizado e seguro, executado localmente.
Kyverno: Políticas de segurança para validar apenas imagens assinadas.
APKO & Melange: Distribuição da aplicação empacotada como apk.
Locust: Testes de carga injetados no cluster.
Grafana + Prometheus: Monitoramento e visualização em tempo real da aplicação.
No meu projeto, o Cluster irá rodar localmente em uma VM no Hyper-V.
| VM | CPU | RAM | OS | CLUSTER KUBERNETES |
|---|---|---|---|---|
| Hyper-V | Xeon 2360 4 Core | 17GB | Rocky Linux | KinD |
Rede:
Irei configurar o IP 192.168.1.81/24 de forma estática na subrede 192.168.1.x junto com meu gateway, o DNS será do Google (8.8.8.8).
nmcli con mod eth0 ipv4.addresses 192.168.1.81/24
nmcli con mod eth0 ipv4.gateway 192.168.1.254
nmcli con mod eth0 ipv4.dns 8.8.8.8
nmcli con mod eth0 ipv4.method manual
Reiniciando a conexão para aplicar as alterações.
nmcli con down eth0 && nmcli con up eth0
Visualizando:
ip a show eth0 nmcli dev show eth0
Rede Configurada. -----------------------------------------------------------------
Atualizando o OS.
dnf update -y
Instale o Git:
sudo dnf install -y git
Docker é uma plataforma que facilita criar, empacotar e rodar aplicações dentro de containers. Um container é um ambiente leve, portátil e isolado que roda uma aplicação e suas dependências (bibliotecas, arquivos de configuração, etc.), tudo empacotado junto.
sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
Containerd é o runtime de containers — ou seja, é o motor que roda containers
sudo dnf install -y docker-ce docker-ce-cli containerd.io
Habilitei o service do docker na inicialização.
sudo systemctl enable docker --now
Verificando a instalação
docker --version
kubectl é a ferramenta de linha de comando usada para interagir com um cluster Kubernetes. Quando você executa um comando, ele envia uma requisição para a API Server, que coordena as ações necessárias no cluster.
Instalando a versão stable do kubectl, dando permissão para execução e movendo a saída para o diretório /usr/local/bin/
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/
Verifique a instalação
kubectl version --client
Kind é uma ferramenta que permite criar clusters Kubernetes locais usando containers Docker como nós do cluster.
Baixaxando o kind, dando permissão de execução e movendo para o diretório /usr/local/bin/kind
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
Verificando a instalação:
kind --version
Kind instalado com sucesso. Irei criar um manifest para deploy do Cluster.
kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
networking:
apiServerAddress: "192.168.1.81" # IP externo do host
apiServerPort: 17443
podSubnet: "10.244.0.0/16"
serviceSubnet: "10.96.0.0/12"
nodes:
- role: control-plane
extraPortMappings:
- containerPort: 80
hostPort: 80
- containerPort: 443
hostPort: 443
- containerPort: 6443
hostPort: 17445 # Porta do servidor API
- role: worker
kind create cluster --name giropops-cluster --config cluster-config.yaml
Vamos verificar se o Cluster subiu corretamente.
Cluster:
kubectl cluster-info --context kind-giropops-cluster
Nodes:
kubectl get nodes
Se tudo estiver OK, você verá:
O endereço do API server https://192.168.1.81:17443
Dois nodes: control-plane e worker com STATUS: Ready
Cluster funcionando perfeitamente.
Preparando a imagem Docker para Deploy
Docker é uma plataforma open-source que permite empacotar uma aplicação e todas as suas dependências em um container — um ambiente isolado que roda de forma consistente em qualquer. Basicamente ele tem a função de mentir para a aplicação para que ela pense que está rodando em uma máquina com hardware independente, quando na realidade todos os recursos estão isolados.
Irei criar um Dockerfile Single-Stage Runtime para a aplicação.
Vamos criar um Dockerfile dentro do diretório da aplicação.
Estrutura para o Dockerfile:
giropops-senhas/
├── app.py
├── requirements.txt
├── templates/
├── static/
└── Dockerfile
Dockerfile
FROM python:3.12-slim #estamos definindo a imagem oficinal Python
WORKDIR /app #Aqui definimos o diretório principal do container, todos os comandos agora irão rodar a parti do /app
COPY requirements.txt . #/app/requirements.txt
COPY app.py . #/app/app.py
COPY templates templates/ #/app/templates/
COPY static static/ #/app/static/
RUN pip install --no-cache-dir -r requirements.txt #aqui, vamos instalar as dependências dentro do .txt
EXPOSE 5000 #o container irá escutar na porta 5000
CMD ["flask", "run", "--host=0.0.0.0"] #definimos um comando para subir o flask e aceitar conexão de qualquer ip.
Buildando a aplicação.
docker build -t giropops-senhas .
Agora irei expor o container na porta 5000 e testar a aplicação.
docker run -p 5000:5000 giropops-senhas
Aplicação funcionando.
Agora que criamos e buildamos a aplicação giropops-senhas no Docker, vamos analisar a imagem.
Usando o comando docker images e docker history conseguimos visualizar a imagem criada e o seu processo de build. Note que a imagem possuí um tamanho de 140MB.
Saída do docker history:
IMAGE CREATED CREATED BY SIZE COMMENT
527daaa695dc 11 minutes ago CMD ["flask" "run" "--host=0.0.0.0"] 0B
<missing> 11 minutes ago EXPOSE map[5000/tcp:{}] 0B
<missing> 11 minutes ago RUN pip install --no-cache-dir ... 15.3MB
<missing> 11 minutes ago COPY static static/ 101kB
<missing> 11 minutes ago COPY templates templates/ 5.78kB
<missing> 11 minutes ago COPY app.py . 5.59kB
<missing> 1 hour ago COPY requirements.txt . 51B
<missing> 1 hour ago WORKDIR /app 0B
<missing> 7 days ago FROM python:3.12-slim 74.8MB (base image + dependências)
Aqui podemos visualizar todas as camadas do processo de build da nossa aplicação. Uma imagem Single-Stage, tamanho um pouco elevado e com uma superfície de ataque elevada. Uma imagem Single-Stage é o modelo mais simples de construção de imagens Docker, onde todo o processo de build acontece em um único Dockerfile.
O Melange é uma ferramenta para construir pacotes para sistemas baseados em Alpine Linux e APKO através de manifestos YAML. Ele permite que você crie pacotes .apk que podem ser incluídos em imagens APKO e usados em containers leves. APKO pega pacotes .apk (como os que o Melange criou) e monta uma imagem de container OCI — segura, minimalista e sem precisar de Dockerfile nem daemon Docker.
Instalando o Melange.
curl -L https://github.com/chainguard-dev/melange/releases/download/v0.23.6/melange_0.23.6_linux_amd64.tar.gz -o melange.tar.gz
tar -xzf melange.tar.gz
Arquivo extraído, irei entrar no diretório, dar permissão e mover para /ur/local/bin/melange
cd melange_0.23.6_linux_amd64/
#ls
LICENSE melange
mv melange /usr/local/bin/
chmod +x /usr/local/bin/melange
Testando o Melange:
melange version
APKO é uma ferramenta mantida epla Chainguard que tem a função de criar imagens de containers seguras, minimalistas e super otimizadas, apenas declarando o que você quer num arquivo de configuração YAML a parti de imagens Alpine Linux, sem precisar usar um Dockerfile.
Instalação:
curl -L https://github.com/chainguard-dev/apko/releases/download/v0.10.0/apko_0.10.0_linux_amd64.tar.gz -o apko.tar.gz
tar -xzf apko.tar.gz
cd apko_0.10.0_linux_amd64/
chmod +x apko
sudo mv apko /usr/local/bin/
Verificando a versão:
apko version
MELANGE e APKO Instalado com sucesso!
Criando pacote de imagem com Melange:
Gerando as chaves:
melange keygen
Agora possuo 2 chaves, uma privada "melange.rsa" e outra pública "melange.rsa.pub".
Renomeando a chave pública:
mkdir keys
cp melange.rsa.pub melange.key
Declarando as especificações da imagem via manifesto melange.yaml
vi melange.yaml
package:
name: giropops-senhas
version: 0.1
description: Aplicação Giropops-senhas - Gerador de senhas - LinuxTips
dependencies:
runtime:
- python3
environment:
contents:
keyring:
- ./melange.rsa.pub
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
- https://dl-cdn.alpinelinux.org/alpine/edge/community
packages:
- alpine-baselayout-data
- ca-certificates-bundle
- busybox
- gcc
- musl-dev
- python3
- python3-dev
- py3-pip
- py3-virtualenv
pipeline:
- name: Build Python application
runs: |
EXECDIR="${{targets.destdir}}/usr/bin"
WEBAPPDIR="${{targets.destdir}}/usr/share/webapps/giropops-senhas"
mkdir -p "${EXECDIR}" "${WEBAPPDIR}"
echo "#!/usr/share/webapps/giropops-senhas/venv/bin/python3" > "${EXECDIR}/giropops-senhas"
cat app.py >> "${EXECDIR}/giropops-senhas"
chmod +x "${EXECDIR}/giropops-senhas"
virtualenv "${WEBAPPDIR}/venv"
cp -r templates/ static/ ${WEBAPPDIR}/
sh -c "source '${WEBAPPDIR}/venv/bin/activate' && pip install -r requirements.txt"
Resumo do manifesto melange:
Define um pacote chamado giropops-senhas, versão 0.1.
Declara dependências do sistema necessárias para compilar e rodar uma app Python (gcc, musl, python3, etc).
Cria uma pipeline de build, que:
Prepara diretórios destino.
Copia e monta o script app.py como um executável (giropops-senhas).
Cria um ambiente virtual Python (venv) no diretório de webapp.
Copia pastas templates/ e static/.
Instala as dependências do requirements.txt.
Agora, irei criar o manifesto do apko.
vi apko.yaml
contents:
repositories:
- https://dl-cdn.alpinelinux.org/alpine/edge/main
- /work/packages
packages:
- alpine-baselayout
- giropops-senhas
- curl
accounts:
groups:
- groupname: nonroot
gid: 65532
users:
- username: nonroot
uid: 65532
gid: 65532
run-as: 65532
environment:
FLASK_APP: "/usr/bin/giropops-senhas"
entrypoint:
command: /usr/bin/giropops-senhas
archs:
- x86_64
Criando o diretório mkdir packages/
mkdir packages
Estrutura:
giropops-senhas/
├── app/ # Código-fonte da aplicação
│ ├── main.py
│ ├── requirements.txt
│ ├── templates/
│ └── static/
├── melange.yaml # Receita do pacote .apk
├── apko.yaml # Receita da imagem
├── melange.key # Chave pública
├── melange.rsa # Chave privada
| melange.rsa.pub # Chave pública
└── packages/ # Onde o .apk será salvo
└── packages/x86_64/
Buildando a imagem.
Empacotando imagem com Melange via Docker.
docker run --rm --privileged \
-v "${PWD}:/work" \
-w /work \
cgr.dev/chainguard/melange \
build melange.yaml \
--arch x86_64 \
--signing-key ./keys/melange.rsa \
--repository-append ./packages
Os campos INFO wrote packages/x86_64/giropops-senhas-0.1-r0.apk e INFO writing signed index to packages/x86_64/APKINDEX.tar.gz indicam que o empacotamento foi realizado com sucesso.
Agora possuo .apk da aplicação.
Ìndice assinado e o diretório packages/x86_64/ pronto para o apko.
#ls packages/x86_64/
APKINDEX.tar.gz giropops-senhas-0.1-r0.apk
Realizando Build com APKO:
docker run --rm \
-v "${PWD}:/work" \
-w /work \
cgr.dev/chainguard/apko \
build apko.yaml giropops-senhas giropops.tar \
-k melange.rsa.pub
apko.yaml → especifica o que vai na imagem (pacotes, entrypoint etc.)
giropops-senhas → nome lógico da imagem
giropops.tar → imagem gerada como tarball
-k melange.rsa.pub → chave pública usada para verificação de pacotes
Imagem APKO Construída com sucesso.
Verificando o arquivo gerado:
ls -lh giropops.tar
Irei carregar a imagem.tar para o repositório local:
docker load < giropops.tar
Realizando um teste da imagem empacotada via APKO:
docker run -p 5000:5000 giropops-senhas:latest-amd64
Irei realizar login no Docker Hub, definir uma tag para a imagem criada e fazer push.
docker login
docker tag
docker push
Imagem APKO upada no Docker Hub com apenas 25.75 MB.
O Trivy é uma ferramenta de código aberto que serve como scanner de vulnerabilidades, ideal para identificar falhas em imagens de contêineres.
Instalando:
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
aquasecurity/trivy info checking GitHub for latest tag
aquasecurity/trivy info found version: 0.61.0 for v0.61.0/Linux/64bit
aquasecurity/trivy info installed /usr/local/bin/trivy
Vamos verificar as vulnerabilidades da nossa imagem AKPO.
trivy image --severity HIGH,CRITICAL --ignore-unfixed geforce8400gsd/giropops-senhas
0 Vulnerabilidades.
Agora, vamos comparar as vulnerabilidades da nossa imagem APKO com o nosso Dockerfile.
105 Vulnerabilidades.
Agora possuo uma imagem Ultra-minimalista com apenas 20 MB e com uma superfície de ataques reduzida, possuindo apenas o necessário. Note que diferença de vulnerablidades, de 105 para 0.
| Critério | Dockerfile Clássico (python:3.12-slim) |
Melange + APKO (alpine) |
|---|---|---|
| Imagem base | python:3.12-slim (~74MB) | alpine (~5MB base) |
| Tamanho final da imagem | ~140MB | ~20-25MB |
| Segurança | Usuário root | Usuário não-root (UID 65532) |
| Imagem limpa | Contém pip, gcc, cache, etc. | Só o necessário, nada de build tools |
| Reprodutibilidade | Parcial | Total (com assinatura RSA) |
| Supply Chain Security | Não possui verificação de pacotes | Melange + assinatura de pacotes |
| Vulnerabilidades | 105 | 0 |
Kubernetes (também chamado de K8s) é uma plataforma open-source de orquestração de containers, ele automatiza o deploy, o scaling e a gestão de aplicações containerizadas. Em melhor palavras, atua como um sistema de orquestração, agrupando contêineres em pods para facilitar o escalamento e a descoberta de serviços.
Com o uso do K8S, possuímos os benefícios de Maior velocidade na gestão dos Containers permitindo a criação de plataformas eficientes e rápidas, com a flexibilidade de multiplos ambientes e economizando o máximo de recursos.
Buildando a imagem no Kubernetes com KinD.
KinD = “Kubernetes IN Docker” É uma forma super leve de rodar um cluster Kubernetes completo dentro de containers Docker.
Como o KiND já está instalado através do manifesto kind-config.yaml, irei prosseguir para o build da aplicação.
Verificando o Cluster.
O KinD é um Kubernetes no qual executa o cluster via containers com Docker, irei verificar os Containers responsáveis para o cluster k8s.
Verificando os containers Kind:
docker ps -a
Container giropops-cluster-worker e giropops-cluster-control-plane rodando perfeitamente.
Verificando o Cluster:
kind get clusters
Verificando informações do Cluster e os Nodes
kubectl cluster-info
kubectl get nodes
Por último, Pods do Kubernetes:
kubectl get pods -A
Agora podemos visualizar a menor unidade do nosso Cluster Kubernetes, os PODs, em resumo:
coredns = DNS interno do cluster
etcd = Banco de dados do cluster
kube-apiserver = API principal do Kubernetes
kube-controller-manager = Controlador de recursos
kube-scheduler = Agenda pods nos nodes
kube-proxy = Regras de rede por node
kindnet = CNI de rede do KinD
Conseguimos visualizar a saúde do nosso Cluster, todos os Pods necessários para o cluster funcionar estão rodando perfeitamente na namespace kube-system.
Buildando a imagem Kubernetes.
Irei realizar o build e teste da aplicação.
Manifestos:
giropops-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: giropops-senhas
name: giropops-senhas
spec:
replicas: 1
selector:
matchLabels:
app: giropops-senhas
template:
metadata:
labels:
app: giropops-senhas
spec:
containers:
- image: geforce8400gsd/giropops-senhas:latest
name: giropops-senhas
env:
- name: REDIS_HOST
value: redis-service
ports:
- containerPort: 5000
giropops-service.yaml
apiVersion: v1
kind: Service
metadata:
name: giropops-senhas-service
spec:
type: NodePort
selector:
app: giropops-senhas
ports:
- protocol: TCP
port: 5000
targetPort: 5000
nodePort: 32000
redis-deployment.yaml
apiVersion: v1
kind: Service
metadata:
name: redis-service
spec:
selector:
app: redis
ports:
- protocol: TCP
port: 6379
targetPort: 6379
type: ClusterIP
redis-service.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: redis
name: redis-deployment
spec:
replicas: 1
selector:
matchLabels:
app: redis
template:
metadata:
labels:
app: redis
spec:
containers:
- image: redis
name: redis
ports:
- containerPort: 6379
resources:
limits:
memory: "256Mi"
cpu: "500m"
requests:
memory: "128Mi"
cpu: "250m"
Build:
kubectl apply -f deployment.yaml
kubectl apply -f service.yaml
kubectl apply -f redis-deployment.yaml
kubectl apply -f redis.yaml
Manifestos yaml buildados, vamos visualizar os Pods e Services.
kubectl get pods --all-namespaces
kubectl get svc --all-namespaces
Agora podemos ver os Pods e Services da aplicação e do Redis, irei expor via port-foward e testar o acesso da aplicação.
Port-Foward:
kubectl port-forward deployment/giropops-senhas 5000:5000
Aplicação Buildada e acessada no Kubernetes com sucesso.
O HELM é uma ferramenta open-source que permite gerenciar aplicações Kubernetes de forma simples e eficiente. Com o Helm, você pode instalar, atualizar e desinstalar aplicações em um cluster Kubernetes com facilidade.
Instalando HELM:
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
Verifique a versão:
HELM version
Estruturando Cluster com HELM
Definindo a estrutura do Projeto.
giropops-senhas/
├── Chart.yaml
├── values-prod.yaml
├── values-dev.yaml
├── values-prod.yaml
├── templates/
│ ├── deployment.yaml
│ ├── deploymentlocust.yaml
│ ├── redis-deployment.yaml
│ ├── redis-service.yaml
│ ├── service.yaml
│ ├── service-locust.yaml
│ ├── hpa.yaml
│ ├── locust-configmap.yaml
│ └── _helpers.tpl
No projeto, um dos requisitos é ter 3 ambientes, um de produçao, outro de teste e outro de desenvolvimento. A separação dos ambientes será feita através dos manifestos values, exemplo: values-prod.yaml, values-dev.yaml e values-staging.yaml.
Criando as Namespaces:
kubectl create namespace dev
kubectl create namespace staging
kubectl create namespace prod
Manifestos HELM: / Chart.yaml
apiVersion: v2
name: giropops-senhas
description: Helm chart para automatizar o deployment da aplicação giropops-senhas
type: application
version: 0.1.0
appVersion: "1.0"
/ values-prod.yaml
deployments:
giropops-senhas:
name: "giropops-senhas"
image: "geforce8400gsd/giropops-senhas:1.0"
replicas: 1
ports:
- port: 5000
targetPort: 5000
name: "giropops-senhas-port"
serviceType: "NodePort"
NodePort: 32500
- port: 8088
targetPort: 8088
name: "giropops-senhas-metrics"
serviceType: "ClusterIP"
labels:
app: "giropops-senhas"
env: "labs"
live: "true"
resources:
requests:
memory: "32Mi"
cpu: "350m"
limits:
memory: "128Mi"
cpu: "500m"
redis:
name: "redis"
image: "redis"
replicas: 1
labels:
app: "redis"
env: "labs"
live: "true"
resources:
requests:
memory: "64Mi"
cpu: "250m"
limits:
memory: "128Mi"
cpu: "500m"
services:
giropops-senhas:
ports:
- port: 5000
targetPort: 5000
name: "giropops-senhas-port"
serviceType: "ClusterIP"
NodePort: 32500
- port: 8088
targetPort: 8088
name: "giropops-senhas-metrics"
serviceType: "ClusterIP"
labels:
app: "giropops-senhas"
env: "labs"
live: "true"
redis:
ports:
- port: 6379
targetPort: 6379
name: "redis-port"
serviceType: "ClusterIP"
labels:
app: "redis"
env: "labs"
live: "true"
hpa:
enabled: true # Define se o HPA será criado
minReplicas: 1
maxReplicas: 3
cpuUtilization: 50 # Escala se a CPU ultrapassar 50%
memoryUtilization: 70 # Escala se a Memória ultrapassar 70%
targetDeployment: giropops-senhas
locust:
enabled: true # Habilita ou desabilita o Locust
image: "linuxtips/locust-giropops:1.0"
replicas: 1
service:
type: NodePort # Pode ser LoadBalancer na nuvem
port: 8089
scriptConfigMap: "locust-scripts" # Nome do ConfigMap com o locustfile.py
ingress:
enabled: true
host: prod.giropops.local
serviceName: giropops-senhas-giropops-senhas-port
servicePort: 5000
allowIpAccess: false
tlsSecretName: giropops-tls # Secret TLS emitido pelo cert-manager
issuerName: giropops-ca-issuer
/ values-staging.yaml
deployments:
giropops-senhas:
name: "giropops-senhas"
image: "geforce8400gsd/giropops-senhas:latest"
replicas: 3
ports:
- port: 5000
targetPort: 5000
name: "giropops-senhas-port"
serviceType: "NodePort"
NodePort: 32500
- port: 8088
targetPort: 8088
name: "giropops-senhas-metrics"
serviceType: "ClusterIP"
labels:
app: "giropops-senhas"
env: "labs"
live: "true"
resources:
requests:
memory: "32Mi"
cpu: "250m"
limits:
memory: "64Mi"
cpu: "500m"
redis:
name: "redis"
image: "redis"
replicas: 1
labels:
app: "redis"
env: "labs"
live: "true"
resources:
requests:
memory: "32Mi"
cpu: "250m"
limits:
memory: "64Mi"
cpu: "500m"
services:
giropops-senhas:
ports:
- port: 5000
targetPort: 5000
name: "giropops-senhas-port"
serviceType: "ClusterIP"
NodePort: 32500
- port: 8088
targetPort: 8088
name: "giropops-senhas-metrics"
serviceType: "ClusterIP"
labels:
app: "giropops-senhas"
env: "labs"
live: "true"
redis:
ports:
- port: 6379
targetPort: 6379
name: "redis-port"
serviceType: "ClusterIP"
labels:
app: "redis"
env: "labs"
live: "true"
hpa:
enabled: true # Define se o HPA será criado
minReplicas: 1
maxReplicas: 1
cpuUtilization: 70 # Escala se a CPU ultrapassar 50%
memoryUtilization: 70 # Escala se a Memória ultrapassar 70%
targetDeployment: giropops-senhas
locust:
enabled: true # Habilita ou desabilita o Locust
image: "linuxtips/locust-giropops:1.0"
replicas: 1
service:
type: NodePort # Pode ser LoadBalancer na nuvem
port: 8089
scriptConfigMap: "locust-scripts" # Nome do ConfigMap com o locustfile.py
ingress:
enabled: true
host: staging.giropops.local
serviceName: giropops-senhas-giropops-senhas-port
servicePort: 5000
allowIpAccess: true
/ values-dev.yaml
deployments:
giropops-senhas:
name: "giropops-senhas"
image: "geforce8400gsd/giropops-senhas:latest"
replicas: 1
ports:
- port: 5000
targetPort: 5000
name: "giropops-senhas-port"
serviceType: "NodePort"
NodePort: 32500
- port: 8088
targetPort: 8088
name: "giropops-senhas-metrics"
serviceType: "ClusterIP"
labels:
app: "giropops-senhas"
env: "labs"
live: "true"
resources:
requests:
memory: "32Mi"
cpu: "250m"
limits:
memory: "64Mi"
cpu: "500m"
redis:
name: "redis"
image: "redis"
replicas: 1
labels:
app: "redis"
env: "labs"
live: "true"
resources:
requests:
memory: "32Mi"
cpu: "250m"
limits:
memory: "64Mi"
cpu: "500m"
services:
giropops-senhas:
ports:
- port: 5000
targetPort: 5000
name: "giropops-senhas-port"
serviceType: "ClusterIP"
NodePort: 32500
- port: 8088
targetPort: 8088
name: "giropops-senhas-metrics"
serviceType: "ClusterIP"
labels:
app: "giropops-senhas"
env: "labs"
live: "true"
redis:
ports:
- port: 6379
targetPort: 6379
name: "redis-port"
serviceType: "ClusterIP"
labels:
app: "redis"
env: "labs"
live: "true"
configMap:
name: "locust-config"
namespace: "dev"
data:
REDIS_HOST: "redis"
REDIS_PORT: "6379"
hpa:
enabled: false # Define se o HPA será criado
minReplicas: 1
maxReplicas: 1
cpuUtilization: 50 # Escala se a CPU ultrapassar 50%
memoryUtilization: 70 # Escala se a Memória ultrapassar 70%
locust:
enabled: false # Habilita ou desabilita o Locust
image: "geforce8400gsd/giropops-senhas:latest"
replicas: 1
service:
type: NodePort # Pode ser LoadBalancer na nuvem
port: 8089
scriptConfigMap: "locust-scripts" # Nome do ConfigMap com o locustfile.py
ingress:
enabled: true
host: dev.giropops.local
serviceName: giropops-senhas-giropops-senhas-port
servicePort: 5000
allowIpAccess: true
/templates/ _helpers.tpl
{{- define "giropops.fullname" -}}
{{ .Release.Name }}-{{ .Chart.Name }}
{{- end }}
{{- define "giropops.labels" -}}
app: {{ $.Chart.Name | default "giropops-app" }}
release: {{ $.Release.Name }}
env: {{ (index $.Values "global" "environment") | default "dev" }}
{{- end }}
{{- define "giropops.image" -}}
{{ .image | default "geforce8400gsd/giropops-senhas:latest" }}
{{- end }}
{{- define "giropops.env" -}}
{{- if eq .component "giropops-senhas" }}
- name: REDIS_HOST
value: "redis"
- name: REDIS_PORT
value: "6379"
{{- end }}
{{- end }}
{{- define "giropops.serviceName" -}}
{{ $.Release.Name }}-{{ $.Chart.Name }}-{{ .component }}
{{- end }}
{{- define "giropops.servicePorts" -}}
{{- range .ports }}
- port: {{ .port }}
targetPort: {{ .targetPort }}
protocol: TCP
name: {{ .name }}
{{- if eq .serviceType "NodePort" }}
nodePort: {{ .NodePort }}
{{- end }}
{{- end }}
{{- end }}
Explicando _helpers.tpl
O _helpers.tpl é como se fosse uma biblioteca de funções auxiliares do Helm. Com ele é possível Padronizar e reutilizar trechos de código.
"giropops.fullname" gera um nome único para cada recurso do Kubernetes.
{{- define "giropops.fullname" -}}
{{ .Release.Name }}-{{ .Chart.Name }} #.Release = Nome do release passado na linha de comando do HELM (giropops-dev)
{{- end }}
"giropops.labels" Padriniza as labels dos recursos do Kubernetes.
{{- define "giropops.labels" -}}
app: {{ $.Chart.Name | default "giropops-app" }} #nome do chart, se não tiver nada, usa "giropops-app"
release: {{ $.Release.Name }} #nome do release Helm
env: {{ (index $.Values "global" "environment") | default "dev" }} #ambiente, se não for definido, usa o "dev" como default.
{{- end }}
Definindo "giropops.image" tem a função de sempre garantir a definição de uma imagem, se não especificar dentro do values, irá ficar a default.
{{- define "giropops.image" -}}
{{ .image | default "geforce8400gsd/giropops-senhas:latest" }} #define a imagem defalut
{{- end }}
Se .component for igual a giropops-senhas, declara as váriaveis REDIS_HOST e REDIS_PORT.
{{- define "giropops.env" -}}
{{- if eq .component "giropops-senhas" }}
- name: REDIS_HOST
value: "redis"
- name: REDIS_PORT
value: "6379"
{{- end }}
{{- end }}
"giropops.serviceName" tem a função de definir dinamicamente os nomes dos serviços do helm de acordo com os componentes da aplicação. Ele concatena Release + Chart + Componente e define o nome. Exemplo: Release = giropops-dev + Chart = giropops-senhas + Component = giropops-senhas-port = giropops-dev-giropops-senhas-giropops-senhas-port
{{- define "giropops.serviceName" -}}
{{ $.Release.Name }}-{{ $.Chart.Name }}-{{ .component }}
{{- end }}
O "giropops.servicePorts" gera dinamicamente as portas dos serviços do Kubernetes. (Porta externa = port, Porta interna do Pod = TargetPort, nome da porta = name, tipo da porta = NodePort)
{{- define "giropops.servicePorts" -}} #inicia o helper
{{- range .ports }} #lista as portas definidas
- port: {{ .port }} #define a porta externa do service
targetPort: {{ .targetPort }} #define a porta do container
protocol: TCP #define o padrão TCP
name: {{ .name }} #nome da porta
{{- if eq .serviceType "NodePort" }} #Garante o uso da tipo de porta como NodePort.
nodePort: {{ .NodePort }} #define NodePort
{{- end }} #fecha o loop do range
{{- end }}
{{- end }}
/templates/ deploymeny.yaml
{{- range $component, $config := .Values.deployments }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ $component }}
labels:
app: {{ $config.labels.app }}
spec:
replicas: {{ $config.replicas }}
selector:
matchLabels:
app: {{ $config.labels.app }}
template:
metadata:
labels:
app: {{ $config.labels.app }}
spec:
containers:
- name: {{ $component }}
image: {{ $config.image }}
ports:
{{- range $config.ports }}
- containerPort: {{ .port }}
{{- end }}
env:
{{- if eq $component "giropops-senhas" }}
- name: REDIS_HOST
value: "redis"
- name: REDIS_PORT
value: "6379"
{{- end }}
resources:
requests:
memory: {{ $config.resources.requests.memory }}
cpu: {{ $config.resources.requests.cpu }}
limits:
memory: {{ $config.resources.limits.memory }}
cpu: {{ $config.resources.limits.cpu }}
{{- end }}
/templates/ redis-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Values.deployments.redis.name }}
labels:
app: {{ .Values.deployments.redis.labels.app }}
spec:
replicas: {{ .Values.deployments.redis.replicas }}
selector:
matchLabels:
app: {{ .Values.deployments.redis.labels.app }}
template:
metadata:
labels:
app: {{ .Values.deployments.redis.labels.app }}
spec:
containers:
- name: redis
image: {{ .Values.deployments.redis.image }}
ports:
- containerPort: 6379
resources:
requests:
memory: {{ .Values.deployments.redis.resources.requests.memory }}
cpu: {{ .Values.deployments.redis.resources.requests.cpu }}
limits:
memory: {{ .Values.deployments.redis.resources.limits.memory }}
cpu: {{ .Values.deployments.redis.resources.limits.cpu }}
command: ["redis-server", "--appendonly", "yes"] # Permite persistência dos dados
/templates/ redis-service.yaml
apiVersion: v1
kind: Service
metadata:
name: redis
labels:
app: {{ .Values.services.redis.labels.app }}
spec:
type: ClusterIP
selector:
app: {{ .Values.services.redis.labels.app }}
ports:
- protocol: TCP
port: 6379
targetPort: 6379
/templates/ service.yaml
{{- range $component, $config := .Values.services }}
{{- range $port := $config.ports }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ $component }}-{{ $port.name }}
labels:
app: {{ $config.labels.app }}
spec:
type: {{ $port.serviceType }}
selector:
app: {{ $config.labels.app }}
ports:
- port: {{ $port.port }}
targetPort: {{ $port.targetPort }}
protocol: TCP
name: {{ $port.name }}
{{- if eq $port.serviceType "NodePort" }}
nodePort: {{ $port.NodePort }}
{{- end }}
{{- end }}
{{- end }}
/templates/ configmap.yaml
# templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ .Values.configMap.name }}
namespace: {{ .Values.configMap.namespace }}
data:
REDIS_HOST: {{ .Values.configMap.data.REDIS_HOST | quote }}
REDIS_PORT: {{ .Values.configMap.data.REDIS_PORT | quote }}
Deploy Helm: values-dev.yaml, values-staging.yaml e values-prod.yaml
Dev:
helm install --install giropops-dev . \
--namespace dev \
--values values-dev.yaml
Staging:
helm install giropops-staging . \
--namespace staging \
--values values-staging.yaml
Prod:
helm install --install giropops-prod . \
--namespace prod \
--values values-prod.yaml
Agora o cluster possuí as Namespaces dos 3 ambientes, prod, dev e staging.
helm list
Verificando os pods:
kubectl get pods -n dev
kubectl get pods -n staging
kubectl get pods -n prod
Vamos verificar os pods criados:
kubectl get all --all-namespaces
Um detalhe, no values-dev.yaml defini hpa: enabled: false para que o HPA funcione apenas nos ambientes prod e staing.
Ingress é um recurso do Kubernetes que gerencia o acesso externo de um serviço dentro do Cluster.
Ele funciona como uma camada de Roteamento HTTP/HTTPS, permitindo a definição de regras para direcionar o tráfego externo para diferentes serviços back-end.
Como ele irá funcionar?
Navegador ↓
http://giropops.local:32080
↓
Ingress Controller (NGINX)
↓
Ingress Rule (roteamento)
↓
Service giropops-senhas
Instalação Nginx Ingress:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.10.1/deploy/static/provider/kind/deploy.yaml
Verificando os Pods Nginx.
O NGINX Controller, por padrão, não especifica um nodeSelector, mas às vezes os taints/tolerations ou a falta de afinidade impedem ele de ser escalonado nesse único node. Se o pod não iniciar, rode:
kubectl patch deployment ingress-nginx-controller -n ingress-nginx \
--type='json' -p='[{
"op": "add",
"path": "/spec/template/spec/nodeSelector",
"value": {
"kubernetes.io/hostname": "giropops-cluster-control-plane"
}
}]'
kubectl get pods -n ingress-nginx
Realizando o Deploy do Ingress NGINX com as portas 32080 (HTTP) e 32443 (HTTPS)
Agora irei realizar deploy do manifestos ingress.yaml.
ingress.yaml
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: giropops-ingress
namespace: {{ .Release.Namespace }}
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
{{- if .Values.ingress.allowIpAccess }}
nginx.ingress.kubernetes.io/whitelist-source-range: "192.168.1.0/24"
{{- end }}
spec:
ingressClassName: nginx
rules:
- host: {{ .Values.ingress.host }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: {{ .Values.ingress.serviceName }}
port:
number: {{ .Values.ingress.servicePort }}
{{- end }}
Para expor todos os Cluster com 1 manifesto Ingress foi definido as especificações via váriaveis.
No Manifesto ingress defini váriaveis irá buscar o valor em cada 1 dos values. Exemplo Ingress.yaml:
name: {{ include "giropops.fullname" . }}-ingress
service:
name: {{ .Values.ingress.serviceName }}
port:
number: {{ .Values.ingress.servicePort }}
values-dev.yaml:
ingress:
enabled: true
host: dev.giropops.local
serviceName: giropops-senhas-giropops-senhas-port
servicePort: 5000
allowIpAccess: true
Agora, irei fazer upgrade do HELM para atualizar os 3 Ambientes prod, staging e dev:
helm upgrade --install giropops-dev . \
--namespace dev \
--values values-dev.yaml
helm upgrade --install giropops-staging . \
--namespace staging \
--values values-staging.yaml
helm upgrade --install giropops-prod . \
--namespace prod \
--values values-prod.yaml
Verificando os ingress criados:
kubectl get ingress -n dev
kubectl get ingress -n prod
kubectl get ingress -n staging
Verificando os Pods Nginx Ingress.
kubectl get pods --all-namespaces -l app=ingress-nginx
kubectl get ingress -A
kubectl get pods --all-namespaces -l app.kubernetes.io/name=ingress-nginx
kubectl get pods -n ingress-nginx
Verificando o Service:
kubectl get svc -n ingress-nginx
Note que o meu ingress-nginx-controller está como Type NodePort. Isso indica que expõe o serviço para fora do cluster via porta do nó (host). A porta 80 (HTTP) está acessível externamente via 32080 e a porta 443 (HTTPS) via 32443.
O Trafego está da seguinte maneira:
[ Navegador]
↓
http://prod.giropopssenhas.local)
↓
[ ingress-nginx-controller Service ]
↓
Pod Ingress Controller
↓
Regra Ingress para apontar para o deployment
↓
Giropops-senhas
Como meu Cluster foi deployado em uma rede LAN, apontei 3 endereços DNS para o meu Localhost, assim conseguirei acessar os DNS de cada ambiente.
vi /etc/hosts
127.0.0.1 dev.giropops.local
127.0.0.1 staging.giropops.local
127.0.0.1 prod.giropops.local
Nos manifestos values, defini um parametro para declarar o endereço DNS de cada Ingress.
values-dev.yaml
ingress:
enabled: true
host: dev.giropops.local # DNS
Agora o ingress irá apontar para o DNS dev.giropops.local que foi definido no arquivos hosts do Server.
É um recurso nativo do kubernetes que ajusta automaticamente o número de réplicas(pods) de um Deployment, ReplicaSet ou StatefulSet com base na utilização de recursos ou métricas personalizadas.
Exemplo: Utiliza métricas definidas em "resource" e "requests" dos containers para escalar.
Para o HPA funcionar, é necessário o Metrics Server instalado no Cluster.
METRICS SERVER é um agregador de métricas de recursos de sistemas, que coleta métricas como uso de CPU e memória dos nós e pods no Cluster. Essas métricas são utilizadas no HPA para fazer o escalonamento dos Pods.
Instalando Metrics Server no Kind.
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml
kubectl patch deployment metrics-server -n kube-system --type=json -p='[
{
"op": "add",
"path": "/spec/template/spec/containers/0/args/-",
"value": "--kubelet-insecure-tls"
},
{
"op": "add",
"path": "/spec/template/spec/containers/0/args/-",
"value": "--kubelet-preferred-address-types=InternalIP"
}
]'
Verificando instalação:
kubectl get pods -n kube-system | grep metrics-server
Verificar se o Metrics Server está rodando corretamente:
kubectl get deployment metrics-server -n kube-system
kubectl logs -n kube-system deployment/metrics-server
Vamos verificar as métricas de CPU dos Nodes:
kubectl top nodes
kubectl top pods -n dev
Metrics Server Instalando e coletando dados.
Deploy HPA:
em /templetes criei um manifesto HPA.yaml que declara as especificações de AutoScaling.
Nos manifestos Values declarei um campo para definir as especificações do HPA. Esses valores são carregados automaticamente pelo Helm quando rodo o Update.
hpa:
enabled: true
minReplicas: 1 #aqui defino o mínimo de replicas.
maxReplicas: 3 # aqui defino o máximo de replicas.
cpuUtilization: 80 #aqui defini o requisito mínimo de CPU para ativar a regra.
memoryUtilization: 95 #aqui defini o requisito mínimo de memória RAM para ativar a regra.
targetDeployment: giropops-senhas # aqui estou apontando para meu deployment.
Primeiro busca o valor hpa.targetDeployment do values.yaml. Se não estiver definido, cai no helper giropops.fullname.
scaleTargetRef:
name: {{ .Values.hpa.targetDeployment | default (include "giropops.fullname" .) }}
Dentro do meu _helpers eu defini o nome da aplicação em váriaveis:
{{- define "giropops.fullname" -}}
{{- if .Values.fullnameOverride -}}
{{ .Values.fullnameOverride }}
{{- else -}}
{{ .Release.Name }}-{{ .Chart.Name }}
{{- end -}}
{{- end }}
Com esta maneira, posso chamar cada values de forma dinâmica através do:
{{ include "giropops.fullname" . }}
HPA.yaml
{{- if .Values.hpa.enabled }}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: {{ .Release.Name }}-hpa
namespace: {{ .Release.Namespace }}
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: {{ .Values.hpa.targetDeployment | default (include "giropops.fullname" .) }}
minReplicas: {{ .Values.hpa.minReplicas }}
maxReplicas: {{ .Values.hpa.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: {{ .Values.hpa.cpuUtilization }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: {{ .Values.hpa.memoryUtilization }}
{{- end }}
Vou atualizar o Helm nos ambientes Staging e Prod para aplicar o HPA.
helm upgrade --install giropops-staging . \
--namespace staging \
--values values-staging.yaml
helm upgrade --install giropops-prod . \
--namespace prod \
--values values-prod.yaml
Verificando Métricas de Prod e Staging:
kubectl get hpa -n staging
kubectl get hpa -n prod
Locust é uma ferramenta open source escrita em Python para fazer testes de performance e carga.Você escreve scripts de teste em Python que simulam o comportamento de usuários usando sua aplicação.
Como o Locust Funciona?
Você escreve um script Python descrevendo as ações que cada "usuário virtual" deve fazer (ex: logar, acessar página, fazer post, etc).
Você executa o Locust — que cria milhares de usuários virtuais simulando essas ações.
Ele gera relatórios interativos em tempo real via Web UI.
Você analisa latência, throughput, erros e muito mais.
Instalação:
Em templates/ foi criado um manifesto locust-configmap.yaml com a função de rodar um script(locustfile.py) para simular um teste de carga.
locust-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: locust-scripts
data:
locustfile.py: |
from locust import HttpUser, task, between
class GiropopsLoadTest(HttpUser):
wait_time = between(1, 3) # Tempo de espera entre requisições
@task
def test_homepage(self):
self.client.get("/") # Simula requisições para a aplicação
@task
def test_generate_password(self):
self.client.get("/generate") # Simula geração de senhas
locustfile.py
from locust import HttpUser, task, between
class Giropops(HttpUser):
wait_time = between(1, 2)
@task(1)
def gerar_senha(self):
self.client.post("/api/gerar-senha", json={"tamanho": 8, "incluir_numeros": True, "incluir_caracteres_especiais": True})
@task(2)
def listar_senha(self):
self.client.get("/api/senhas")
Para Deployment do locust, foi criado um manifesto locust-deployment.yaml e um Service locust-service.yaml.
locust-deployment.yaml
{{- if .Values.locust.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust-giropops
labels:
app: locust-giropops
spec:
replicas: {{ .Values.locust.replicas }}
selector:
matchLabels:
app: locust-giropops
template:
metadata:
labels:
app: locust-giropops
spec:
containers:
- name: locust-giropops
image: {{ .Values.locust.image }}
env:
- name: LOCUST_LOCUSTFILE
value: "/usr/src/app/scripts/locustfile.py"
ports:
- containerPort: 8089
imagePullPolicy: Always
volumeMounts:
- name: locust-scripts
mountPath: /usr/src/app/scripts
volumes:
- name: locust-scripts
configMap:
name: {{ .Values.locust.scriptConfigMap }}
optional: true
{{- end }}
locust-service.yaml
{{- if .Values.locust.enabled }}
apiVersion: v1
kind: Service
metadata:
name: locust-service
spec:
selector:
app: locust-giropops
ports:
- protocol: TCP
port: 8089
targetPort: 8089
type: {{ .Values.locust.service.type }}
{{- end }}
Para ativar o Locust nos ambientes, declarei um campo para o Locust marcando como enable: true no ambiente de Staging, os demais ambientes deixei o parâmetro como false.
values-staging.yaml
locust:
enabled: true # Habilita ou desabilita o Locust
image: "linuxtips/locust-giropops:1.0"
replicas: 1
service:
type: NodePort # Pode ser LoadBalancer na nuvem
port: 8089
scriptConfigMap: "locust-scripts" # Nome do ConfigMap com o locustfile.py
Agora irei verificar o HPA junto com o LOCUST realizando um teste de carga.
No manifesto values-staging.yaml, defini o valor máximo de 10 Pods.
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
Primeiro irei verificar o nome do Pod do Locust e expor via Port-Foward.
kubectl get pods -n staging -l app=locust-giropops
kubectl port-forward -n staging pod/locust-giropops-75b9ff794d-dnwbq 8089:8089
Locust acessado, irei simular um teste de carga.
Teste: a cada segundo, 20 novos usuários começam a usar a aplicação até chegar em 1000 usuários.
Aqui podemos ver o HPA entrando em ação após o estresse do Locust, Os Pods existentes bateram a tigger de limite de CPU e Memória e começaram a criar novos Pods.
No gráfico, podemos notar o aumento dos números de usuários e o tempo de requisição subindo.
Cosign é uma ferramenta de linha de comando desenvolvida como parte do projeto Sigstore. Ela é usada para assinar, verificar, armazenar e recuperar artefatos de software, particularmente imagens de container, através de interfaces com registradores OCI (Open Container Initiative)
Instalação Cosign:
COSIGN_VERSION=$(curl -s https://api.github.com/repos/sigstore/cosign/releases/latest | grep tag_name | cut -d '"' -f 4)
curl -Lo cosign https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-amd64
chmod +x cosign
sudo mv cosign /usr/local/bin/
cosign version
Cosign instalado, agora irei começar o processo de assinatura de imagem.
Vou gerar um par de chaves:
cosign generate-key-pair
Agora possuo 2 chaves, uma privada e outra pública.
Assinando imagem:
cosign sign --key cosign.key docker.io/geforce8400gsd/giropops-senhas:latest
Verificando assinatura:
cosign verify --key cosign.pub docker.io/geforce8400gsd/giropops-senhas:latest
O kubectl create secret armazena a chave pública Cosign em um Secret no Kubernetes, para que o Kyverno consiga usá-la ao validar imagens assinadas.
Na ClusterPolicy verificando-assinaturas-images-cosign, foi passado o parametro key: "k8s://cert-manager/cosign-pub". Isso significa que o Kyverno vai tentar carregar o Secret chamado cosign-pub no namespace cert-manager.
kubectl create secret generic cosign-pub \
--from-file=cosign.pub=/home/pick/PICK/.github/cosign/cosign.pub \
-n cert-manager
kubectl create secret generic cosign-pub \
--from-file=cosign.pub=/home/pick/PICK/.github/cosign/cosign.pub \
-n cert-manager
Kube Prometheus é uma coleção de componentes para instalar e configurar um stack completo de monitoramento no Kubernetes, feito pela comunidade Prometheus + CoreOS.
Componente Função
Prometheus Coleta e armazena métricas (CPU, memória, requests, etc.)
Grafana Dashboards
Alertmanager Envia alertas
Node Exporter Exporta métricas do nó
kube-state-metrics Métricas do estado dos recursos K8s
Prometheus Operator Facilita deploys de Prometheus via CRDs
Iniciando a instalação Kube Prometheus, irei adicionar as CDR (Custom Resource Definition) no HELM:
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
Criando Namespace:
kubectl create namespace monitoring
Agora irei iniciar a instalação do kube prometheus stack na namespace monitoring. Por conta de limitações de hardware, irei definir os limites de memória em 120Mi e CPU 100m.
helm install kube-prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--set grafana.enabled=true \
--set prometheus.resources.limits.cpu=100m \
--set prometheus.resources.limits.memory=128Mi \
--set grafana.resources.limits.cpu=100m \
--set grafana.resources.limits.memory=128Mi
Verificando a instalação:
kubectl get pods -n monitoring
kubectl get svc -n monitoring
Pods e Services rodando com sucesso, agora irei criar o Service Monitor.
Service monitor é uma Custom Resource Definition (CRD) usado pelo Prometheus Operator no kubernetes...
Ele já vem instalado no kube-prometheus. O Kube-prometheus já vem com vários ServiceMonitors configurados. Para visualizar os servicemonitors:
kubectl get servicemonitors -n monitoring
Criei um manifesto para o service monitor para monitorar o ambiente de Produção.
servicemonitor-prod.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: giropops-senhas-servicemonitor
namespace: monitoring # mesmo namespace do Prometheus
labels:
release: kube-prometheus # importante se Prometheus usa labelSelector
spec:
selector:
matchLabels:
app: giropops-senhas # deve bater com o .Values.services.giropops-senhas.labels.app
namespaceSelector:
matchNames:
- dev # ou "prod", dependendo de onde está seu app
endpoints:
- port: giropops-senhas-metrics # mesmo nome usado no service
path: /metrics # endpoint exposto no app
interval: 15s
Aplicando o manifest:
kubectl apply -f servicemonitor-prod.yaml
Para expor o Grafana e Prometheus, criei 2 manifestos ingress que apontam para os services kube-prometheus-grafana e kube-prometheus-kube-prome-prometheus.
ingress-grafana.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: grafana-ingress
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: grafana.giropops.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kube-prometheus-grafana
port:
number: 80
ingress-prometheus.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: prometheus-ingress
namespace: monitoring
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
ingressClassName: nginx
rules:
- host: prometheus.giropops.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kube-prometheus-prometheus
port:
number: 9090
Para o DNS, adicionei prometheus.giropops.local e grafana.giropops.local no arquivo /etc/hosts.
Aplicando Ingress:
kubectl apply -f ingress-prometheus.yaml
kubectl apply -f grafana.yaml
Prometheus e Grafana acessados com sucesso.
Aqui consigo visualizar as métricas do meu ambiente, 35 Pods, 87 Containers.

O Cert-Manager é um controlador de certificados para Kubernetes que automatiza a emissão, renovação e gerenciamento de certificados TLS — de forma segura e integrada ao cluster.
Instalando:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/latest/download/cert-manager.yaml
Verificando os PODs:
kubectl get all --namespace cert-manager
Em ambientes locais como o Kind, irei usar o SelfSigned para emitir os certificado pois meu cluster local não DNS público nem acesso externo pela WAN. Certificados Self-Singed são certificados auto assinados pelo Cluster, basicamente o Cluster vira uma CA (Autoridade Certificadora).
Nesta etapa, estou definindo um manifesto para deploy do Issue selfSigned + Certificado CA.
Emitindo a CA e criando um certificado autofirmado (self-signed):
selfsigned-giropops-prod.yaml
apiVersion: cert-manager.io/v1
kind: Issuer #um emissor de certificados
metadata:
name: selfsigned-issuer
namespace: prod
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: giropops-ca
namespace: prod
spec:
isCA: true
secretName: giropops-ca-secret
commonName: giropops.local
issuerRef:
name: selfsigned-issuer
kind: Issuer
Agora irei criar o manifesto Issue do tipo CA. Esse Issuer permite que o cert-manager emita certificados TLS usando uma CA existente, que está armazenada no cluster dentro de um Secret TLS.
inssuer-ca.yaml
apiVersion: cert-manager.io/v1 #Versão da API do cert-manager
kind: Issuer #um emissor de certificados
metadata:
name: giropops-ca-issuer #Nome do emissor
namespace: prod #namespace
spec:
ca:
secretName: giropops-ca-secret #Nome do Secret TLS que possuí a CA e a key.
Por último, irei definir o manifesto para o certificate apontando para prod.giropops.local.
certificado-giropops.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: giropops-cert
namespace: prod
spec:
secretName: giropops-tls
dnsNames:
- giropops.local
issuerRef:
name: giropops-ca-issuer
kind: Issuer
Aplicando:
kubectl apply -f selfsigned-giropops-prod.yaml
kubectl apply -f inssuer-ca.yaml
kubectl apply -f certificado-giropops.yaml
Fluxo:
[ giropops-ca-issuer (Issuer) ]
↓
[ giropops-cert (Certificate) ]
↓
[ giropops-tls (Secret TLS com cert + key) ]
↓
[ Ingress TLS → HTTPS habilitado em giropops.local ]
Vou verificar as CA no namespace prod:
kubectl get issuer -n prod
kubectl get certificate -n prod
kubectl get secret giropops-tls -n prod
Agora já consigo visualizar o Secret giropops-tls do meu cluster.
Extraindo ca.crt e .crt:
giropops.ca.crt
kubectl get secret giropops-ca-secret -n prod -o jsonpath="{.data['ca\.crt']}" | base64 -d > giropops-ca.crt
giropops.crt
kubectl get secret giropops-tls -n prod -o jsonpath="{.data['tls\.crt']}" | base64 -d > giropops.crt
Podemos verificar os certicados giropops-ca.crt e giropops.crt
#ls
certificado-giropops.yaml giropops-ca.crt giropops.crt inssuer-ca.yaml selfsigned-giropops-prod.yaml`
giropops.crt = Chave privada
giropops.ca.crt = Chave pública
Verificando o CN:
openssl x509 -in giropops-ca.crt -noout -text -issuer -subject -dates
No manifesto ingress.yaml, foi passado váriaveis para percorrer o values-prod.yaml
tls:
- hosts:
- {{ .Values.ingress.host }}
secretName: {{ .Values.ingress.tlsSecretName }}
No values-prod.yaml, defini os valores do SecretName, para se conectar ao CA criado.
tlsSecretName: giropops-tls
issuerName: giropops-ca-issuer
Resumindo, o Cert-Maneger atraves dos manifests emitiu o Secret com cert e chave. O Ingress aponta para este Secret e o Nginx Controlller serve o certificado para o Host.
Realizando teste do certificado:
curl -v https://prod.giropops.local --cacert giropops-ca.crt
Certificado Válidado com sucesso.
Detalhe: Como meu Cluster é local,as outras máquinas na rede não conhecem e nem confiam na minha CA. Neste caso, irei instalar manualmente o certificado giropops-ca.crt no host.
Uma maneira simples de coletar o certificado dentro do cluster é usando o SCP no PowerShell do Windows.
scp root@192.168.1.81:/home/pick/PICK/.github/workflows/cert-manager/giropops-ca/ca.crt $HOME\Downloads\giropops-ca.crt
Certificado instalado e válido.

Kyverno é uma ferramenta de gerenciamento de políticas para Kubernetes, ele trabalha como um Policy Engine servindo para aplicar regras de segurança, validar configurações e automatizar correções dentro de clusters.
Funcionalidades: Validação e Mutação de Recursos. Gerenciamento de Políticas. Relatórios e Exceções. Verificação de Assinaturas de Imagens.
Instalação:
Adicionando Repo ao HELM:
helm repo add kyverno https://kyverno.github.io/kyverno/
helm repo update
Criando Namespace kyverno e instalando os pacotes do kyverno:
kubectl create namespace kyverno
helm install kyverno kyverno/kyverno --namespace kyverno
Verificando CRD.
kubectl get crd | grep kyverno
Verificando Pods da namespace kyverno:
kubectl get pods -n kyverno
Kyverno Instalado com sucesso!
Criando as Políticas de Segurança:
Esta Policy só permite criar Pods com imagens assinadas pela chave pública Cosign
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verificando-assinaturas-images
spec:
validationFailureAction: Enforce
background: true
rules:
- name: verify-signed-images
match:
any:
- resources:
kinds:
- Pod
namespaces:
- prod
verifyImages:
- imageReferences:
- "docker.io/geforce8400gsd/*"
key: "k8s://cert-manager/cosign-pub"
attestations: [] #Neste campo, defini que a chave está no cluster, o namespace onde procurar e o nome da chave.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: desabilitando-root
spec:
validationFailureAction: Enforce
background: true
rules:
- name: no-run-as-root
match:
any:
- resources:
kinds:
- Pod
namespaces:
- prod
validate:
message: "Rodar como root é proibido na namespace prod."
pattern:
spec:
securityContext:
runAsNonRoot: true
containers:
- securityContext:
runAsNonRoot: true
Bloqueia a criação de Pods na namespace prod se eles tiverem variáveis de ambiente sensíveis declaradas nos containers, como:
PASSWORD
SECRET
TOKEN
AWS_ACCESS_KEY
AWS_SECRET_KEY
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: block-sensitive-env-vars-prod
spec:
validationFailureAction: enforce # Quando violado, o bloqueio é realizado
background: true #aplica a pods existentes
rules:
- name: bloqueando-env
match:
any: #aqui, estou setando para a namespace Prod
- resources:
kinds:
- Pod
namespaces:
- prod
validate:
message: "Variáveis de ambiente sensíveis não são permitidas em 'prod'."
deny:
conditions:
any:
- key: "{{ request.object.spec.containers[].env[].name }}"
operator: AnyIn
value:#bloqueio
- PASSWORD
- SECRET
- TOKEN
- AWS_ACCESS_KEY
- AWS_SECRET_KEY
Essa policy força que Deployments da namespace prod:
Não usem root
Não peçam privilégios extras
Não modifiquem seu filesystem
Não carreguem capabilities perigosas
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: compliance
spec:
validationFailureAction: enforce # faz um bloqueio se não for conforme a regra
background: true #Também verifica recursos já existentes, não só novos
rules:
- name: compliance-padrões-mínimos
match:
any:
- resources:
kinds:
- Deployment
namespaces:
- prod #atacando todos os Pods da namespace prod
validate:
message: "Deployments devem seguir padrões de segurança mínimos."
pattern:
spec:
template:
spec:
securityContext:
runAsNonRoot: true #não-root
containers:
- securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false #sem escalação de privilégios
capabilities:
drop:
- ALL #aqui, defini para bloquear todas as capacidades de ADMIN
readOnlyRootFilesystem: true # O Filesystem foi definido como somente leitura
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: resources-limits
spec:
validationFailureAction: enforce #Neste argumento, um bloqueio é realizado em caso de violação.
rules:
- name: validando-limites-prod
match:
any:
- resources:
kinds:
- Pod #atacando todos os Pods
namespaces:
- prod #namespace prod
validate:
message: "Precisa definir o limite de recursos de CPU e Memória RAM para todos os Pods de Produlçao"
pattern:
spec:
containers:
- name: "*" #Wildcard, assim, a policy pega em todos os Containers.
resources:
limits:
cpu: "?*" # Obrigatório uso de argumento para CPU
memory: "?*" #Obrigatório uso de argumento para Memóri
role.yaml
Na Policy verificando-assinaturas-images ela precisa encontrar a chave pública do COSIGN que está na namespace cert-manager para poder verificar as assinaturas de imagem através do parametro key: "k8s://cert-manager/cosign-pub".
Na Policy role.yaml dou permissão ao Kyverno de acesso aos Secrets no namespace cert-manager.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: cert-manager
name: kyverno-cosign-reader
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kyverno-cosign-reader-binding
namespace: cert-manager
subjects: #Dá essas permissões para o Kyverno
- kind: ServiceAccount
name: kyverno-admission-controller
namespace: kyverno
roleRef:
kind: Role
name: kyverno-cosign-reader
apiGroup: rbac.authorization.k8s.io
kubectl apply -f role.yaml
Agora o Kyverno possuí acesso ao cert-manager. Irei testar o acesso com kubectl get rolebinding.
kubectl get rolebinding kyverno-cosign-reader-binding -n cert-manager
Em resumo: Concendi ao ServiceAccount kyverno-admission-controller no namespace kyverno a permissão de usar a Role kyverno-cosign-reader, que permite ler Secrets no namespace cert-manager.
Aplicando as Policys:
kubectl apply -f verificando-assinaturas-images-cosign.yaml
kubectl apply -f desabilitando-root.yaml
kubectl apply -f bloqueando-env.yaml
kubectl apply -f compliance.yaml
kubectl apply -f resources-limits.yaml
kubectl apply -f role.yaml
No diretório /kyverno/testes-Policys, criei alguns manifestos para testar as policys aplicadas.
Note que o deploy foi bloqueado por conta das regras compliance, desabilitando-root e resources-limits.

Outros exemplos do Kyverno entrando em ação:
Aqui, tento atualizar o ambiente de produção com a imagem docker.io/geforce8400gsd/giropops-senhas:latest e o deploy é bloqueado pela Policy verificando-assinaturas-images. Essa Policy não permite imagens Latest.
Agora usei a tag :1.0 ao invés de :latest na imagem de values-prod.yaml. image: "geforce8400gsd/giropops-senhas:1.0"
Deploy:
Ao respeitar todas as Policys, o deploy foi realizado com sucesso!
Minha Pipeline de CI/CD está sendo gerenciada pelo Github Actions, como meu server está sendo executando em uma rede local privada, optei por usar o Runner Local para se conectar ao Repositório do GitHub.
Como o CI/CD está sendo feito?
Verifica assinatura da imagem com Cosign
Assina automaticamente se necessário
Realiza deploy via helm upgrade --install para:
dev (branch: develop)
staging (branch: staging)
prod (branch: main, somente com acionamento manual)
Secrets
Dentro do Repo, foi passado 2 valores de Secrets, COSIGN_KEY e COSIGN_PASSWORD. Esees secrets contém a Chave para assinatura das imagens do Cosign e a senha.
O usuário do Runner precisa ter efetuado login no Docker Hub, assim o acesso estará salvo em /home/github-runner/.docker/config.json.
O Git já está instalado no Server e a conexão via SSH foi efetuada com sucesso.
-Login GitHub
git config --global user.name
git config --global user.email
-Gerando as chaves para conectar via SSH
ssh-keygen -t idcode -C e-mail
Apos conexão, verifique a conexão vis SSH.
ssh -T git@github.com
As chaves foram armazenadas em ~/.ssh/config
nano ~/.ssh/config
Host github.com
HostName host
IdentityFile /home/pick/giropops-senhas/cert
User usuário
Verificando a chave: ssh-add -l
Parametros passados em sshd_config:
nano /etc/ssh/sshd_config
PermitRootLogin yes
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
ChallengeResponseAuthentication no
UsePAM yes
Reiniciando sshd: systemctl restart sshd
GitHub Actions Runner Local:
Para executar o Runner localmente, é necessário criar um usuário não-root para executar o Runner.
useradd -m -s /bin/bash github-runner
Configurando acesso ao repositório:
./config.sh --url https://github.com/Marcusronney/PICK-2024 --token AO3ISEKIGKAETKLIZBVFAODH3NOCA
Criei um Service para executar o run.sh, assim não será necessário rodar o script manualmente.
nano /etc/systemd/system/github-runner.service
[Unit]
Description=GitHub Actions Runner
After=network.target
[Service]
ExecStart=/opt/actions-runner/run.sh
User=github-runner
WorkingDirectory=/opt/actions-runner
Restart=always
[Install]
WantedBy=default.target
Coloquei o Service para ser executado automaticamente, reiniciei os Daemons e dei permissão ao usuário do Runner para executar o Docker.
systemctl daemon-reload
systemctl enable github-runner
usermod -aG docker github-runner
Instalando Runner Local:
mkdir /opt/actions-runner && cd /opt/actions-runner
curl -O -L https://github.com/actions/runner/releases/download/v2.x/actions-runner-linux-x64-2.x.tar.gz
tar xzf actions-runner-linux-x64-2.x.tar.gz
./config.sh --url https://github.com/<user>/<repo> --token <TOKEN>
./svc.sh install && ./svc.sh start
Neste campo, estou provisioanndo o usuário do Runner para ter acesso as chaves do Cosign, assim podendo fazer as assinaturas via CI/CD.
mkdir -p /home/github-runner/.cosign
cp /home/pick/PICK/cosign/cosign.key /home/github-runner/.cosign/
chown github-runner:github-runner /home/github-runner/.cosign/cosign.key
chmod 600 /home/github-runner/.cosign/cosign.key
Automatizando o processo de validação de imagem Docker e deploy em cluster Kubernetes via Helm, de forma separada por ambiente (dev, staging, prod), garantindo que somente imagens assinadas possam ser aplicadas.
name: CI/CD - Helm CI/CD (Dev, Staging, Prod)
on:
push:
branches:
- main
- develop
- staging
paths:
- '**/*.yaml'
workflow_dispatch:
jobs:
deploy:
name: Helm Upgrade - Kubernetes
runs-on: self-hosted
steps:
- name: Checkout do Código
uses: actions/checkout@v4
- name: Configurar kubectl
run: |
export KUBECONFIG=/home/github-runner/.kube/config
kubectl cluster-info
kubectl get nodes
- name: Verificar Estrutura do Projeto
run: |
pwd
ls -laR
- name: Verificar assinatura da imagem
run: |
echo "${{ secrets.COSIGN_PUB }}" > cosign.pub
cosign verify --key cosign.pub docker.io/geforce8400gsd/giropops-senhas:1.0|| echo " Imagem ainda não assinada."
- name: Assinar imagem com Cosign (se necessário)
run: |
echo "${{ secrets.COSIGN_KEY }}" > cosign.key
echo "${{ secrets.COSIGN_PASSWORD }}" > cosign.pass
export COSIGN_PASSWORD=$(cat cosign.pass)
cosign sign --key cosign.key --yes docker.io/geforce8400gsd/giropops-senhas:1.0
env:
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
- name: Deploy para DEV
if: github.ref == 'refs/heads/develop'
run: |
helm upgrade --install giropops-dev ${{ github.workspace }} \
--namespace dev \
-f ${{ github.workspace }}/values-dev.yaml \
--set deployments.giropops-senhas.image=geforce8400gsd/giropops-senhas:latest
- name: Deploy para STAGING
if: github.ref == 'refs/heads/staging'
run: |
helm upgrade --install giropops-staging ${{ github.workspace }} \
--namespace staging \
-f ${{ github.workspace }}/values-staging.yaml \
--set deployments.giropops-senhas.image=geforce8400gsd/giropops-senhas:1.0
- name: Deploy para PRODUÇÃO (manual)
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
run: |
helm upgrade --install giropops-prod ${{ github.workspace }} \
--namespace prod \
-f ${{ github.workspace }}/values-prod.yaml \
--set deployments.giropops-senhas.image=geforce8400gsd/giropops-senhas:1.0
Aqui, defino a execução do runner direto no server.
jobs:
deploy:
runs-on: self-hosted
Efetuando a clonagem do repo.
- name: Checkout do Código
uses: actions/checkout@v4
Aqui, estou verificando se o Runner possuí acesso ao cluster com kubectl cluster-info e kubectl get nodes.
- name: Configurar kubectl
run: |
export KUBECONFIG=/home/github-runner/.kube/config
kubectl cluster-info
kubectl get nodes
Cosign - Verificando se a imagem está assinada, se não validar a assinatura, imprime o erro Imagem ainda não assinada.
- name: Verificar assinatura da imagem
run: |
echo "${{ secrets.COSIGN_PUB }}" > cosign.pub
cosign verify --key cosign.pub docker.io/geforce8400gsd/giropops-senhas:1.0 || echo " Imagem ainda não assinada."
Assinando imagens, como passei os Secrets das Key para o Secret do repositório, a chave será usada para assinar a imagem.
- name: Assinar imagem com Cosign
cosign sign --key cosign.key docker.io/geforce8400gsd/giropops-senhas:1.0
Deploy do Pipeline:
Ambiente Dev: Executa somente se o push for no ambiente de Desenvolvimento.
if: github.ref == 'refs/heads/develop'
helm upgrade --install giropops-dev ... --namespace dev -f values-dev.yaml
Ambiente Staging: Executa somente se o push for no ambiente de Teste.
if: github.ref == 'refs/heads/staging'
helm upgrade --install giropops-staging ... --namespace staging -f values-staging.yaml
Ambiente de Produção: Executado manualmente para evitar pushs errados.
if: github.ref == 'refs/heads/main' && github.event_name == 'workflow_dispatch'
helm upgrade --install giropops-prod ... --namespace prod -f values-prod.yaml















































































