Construindo uma API de métricas em AWS EC2 com FastAPI, k3s (Kubernetes), Terraform, Ansible e CI/CD com GitHub Actions
API em FastAPI executando em um cluster k3s dentro de uma instância AWS EC2, expondo métricas de performance da máquina e da aplicação, provisionada com Terraform, configurada com Ansible e atualizada via GitHub Actions.
Publicado em: · Última atualização: · 21 min de leitura
Proprósito deste projeto #
Em busca de aprofundar meus conhecimentos em arquitetura de nuvem e nas variadas formas de se construir ambientes em nuvem, decidi construir do zero uma API e provisioná-la em uma instância EC2 Linux para testar certos conceitos dos quais tenho aprendido nos últimos dias, este principalmente em meus estudos para certificação AWS Cloud Architect.
Objetivamente, desenvolvi uma API em Python (FastAPI) que coleta métricas da instância e da aplicação nela sendo executada e a coloquei para rodar em um cluster Kubernetes numa instância EC2 na AWS. Toda a infraestrutura é provisionada com Terraform, a configuração do servidor e do cluster é feita com Ansible, e o deploy é automatizado com GitHub Actions. O projeto foi pensado para ser integrado posteriormente com Prometheus e Grafana para observabilidade completa.
Objetivos deste projeto #
- Construir uma API simples que exporá métricas de performance da instância e da própria aplicação, em Python (servida pelo web framework FastAPI);
- Rodará num cluster Kubernetes dentro de uma instância AWS EC2 Linux;
- Será provisionada por Terraform (VPC, sub-rede, EC2, Security Groups, IAM);
- Configurada com Ansible (instala Docker, K3s, dependências, faz deploy);
- Atualizada via CI/CD com GitHub Actions (build da imagem e pull do DockerHub, push, deploy automático).
Diagrama de arquitetura do projeto #

API com FastAPI #
O papel desta API é expor métricas de desempenho tanto do host onde o pod está executando quanto da própria aplicação, isto é, a API em si, software backend escrito em Python, empacotado em Docker e executado dentro de um cluster Kubernetes — cujo propósito é coletar e expor métricas operacionais tanto do próprio ambiente onde está rodando quanto do seu estado interno.
Trata-se de uma camada fundamental para permitir integração futura com ferramentas de observabilidade como Prometheus e Grafana. Para isso, optei pelo framework FastAPI, que oferece rotas rápidas, excelente performance assíncrona e um modelo interno de documentação automática. Essa escolha viabiliza uma comunicação eficiente entre os componentes distribuídos do cluster, além de proporcionar simplicidade na implementação dos endpoints.
O design da API serve os seguintes endpoints:
GET /health: usado pelo Kubernetes para testes de Liveness e Readiness probe.- Exemplo:
{ "status": "ok", "app": "k8s-cluster-performance-stack", "version": "1.0.0" }GET /info: fornece informações sobre a aplicação que está rodando na instância.- Exemplo:
{ "app_name": "k8s-cluster-performance-stack", "version": "1.0.0", "environment": "dev", "server_time": "2025-11-15T15:30:00Z" }GET /metrics/system: métricas do host onde o pod está rodando.- Exemplo:
{ "cpu": { "percent": 21.3, "cores": 2 }, "memory": { "total_mb": 993, "used_mb": 450, "percent": 45.3 }, "disk": { "total_gb": 20.0, "used_gb": 8.4, "percent": 42.0 }, "load_average": { "1m": 0.42, "5m": 0.36, "15m": 0.30 } }GET /metrics/app: métricas da própria aplicação (em nível “aplicação”).- Exemplo:
{ "uptime_seconds": 1234, "requests_count": 87, "startup_time": "2025-11-15T15:00:00Z" }
As seguintes dependências são utilizadas como requisitos para que a API seja executada, estando localizadas em um arquivo .txt na raiz do repositório do projeto:
fastapi==0.121.3
uvicorn[standard]==0.38.0
psutil==7.1.3
python-dotenv==1.2.1
Para testar localmente, utilizei o Uvicorn, uma implementação de servidor baseado no protocolo ASGI, em vistas de utilizar a seção /docs do FastAPI:
uvicorn app.main:app --reload
O repositório com o código da API pode ser acessado aqui.
Conteinerização da API com Docker #
Dando continuidade, dá-se a conteinerização da API, pois todo o restante da arquitetura depende de uma imagem consistente, padronizada e facilmente replicável. Para isso, crio um Dockerfile minimalista, baseado em uma imagem “slim” do Python 3.12, garantindo que o ambiente fosse leve, rápido para construir e adequado para rodar em máquinas com recursos limitados, como uma instância EC2 t3.small utilizada no Free Tier da AWS.
Dentro do Dockerfile, defino boas práticas essenciais como a configuração das variáveis de ambiente PYTHONDONTWRITEBYTECODE e PYTHONUNBUFFERED, que reduzem o overhead de escrita em disco e melhoram a observabilidade de logs. Em seguida, instala-se as dependências de compilação necessárias para pacotes como psutil, inclui-se o requirements.txt para instalar as dependências Python e copia-se o código da pasta app/ para dentro da imagem. Por fim, configuro o container para expor a porta 8000 e rodar o servidor Uvicorn, permitindo que a API FastAPI responda requisições HTTP dentro do cluster Kubernetes.
Arquivo Docker na raiz do projeto #
# =========================
# 1) Builder: installs deps
# =========================
FROM python:3.12-slim AS builder
# Better logging and no .pyc
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Build dependencies (psutil, etc.)
RUN apt-get update && \
apt-get install -y --no-install-recommends \
build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install Python dependencies in a venv
COPY requirements.txt .
RUN python -m venv /opt/venv \
&& /opt/venv/bin/pip install --upgrade pip \
&& /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
# =========================
# 2) Runtime: final image
# =========================
FROM python:3.12-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
WORKDIR /app
# Copy the already-prepared virtual environment
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# Copy only the application code
COPY app ./app
# API port
EXPOSE 8000
# Default variables
ENV APP_ENV=prod
# Command to run the FastAPI API
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Build e push manual da imagem (para teste) #
# Build the image
docker build -t cassiano00/metrics-api:latest .
# Test locally
docker run --rm -p 8000:8000 cassiano00/metrics-api:latest
# Push to Docker Hub (for Kubernetes to pull)
docker push cassiano00/metrics-api:latest

