Skip to content

Projeto final do Programa Intensivo em Containers e Kubernetes | PICK LINUXtips

Notifications You must be signed in to change notification settings

Marcusronney/LINUXtips-PICK

Repository files navigation

Projeto final do Programa Intensivo em Containers e Kubernetes | PICK LINUXtips

pick

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.

Tecnologias envolvidas no projeto:

Python Redis Flask GitHub Actions YAML Docker Kubernetes Grafana Prometheus Linux Locust Melange Apko Helm Kyverno Cosign Kind Trivy NGINX Ingress DNS HPA Kube-Prometheus Chainguard Docker Hub Hyper-V


INFRAESTRUTURA ON PREMISE

No meu projeto, o Cluster irá rodar localmente em uma VM no Hyper-V.

Especificações:

VM CPU RAM OS CLUSTER KUBERNETES
Hyper-V Xeon 2360 4 Core 17GB Rocky Linux KinD

PROVISIONANDO SERVER PARA O CLUSTER.

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

VM atualizada e Git instalado, agora vamos para o Docker.

Linux

Docker

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

Conterinerd

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

Title

Kubectl

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

Title

KinD - Kubernetes in Docker

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

Title

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

Deploy:

kind create cluster --name giropops-cluster --config cluster-config.yaml

Title

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

Title

Cluster funcionando perfeitamente.

Preparando a imagem Docker para Deploy

DOCKER Docker

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

Title

Aplicação funcionando.

Title

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.

Title

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)

Title

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.

Agora, irei realizar o build da aplicação utilizando Melange + APKO.

MELANGE

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

Title

Testando o Melange:

melange version

Title

APKO

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/

Title

Verificando a versão:

apko version

Title

MELANGE e APKO Instalado com sucesso!

Criando pacote de imagem com Melange:

Gerando as chaves:

melange keygen

Title

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.

Title

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.

Title

Verificando o arquivo gerado:

ls -lh giropops.tar

Title

SUBINDO IMAGEM APKO para o DOCKER HUB.

Irei carregar a imagem.tar para o repositório local:

docker load < giropops.tar

Title

Realizando um teste da imagem empacotada via APKO:

docker run -p 5000:5000 giropops-senhas:latest-amd64

Build Melange e empacotamento APKO realizado com sucesso!

Title

Docker HUB

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.

Title

TRIVY - Análise de Vulnerabilidades

O Trivy é uma ferramenta de código aberto que serve como scanner de vulnerabilidades, ideal para identificar falhas em imagens de contêineres.

Linux

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

Title

Vamos verificar as vulnerabilidades da nossa imagem AKPO.

trivy image --severity HIGH,CRITICAL --ignore-unfixed geforce8400gsd/giropops-senhas

Title

0 Vulnerabilidades.

Agora, vamos comparar as vulnerabilidades da nossa imagem APKO com o nosso Dockerfile.

Title

105 Vulnerabilidades.

Comparativo de Imagens - AKPO vs Docker

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

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.

Kubernetes

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

Title

Container giropops-cluster-worker e giropops-cluster-control-plane rodando perfeitamente.

Verificando o Cluster:

kind get clusters

Title

Verificando informações do Cluster e os Nodes

kubectl cluster-info
kubectl get nodes

Title

Por último, Pods do Kubernetes:

kubectl get pods -A

Title

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.

Title

Port-Foward:

kubectl port-forward deployment/giropops-senhas 5000:5000

Title

Aplicação Buildada e acessada no Kubernetes com sucesso.

Title

HELM - Gestão de Pacotes do Kubernetes

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

Title


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

Title

helm

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

Title

Agora o cluster possuí as Namespaces dos 3 ambientes, prod, dev e staging.

helm list

Title

Verificando os pods:

kubectl get pods -n dev
kubectl get pods -n staging
kubectl get pods -n prod

Title

Vamos verificar os pods criados:

kubectl get all --all-namespaces

Title

Um detalhe, no values-dev.yaml defini hpa: enabled: false para que o HPA funcione apenas nos ambientes prod e staing.

Title

NGINX INGRESS - Expondo o Cluster

Nginx

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

Title

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

Title

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

Title

Verificando o Service:

kubectl get svc -n ingress-nginx

Title

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

DNS - Domain Name System

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

Title

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.


HPA - Horizontal Pod Autoscaler.

É 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

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"
  }
]'

Title

Verificando instalação:

kubectl get pods -n kube-system | grep metrics-server

Title

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

Title

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

Title


LOCUST - TESTE DE CARGA

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

TESTE DE CARGA - LOCUST - HPA

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

Title

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.

Title

Title

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.

Title

No gráfico, podemos notar o aumento dos números de usuários e o tempo de requisição subindo.

Title


COSIGN - IMAGENS ASSINADAS E SEGURAS

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

Title

Cosign instalado, agora irei começar o processo de assinatura de imagem.

Vou gerar um par de chaves:

cosign generate-key-pair

Title

Agora possuo 2 chaves, uma privada e outra pública.

Title

Assinando imagem:

cosign sign --key cosign.key docker.io/geforce8400gsd/giropops-senhas:latest

Imagem assinada. Title

Verificando assinatura:

cosign verify --key cosign.pub docker.io/geforce8400gsd/giropops-senhas:latest

Secret - Cosign - Chave para o Kyverno verificar imagens assinadas.

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

Title

KUBE PROMETHEUS

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

Title

Title

Pods e Services rodando com sucesso, agora irei criar o Service Monitor.

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

Nginx Ingress e DNS - EXPONDO GRAFANA E PROMETHEUS

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.

Title

Aplicando Ingress:

kubectl apply -f ingress-prometheus.yaml
kubectl apply -f grafana.yaml

Prometheus e Grafana acessados com sucesso.

Title

Dashboard - Cluster Kubernetes

Aqui consigo visualizar as métricas do meu ambiente, 35 Pods, 87 Containers. Title

Title

Title

Title

Title

Cert-Manager

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

Title

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

Title

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

Title

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

Title

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

Title

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

Title

Certificado instalado e válido. Title

KYVERNO - Políticas de Segurança

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

Title

Verificando CRD.

kubectl get crd | grep kyverno

Title

Verificando Pods da namespace kyverno:

kubectl get pods -n kyverno

Title

Kyverno Instalado com sucesso!

Criando as Políticas de Segurança:

verificando-assinaturas-images-cosign.yaml

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.

desabilitando-root.yaml

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

bloqueando-env.yaml

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

compliance.yaml

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

resources-limits.yaml

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

RoleBinding - Kyverno

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 

Title

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.

Title

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

Title

Realizando testes de Policys.

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. Title

Title

Outros exemplos do Kyverno entrando em ação:

Title

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.

Title

Agora usei a tag :1.0 ao invés de :latest na imagem de values-prod.yaml. image: "geforce8400gsd/giropops-senhas:1.0"

Deploy:

Title

Ao respeitar todas as Policys, o deploy foi realizado com sucesso!


CI/CD - GitHub Actions

github

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.

Provisionando o Server para CI/CD com Github Actions Runners Localmente.

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

WORKFLOW

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

About

Projeto final do Programa Intensivo em Containers e Kubernetes | PICK LINUXtips

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published