Orquestração em um cluster Kubernetes #
Dada a imagem montada, orquestro um cluster Kubernetes através de manifests YAML. O primeiro componente criado foi o namespace.yaml, uma prática fundamental que organiza logicamente os recursos e evita conflitos entre serviços diferentes dentro do cluster. Criar o namespace metrics-api garante isolamento e facilita futuras operações de gerenciamento e automação.
Para testar localmente, utilizo o Minikube, que implementa um cluster Kubernetes local.
apiVersion: v1
kind: Namespace
metadata:
name: metrics-api
labels:
name: metrics-api
Aplicando as configurações presentes no arquivo namespace.yaml:
kubectl apply -f k8s/namespace.yaml

Em seguida, definino o deployment.yaml, recurso central no Kubernetes responsável por gerenciar e manter a aplicação em execução de forma declarativa. No Deployment, configuro o lançamento de 1 (uma) réplica, visto que é um ambiente de teste. Especifico a imagem Docker construída anteriormente e adiciono probes de liveness e readiness apontando para o endpoint /health. Essas probes são essenciais para que o Kubernetes detecte automaticamente falhas de containers e garanta que somente instâncias saudáveis recebam tráfego. Além disso, configuro requests e limits de CPU e memória, evitando que o container consuma mais recursos do que deveria — o que é crítico em ambientes pequenos como um EC2 de baixo custo.
apiVersion: apps/v1
kind: Deployment
metadata:
name: metrics-api-deployment
namespace: metrics-api
labels:
app: metrics-api
spec:
replicas: 1
selector:
matchLabels:
app: metrics-api
template:
metadata:
labels:
app: metrics-api
spec:
containers:
- name: metrics-api
image: cassiano00/metrics-api:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8000
env:
- name: APP_ENV
value: "prod"
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 10
periodSeconds: 20
timeoutSeconds: 2
failureThreshold: 3
Executando as configurações:
kubectl apply -f k8s/deployment.yaml

Com o Deployment definido, crio um service.yaml do tipo ClusterIP para fornecer um endpoint interno estável que abstrai os pods. O Service expõe a porta 80 internamente e repassa chamadas para a porta 8000 do container, padronizando o acesso e permitindo que outros componentes, como o Ingress, interajam com o backend sem depender da estrutura interna do Deployment. Esta é uma boa prática de isolamento em clusters.
apiVersion: v1
kind: Service
metadata:
name: metrics-api-service
namespace: metrics-api
labels:
app: metrics-api
spec:
selector:
app: metrics-api
ports:
- name: http
port: 80
targetPort: 8000
type: ClusterIP
Aplicando as configurações:
kubectl apply -f k8s/service.yaml

Finalizando a camada Kubernetes, implemento o arquivo ingress.yaml, responsável por fornecer uma interface HTTP externa ao cluster por meio do NGINX Ingress Controller. No ambiente local, onde o Minikube está sendo executado com driver Docker no Windows, o acesso direto ao IP interno do cluster não é possível. Por isso, utilizo o minikube tunnel, que cria uma rota entre o host e o Ingress Controller, expondo o tráfego de entrada de forma confiável em 127.0.0.1.
Com essa configuração, o Ingress atua como o ponto de entrada oficial da aplicação dentro do cluster, mapeando requisições externas para o Service interno (metrics-api-service:80). Isso remove a necessidade de túneis temporários como o minikube service ... --url e garante um fluxo de tráfego idêntico ao utilizado em ambientes reais: cliente → Ingress NGINX → Service → Pods.
Ao centralizar o acesso externo no Ingress, a arquitetura se torna mais organizada, escalável e alinhada ao padrão utilizado em clusters Kubernetes de produção. Esse componente também habilita futuras extensões — como suporte a TLS, autenticação, rate limiting e roteamento avançado. Além disso, prepara naturalmente o ambiente para a futura integração com Prometheus e Grafana, facilitando a exposição de métricas, dashboards e observabilidade completa da aplicação.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: metrics-api-ingress
namespace: metrics-api
annotations:
kubernetes.io/ingress.class: "nginx"
spec:
rules:
- http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: metrics-api-service
port:
number: 80
Executando as configurações:
kubectl apply -f k8s/ingress.yaml

Testando o endpoint via comando curl:
curl http://127.0.0.1/health
curl http://127.0.0.1/metrics/system

Provisionando a infraestrutura via Terraform #
Ao avançar para a etapa de infraestrutura do projeto, minha prioridade foi estabelecer uma base sólida e reprodutível para executar o cluster Kubernetes que futuramente hospedará a API de métricas. Para isso, iniciei pelo provisionamento da camada de rede e computação utilizando Terraform. O objetivo era garantir que toda a fundação da aplicação — desde a VPC até a instância EC2 — fosse criada de forma declarativa, auditável e consistente. Criei uma VPC dedicada, uma sub-rede pública e uma tabela de rotas conectada a um Internet Gateway, assegurando que a instância tivesse acesso à internet para instalar dependências, baixar imagens e operar o k3s sem restrições. Em seguida, configurei um Security Group com regras estritamente necessárias: acesso SSH para administração e portas HTTP/HTTPS abertas para o futuro Ingress Controller. Feito isso, defini uma instância EC2 t3.small — suficiente para um ambiente de validação — utilizando a AMI do Ubuntu 22.04, garantindo compatibilidade com Docker, k3s e demais ferramentas da stack.
Os seguinte itens serão provisionados em nuvem:
- 1 VPC
- 1 public subnet
- Internet gateway + route table
- 1 ElasticIP (fixed public Ip)
- 1 Security Group (SSH + HTTP/HTTPS)
- 1 instância EC2 (t3.small) que servirá como um node k3s
Configurando a VPC #
Comecei criando uma VPC dedicada com um bloco CIDR amplo (10.0.0.0/16), permitindo flexibilidade para expansão futura de sub-redes, balanceadores ou nós adicionais. Em seguida, configurei uma sub-rede pública (10.0.1.0/24) com map_public_ip_on_launch habilitado, garantindo que instâncias dentro dessa sub-rede recebessem um IP público automaticamente, eliminando a necessidade de Elastic IPs no ambiente de validação. Associei essa sub-rede a uma tabela de rotas contendo a rota padrão (0.0.0.0/0) direcionada para um Internet Gateway recém-criado, assegurando conectividade externa total — algo essencial para que o nó pudesse baixar pacotes, realizar pull de imagens Docker e se comunicar com registries públicos. Também provisionei um endereço IPv4 estático, através de um ElasticIP associado à instância.
main.tf:
# VPC
resource "aws_vpc" "this" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "${var.project_name}-vpc"
}
enable_dns_hostnames = true
enable_dns_support = true
}
# Public Subnet
resource "aws_subnet" "public" {
vpc_id = aws_vpc.this.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-subnet"
}
}
# Public Route Table for subnet
resource "aws_route_table" "public" {
vpc_id = aws_vpc.this.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.this.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
...
# Elastic IP associated with the k3s_node instance
resource "aws_eip" "k3s_eip" {
domain = "vpc"
instance = aws_instance.k3s_node.id
tags = {
Name = "${var.project_name}-eip"
}
}
Configurando um Security Group #
Na parte de segurança, construí um Security Group seguindo o princípio do menor privilégio, liberando apenas o tráfego necessário: porta 22 para SSH (limitada via variável parametrizável), porta 80 para receber tráfego HTTP futuramente via Ingress e porta 443 para antecipar cenários de TLS. Todo o restante permaneceu bloqueado.
main.tf:
# Security Group for EC2 instance
resource "aws_security_group" "ec2_sg" {
name = "${var.project_name}-ec2-sg"
description = "Security group for metrics API k3s node"
vpc_id = aws_vpc.this.id
# SSH access
ingress {
description = "Allow SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.allowed_ssh_cidr]
}
# HTTP access
ingress {
description = "Allow HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# HTTPS (for future TLS)
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Egress: allow everything
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-ec2-sg"
}
}
Configurando a instância EC2 #
Com a rede estabelecida, finalizei o provisionamento criando uma instância EC2 t3.small, suficiente para cenários de teste, utilizando a AMI oficial do Ubuntu 22.04. Essa escolha foi deliberada, dadas suas otimizações, compatibilidade plena com Docker e suporte nativo a systemd — fundamental para o funcionamento adequado dos serviços do k3s. Assim, toda a camada fundacional do cluster estava definida não apenas de manera declarativa, mas também calibrada para operar workloads containerizados.
main.tf:
# Ubuntu AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# EC2 instance that will run k3s
resource "aws_instance" "k3s_node" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
key_name = var.key_name
associate_public_ip_address = false
tags = {
Name = "${var.project_name}-k3s-node"
}
}
Antes deste processo, já havia configurado meu ambiente com o AWS CLI, para que as devidas permissões de conexão com meu perfil AWS fossem estabelecidas.
Também tive que criar uma key pair na região sa-east-1 para que o Terraform pudesse aplicar as configurações para criação da instância EC2, esta que se encontra como key_name = var.key_name.
Assim, iniciando o ambiente Terraform no diretório /infra/terraform e aplicando as configurações estabelecidas:
terraform init
terraform apply
O ambiente foi devidamente provisionado na AWS:


O restante das configurações em código do Terraform podem ser acessadas aqui.
Implementando as configurações com Ansible #
Com a infraestrutura provisionada, avancei para a etapa de automação da configuração do nó Kubernetes utilizando Ansible. Minha meta era transformar uma instância recém-criada em um nó Kubernetes funcional, pronto para receber workloads, sem qualquer configuração manual. Estruturei um playbook que executa desde a atualização de pacotes e instalação de dependências base, até a configuração completa do runtime de containers e da própria distribuição k3s. Instalei o Docker para garantir suporte a workloads baseados em containerd e compatibilidade com ferramentas de desenvolvimento, e, em seguida, executei o script oficial de instalação do k3s com opções específicas — incluindo a desativação do Traefik, preservando o cluster limpo para implementações posteriores.
playbook.yaml:
---
- name: Configure EC2 as k3s Kubernetes node
hosts: k3s_node
become: true
vars:
k3s_version: ""
tasks:
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install base packages
apt:
name:
- curl
- wget
- git
state: present
- name: Install Docker
apt:
name:
- docker.io
state: present
- name: Enable and start Docker
systemd:
name: docker
state: started
enabled: true
- name: Add ubuntu user to docker group
user:
name: ubuntu
groups: docker
append: yes
- name: Download and install k3s
shell: |
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --disable traefik" sh -
args:
creates: /usr/local/bin/k3s
- name: Install NGINX Ingress Controller
become: false
command: >
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.14.0/deploy/static/provider/cloud/deploy.yaml
- name: Wait for k3s service to be active
systemd:
name: k3s
state: started
enabled: true
- name: Ensure .kube directory exists for ubuntu user
file:
path: /home/ubuntu/.kube
state: directory
owner: ubuntu
group: ubuntu
mode: "0700"
- name: Copy k3s kubeconfig to ubuntu user
copy:
src: /etc/rancher/k3s/k3s.yaml
dest: /home/ubuntu/.kube/config
owner: ubuntu
group: ubuntu
mode: "0600"
remote_src: yes
- name: Export KUBECONFIG in ubuntu .bashrc
lineinfile:
path: /home/ubuntu/.bashrc
regexp: '^export KUBECONFIG='
line: 'export KUBECONFIG=$HOME/.kube/config'
create: yes
owner: ubuntu
group: ubuntu
mode: "0644"
- name: Create kubectl symlink for convenience
file:
src: /usr/local/bin/kubectl
dest: /usr/local/bin/k3s-kubectl
state: link
ignore_errors: yes
- name: Ensure kubectl installed (k3s includes it)
file:
src: /usr/local/bin/k3s
dest: /usr/local/bin/kubectl
state: link
ignore_errors: yes
Junto com as especificações do meu host no arquivo local host_vars/k3s-ec2-node.yaml:
ansible_host: <instance-public-ip>
ansible_user: ubuntu
ansible_ssh_private_key_file: <metrics-api-key.pem-local-address>
inventory.ini:
[k3s_node]
k3s-ec2-node
Assim, com as configurações dadas, executo o comando para o Ansible aplicá-las:
ansible-playbook -i inventory.ini playbook.yaml

Após a instalação do k3s via Ansible, mantive a kubeconfig padrão gerada pelo serviço em /etc/rancher/k3s/k3s.yaml, sem modificar o endpoint 127.0.0.1:6443, uma vez que a administração do cluster será realizada diretamente dentro da própria instância EC2, utilizando kubectl. Durante a automação, o playbook cuidou da instalação do Docker, da ativação do k3s como serviço, da configuração dos binários e symlinks necessários para o uso do cliente kubectl, garantindo que todos os utilitários essenciais estivessem disponíveis para o usuário padrão da máquina. Ao final dessa etapa, a instância EC2 já operava como um nó Kubernetes funcional, inicializado e pronto para receber aplicações e deployments automatizados via pipeline CI/CD.
Para verificar o estado atual da instância, fiz um acesso via SSH, para averiguar o node sendo executado dentro dela:
Pipeline CI/CD com GitHub Actions #
Com a infraestrutura provisionada e o cluster k3s operacional na EC2, é hora de projetar uma pipeline CI/CD totalmente automatizada usando GitHub Actions. O objetivo é eliminar qualquer necessidade de intervenção manual tanto no build quanto no deploy, garantindo que toda alteração no código da API resulte em uma nova versão do serviço executando no cluster de forma imediata, previsível e confiável.
Bootstrap do cluster na instância EC2 #
Antes de continuar com a crição do fluxo CI/CD, é importante notar que a instância encontra-se atualmente sem os objetos Kubernetes na EC2. Se for rodar o comando kubectl get ns e kubectl get all -n metrics-api, o resultado será que metrics-api não aparece na lista de namespaces (forma de organizar e isolar recursos dentro de um cluster) e nada existe em -n metrics-api (porque o namespace não existe).

Dessa forma, é necessário:
- Criar o namespace
metrics-api; - Criar o Deployment
metrics-api-deployment, o Service, e o Ingress.
Para isso, farei uma bootstrap manual na EC2, ou seja, uma inicialização e configuração da instância de forma manual.
Copiei os manifests Kubernetes da pasta
/k8spara dentro da EC2 (do seu WSL):scp -i ~/.ssh/metrics-api-key.pem -r k8s ubuntu@<public-ip-instance>:/home/ubuntu/k8s

Apois isso, acessei à instância EC2, para verificar se a pasta estava presente:
ssh -i ~/.ssh/metrics-api-key.pem ubuntu@<public-ip-instance>

Apliquei os manifests dentro da instância:
kubectl apply -f /home/ubuntu/k8s/namespace.yaml kubectl apply -f /home/ubuntu/k8s/deployment.yaml kubectl apply -f /home/ubuntu/k8s/service.yaml kubectl apply -f /home/ubuntu/k8s/ingress.yamlE verifiquei se os manifests dentro da instância estão rodando:
kubectl get ns kubectl get deploy -n metrics-api kubectl get pods -n metrics-api kubectl get svc -n metrics-api
Desta forma:
- O namespace
metrics-apiexistirá - O
metrics-api-deploymentexistirá - O Service e o Ingress existirão
Assim, na próxima execução do GitHub Actions, o comando:
kubectl -n metrics-api set image deployment/metrics-api-deployment \
metrics-api=${IMAGE}
vai funcionar, pois as especificações do deployment já existem e só precisam da imagem Docker.

Os seguintes secrets do Repositório foram estabelecidos para serem chamados no arquivo ci-cd.yaml:
DOCKERHUB_USERNAME- nome de usuário no Docker HubDOCKERHUB_TOKEN- token de acesso/senha do Docker HubEC2_HOST- IP público da instância (valorec2_public_ipgerado pelo Terraform)EC2_SSH_USER- escolhiubuntupara AMIs do UbuntuEC2_SSH_KEY- conteúdo da chave privada para SSH (arquivo.pem)
Esse processo inicial cria a estrutura base do cluster — incluindo o namespace metrics-api e os recursos essenciais — permitindo que o pipeline automatizado trabalhe sobre uma fundação já existente. Uma vez que esses objetos iniciais estão aplicados no cluster, todas as atualizações subsequentes podem ser controladas exclusivamente pelo CI/CD, sem necessidade de reaplicar manifests.
Com essa fundação criada, desenvolvi o pipeline do GitHub Actions dividido em duas fases:
Build e Push da imagem Docker: O workflow inicia realizando checkout do repositório e configurando o Docker Buildx. Em seguida, constrói a imagem da API e realiza o push para o Docker Hub utilizando duas tags:
latest, destinada ao ambiente de desenvolvimento contínuo, e uma tag imutável baseada no SHA do commit, assegurando rastreabilidade e versionamento confiável. Essa abordagem garante que cada build seja reproduzível e associado a um ponto específico da evolução do código.Deploy automático no cluster k3s: Na segunda etapa, o pipeline estabelece uma conexão SSH com a instância EC2 e utiliza
kubectl set imagepara atualizar o Deployment existente com a nova imagem publicada. Como o cluster já possui todos os manifests aplicados no bootstrap inicial, o pipeline precisa apenas ajustar a imagem do Deployment, tornando o processo rápido, eficiente e sem duplicação de recursos. Após a atualização, o workflow aguarda o rollout para confirmar que a nova versão foi aplicada com sucesso.

Assim, a pipeline completa — do código à atualização em produção — tornou-se totalmente automatizada, determinística e alinhada às melhores práticas modernas de CI/CD em ambientes Kubernetes.
O código na íntegra da pipeline no GitHub Actions pode ser acesso aqui.
Testes realizados dentro da instância EC2 após deploy #
Visão geral do node, recursos no namespace da API e listagem dos pods #

Teste nos endpoints da API via curl dentro da instância EC2
#

Acessando a seção docs do FastAPI via IP pública da instância EC2
#

Sumário: Acesso à API na EC2 e utilização prática da instância #
Com o provisionamento consolidado, a instância EC2 deixa de ser apenas um “nó de infraestrutura” e passa a atuar como ponto de entrada estável para a API de métricas. A seguir, descrevo como ela está exposta, como pode ser consumida externamente e como pode servir de base para extensões futuras do projeto.
Características da instância e do endpoint público #
A instância EC2 foi provisionada como um nó único de Kubernetes com k3s, utilizando o tipo t3.small, o que garante 2 vCPUs e 2 GiB de RAM — um equilíbrio melhor entre custo e capacidade para rodar o cluster, o Ingress Controller e a aplicação simultaneamente.
Do ponto de vista de rede, a instância está em:
Uma VPC dedicada (
10.0.0.0/16), com:- Sub-rede pública (
10.0.1.0/24) para o nó k3s; - Internet Gateway associado à VPC;
- Tabela de rotas pública com saída
0.0.0.0/0apontando para o IGW;
- Sub-rede pública (
Um Security Group específico para o nó k3s, permitindo:
- SSH (porta 22) apenas a partir do CIDR definido em
allowed_ssh_cidr; - HTTP (porta 80) aberto para
0.0.0.0/0, para acesso público à API; - HTTPS (porta 443) aberto para futuros cenários com TLS.
- SSH (porta 22) apenas a partir do CIDR definido em
Além disso, a instância utiliza um Elastic IP, provisionado via Terraform e exposto por meio do output ec2_public_ip. Isso garante que:
- O IPv4 público da instância permanece estável entre reinicializações;
- O pipeline CI/CD e quaisquer clientes externos podem apontar para um endereço fixo, sem necessidade de atualizar configurações a cada stop/start.
Na prática, é esse Elastic IP que funciona como “endpoint público bruto” da API.
Exposição da API via Ingress NGINX #
Internamente, o tráfego HTTP é roteado pelo NGINX Ingress Controller, instalado no cluster k3s. A topologia lógica fica assim:
Cliente → Elastic IP (port 80) → EC2 (Security Group)
→ ingress-nginx Service LoadBalancer
→ Ingress (rules / path /)
→ Service ClusterIP (metrics-api-service)
→ API pod (container FastAPI on the port 8000)
O recurso Ingress configurado para o namespace metrics-api faz o roteamento da raiz / para o metrics-api-service, que por sua vez encaminha as requisições para o Deployment metrics-api-deployment. Isso significa que, externamente, o consumo da API pode ser feito de forma direta, utilizando o Elastic IP na porta 80:
curl http://<elastic-ip>/health
Esse endpoint /health é exposto pela aplicação FastAPI e funciona como uma verificação simples de disponibilidade do serviço. Dessa forma, qualquer cliente HTTP — desde scripts de monitoramento até ferramentas de observabilidade — pode utilizar esse endereço para validações básicas ou integrações com sondas de saúde.
Utilização da instância para inspeção, debug e operação #
Além de servir a API externamente, a instância também é o ponto central para operações administrativas e de troubleshooting. A partir de uma sessão SSH utilizando a chave privada (EC2_SSH_KEY) e o usuário configurado (EC2_SSH_USER, no caso ubuntu), é possível:
Inspecionar o estado do cluster:
kubectl get nodes -o wide kubectl get all -n metrics-apiAcompanhar o comportamento dos pods da aplicação:
kubectl get pods -n metrics-api -o wide kubectl logs -n metrics-api <nome-do-pod>Testar a API de dentro do cluster, seja via port-forward, seja executando comandos dentro do pod:
# Port-forward of the Service to the local machine (inside EC2) kubectl port-forward svc/metrics-api-service -n metrics-api 8000:80 curl http://127.0.0.1:8000/health # Direct call from inside the pod kubectl exec -it -n metrics-api <pod-name> -- curl -s http://127.0.0.1:8000/health
Isso transforma a instância em um ponto único de observação do ciclo de vida da aplicação: desde o nível Kubernetes (Deployments, Pods, Services, Ingress) até o nível de aplicação (logs, respostas HTTP, status de saúde).
Extensões futuras: observabilidade, TLS e domínios customizados #
A forma como a instância foi desenhada permite uma série de evoluções naturais, sem necessidade de reestruturar a base:
Integração com Prometheus e Grafana: O cluster k3s atual pode receber um stack de observabilidade (como kube-prometheus-stack ou Prometheus Operator) para coletar métricas tanto da infraestrutura quanto da API. O Ingress Controller já oferece um ponto de entrada HTTP que pode ser reutilizado para expor dashboards ou endpoints de métricas.
TLS e domínio customizado (HTTPS): Como a API já está exposta por um Elastic IP, é simples criar um registro DNS em um domínio próprio (via Route 53 ou outro provedor) apontando para esse IP. Em seguida, basta adicionar um novo Ingress com host definido (por exemplo,
api.metrics.meudominio.com) e integrar um emissor de certificados (como AWS Certificate Manager ou Let’s Encrypt) para servir tráfego HTTPS.Ampliação do cluster e novos serviços: A instância atual pode ser o ponto de partida para hospedar outros microservices relacionados a métricas, dashboards ou APIs auxiliares. Novos namespaces, Deployments e Services podem ser adicionados ao cluster, todos expostos via NGINX Ingress Controller com regras específicas de rota e host.
Em resumo, a instância EC2 — combinada com k3s, Ingress NGINX, Docker e a pipeline CI/CD no GitHub Actions — passa a operar como um ambiente de aplicação completo, apto não apenas para servir a API de métricas em produção, mas também para suportar testes, experimentos e futuras extensões em termos de observabilidade, segurança (TLS) e escalabilidade lógica da solução.