Repositório da Aula

Aula 14 – K8S – Configuração do Ingress Controller

⚠️ Pré-requisitos:

Esta aula assume que você já concluiu a Aula 13, com os serviços backend e frontend funcionando via LoadBalancer.

📚 Introdução

Ao longo das aulas anteriores, configuramos nosso ambiente no Kubernetes, garantindo a persistência de dados com PV/PVC, a resiliência da aplicação com probes e a gestão segura de credenciais com Secrets. No entanto, até agora, o acesso ao nosso backend e frontend tem sido feito via Service.type = LoadBalancer, o que pode ser custoso e difícil de escalar.

📌 O que você aprenderá nesta aula?

  • O que é um Ingress Controller e qual sua importância.
  • Diferença entre LoadBalancer e Ingress no Kubernetes.
  • Instalação do NGINX Ingress Controller com Terraform + Helm.
  • Criação do recurso Ingress para rotear tráfego para frontend e backend.
  • Adaptação do cluster para usar Ingress em vez de múltiplos LoadBalancers.
  • Testes e validação da comunicação via Ingress.

💡 Retrospectiva rápida

Recurso O que faz? Quando usamos?
PV / PVC Armazena dados de forma persistente. Usamos na aula anterior para o MySQL.
Probes Garantem que os containers estejam saudáveis. Usamos readiness e liveness para backend e frontend.
Secrets Armazenam dados sensíveis, como senhas e tokens. Usamos para MySQL e Gmail.
LoadBalancer Expõe um serviço diretamente ao tráfego externo. Usamos para o frontend nas aulas anteriores.

🔀 O que é o Ingress?

O Ingress atua como um ponto único de entrada HTTP/HTTPS para dentro do cluster, onde você pode criar regras de roteamento para distribuir o tráfego entre múltiplos serviços com base na URL.

🧠 Analogia didática

  • LoadBalancer é como o porteiro: ele só abre a porta e encaminha para um único serviço.
  • Ingress é como a recepcionista: ela escuta qual o setor que você quer e direciona para o serviço certo (frontend, backend, admin...).

🧭 Diferença principal entre LoadBalancer e Ingress

Recurso O que faz Quem cria/controla
LoadBalancer Expõe um serviço diretamente com IP público (ex: service.type = LoadBalancer) Cloud Provider (DigitalOcean, AWS, etc)
Ingress Controlador de rotas HTTP. Permite vários serviços por um único IP. Ingress Controller (NGINX, Traefik...)

🔧 Ferramenta extra: Helm

Helm é um gerenciador de pacotes para Kubernetes (como o apt ou npm). Ele nos permite instalar aplicativos complexos com um único comando. No nosso caso, usaremos o Helm para instalar o NGINX Ingress Controller.

🎯 Objetivo da Aula

Manter o mesmo projeto da Aula 13, com frontend React e backend Fiber, mas agora com o tráfego externo sendo roteado através de um único Ingress Controller. As imagens Docker já estão publicadas no Docker Hub, e vamos modificar apenas o necessário.

🧐 Mas e a aula passada? A gente usou só 1 LoadBalancer, não foi?

Sim, na aula anterior (Aula 13), o único serviço exposto externamente era o frontend, usando um Service do tipo LoadBalancer. O backend era um ClusterIP, acessado apenas internamente pelo frontend. Isso funcionava, mas tem limitações:
  • Só servia para este caso simples (frontend + backend)
  • Se precisássemos expor mais serviços (como admin, API externa, etc), cada um precisaria de um LoadBalancer separado, gerando mais custos e mais IPs públicos
Com o Ingress Controller, resolvemos isso de forma mais elegante e escalável.

🧠 Mesmo com 1 LoadBalancer, o Ingress ainda traz vantagens:

  • Permite /api, /admin, /outro-app em um único IP público
  • Melhor integração com HTTPS e certificados TLS (via cert-manager)
  • Centraliza o roteamento e facilita o controle de entrada de tráfego
É por isso que, mesmo tendo usado só um LoadBalancer antes, faz sentido migrar para o uso do Ingress nesta aula.

🗂️ Estrutura atualizada do projeto

codigo-fluente-fiber-tutorial/
├── backend/
│   └── fiber-project/
├── frontend/
│   └── react-auth/
│       └── nginx.conf
├── devops/
│   ├── Dockerfile-backend
│   ├── Dockerfile-frontend
│   ├── deployment.tf           <-- Modificado nesta aula
│   ├── helm_ingress.tf         <-- Criado nesta aula
│   ├── ingress.tf              <-- Criado nesta aula
│   ├── main.tf                 <-- Modificado nesta aula
│   ├── outputs.tf              <-- Modificado nesta aula
│   ├── terraform.tfstate
│   ├── terraform.tfstate.backup
│   ├── terraform.tfvars
│   ├── variables.tf            <-- Modificado nesta aula

🚧 Alterações feitas nesta aula

  • Criação do devops/terraform/ingress.tf – ele define as regras de roteamento HTTP.
  • Criação do devops/terraform/helm_ingress.tf – instala o NGINX Ingress Controller com Helm.
  • Removido o LoadBalancer do frontend – agora usaremos apenas o Ingress.

📁 devops/helm_ingress.tf

🔹 Explicando o recurso helm_release "ingress_nginx"

1. O que esse recurso faz?

Esse bloco instala o NGINX Ingress Controller no cluster Kubernetes usando o Helm, que é um gerenciador de pacotes para K8s. Ele automatiza toda a configuração do Ingress sem precisar criar arquivos YAML manualmente.

2. Explicação dos parâmetros utilizados

resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  namespace  = "default"
  timeout    = 600
Explicação: Esse bloco define: - name: nome dado à instalação Helm. - repository: onde está localizado o chart oficial do NGINX. - chart: nome do pacote Helm a ser instalado. - namespace: espaço lógico onde o Ingress será instalado. - timeout: tempo limite de instalação (em segundos).

3. Configurações adicionais com set

set {
  name  = "controller.service.type"
  value = "LoadBalancer"
}

set {
  name  = "controller.publishService.enabled"
  value = "true"
}
Explicação: - O primeiro set configura o tipo do serviço do Ingress como LoadBalancer, o que é necessário na DigitalOcean (ou outros provedores) para que um IP público seja atribuído automaticamente. - O segundo set garante que o serviço seja publicado e que seu IP fique disponível para uso posterior, como na variável `APP_URL` do backend. 🧩 Esse recurso é essencial para que o Ingress Controller funcione corretamente e para que o Kubernetes possa receber e redirecionar requisições HTTP externas ao cluster de forma centralizada, segura e escalável.

devops/helm_ingress.tf


resource "helm_release" "ingress_nginx" {
  name       = "ingress-nginx"
  repository = "https://kubernetes.github.io/ingress-nginx"
  chart      = "ingress-nginx"
  namespace  = "default"
  timeout    = 600

  # Precisamos do Service tipo LoadBalancer
  # para a DigitalOcean provisionar IP externo
  set {
    name  = "controller.service.type"
    value = "LoadBalancer"
  }

  # Publicar service do controller para relatarmos IP
  set {
    name  = "controller.publishService.enabled"
    value = "true"
  }
}

📁 devops/ingress.tf (criação das rotas públicas via Ingress)

🔹 Explicando o recurso kubernetes_ingress_v1

1. Para que serve esse arquivo?

Esse recurso cria um Ingress, que funciona como um "porteiro" central para direcionar as requisições externas (HTTP) para os serviços corretos no cluster Kubernetes. Ele substitui a necessidade de usar múltiplos LoadBalancers, o que reduz custos e organiza melhor o tráfego.

2. Annotations utilizadas


"kubernetes.io/ingress.class"                 = "nginx"
"nginx.ingress.kubernetes.io/ssl-redirect"    = "false"
"nginx.ingress.kubernetes.io/proxy-body-size" = "8m"
Explicação: - A primeira define que o Ingress será gerenciado pelo NGINX. - A segunda evita redirecionamento automático para HTTPS (útil em dev). - A terceira aumenta o limite de tamanho para uploads no body de requisições (útil para formulários grandes).

3. Rotas da API

As seguintes rotas são direcionadas ao serviço auth-api (porta 3000): - /api - /forgot - /reset - /register - /login - /logout - /user Exemplo: Se um usuário acessar https://seusite.com/api/user, o tráfego será roteado internamente para o serviço da API no cluster.

4. Rotas do frontend

As seguintes rotas são enviadas para o serviço do frontend (porta 80): - /reset/ (com barra – para a visualização do formulário por token) - /static (arquivos estáticos como CSS e JS) - / (todas as outras rotas, como /home, /dashboard etc.) Por que separar /reset e /reset/? Para permitir que o POST /reset vá para a API e o GET /reset/token vá para o frontend.

5. Dependência do Helm

depends_on = [helm_release.ingress_nginx]
Explicação: Esse bloco garante que o Ingress só será criado depois que o Ingress Controller (instalado via Helm) estiver pronto no cluster. Esse arquivo é essencial para habilitar o acesso externo com Ingress, e foi cuidadosamente roteado para separar backend e frontend da forma correta.

devops/ingress.tf


resource "kubernetes_ingress_v1" "app_ingress" {
  metadata {
    name = "app-ingress"
    annotations = {
      "kubernetes.io/ingress.class"                 = "nginx"
      "nginx.ingress.kubernetes.io/ssl-redirect"    = "false"
      "nginx.ingress.kubernetes.io/proxy-body-size" = "8m"
    }
  }

  spec {
    ingress_class_name = "nginx"

    rule {
      http {
        # Todas as rotas da API
        path {
          path      = "/api"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        path {
          path      = "/forgot"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        # Rota exata para POST /reset (processamento do formulário)
        path {
          path      = "/reset"
          path_type = "Exact"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        # Rota para /reset/[token] (visualização do formulário)
        path {
          path      = "/reset/"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_ui.metadata[0].name
              port {
                number = 80
              }
            }
          }
        }

        path {
          path      = "/register"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        path {
          path      = "/login"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        path {
          path      = "/logout"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        path {
          path      = "/user"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_api.metadata[0].name
              port {
                number = 3000
              }
            }
          }
        }

        # Serve arquivos estáticos
        path {
          path      = "/static"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_ui.metadata[0].name
              port {
                number = 80
              }
            }
          }
        }

        # Todas as outras rotas (como /reset/:token) vão para o frontend
        path {
          path      = "/"
          path_type = "Prefix"
          backend {
            service {
              name = kubernetes_service.auth_ui.metadata[0].name
              port {
                number = 80
              }
            }
          }
        }
      }
    }
  }

  depends_on = [helm_release.ingress_nginx]
}

📁 devops/deployment.tf (alterações no backend e frontend)

🔹 Explicando as alterações destacadas em azul

1. Substituição do IP do LoadBalancer do frontend pelo IP do Ingress Controller

ANTES:
value = "http://${data.kubernetes_service.auth_ui.status[0].load_balancer[0].ingress[0].ip}"
DEPOIS:
value = "http://${data.kubernetes_service.ingress_nginx.status[0].load_balancer[0].ingress[0].ip}"
Explicação: Com o Ingress gerenciando o tráfego externo, agora o backend usa o IP do Ingress Controller para formar a variável APP_URL. Isso garante que a aplicação saiba qual é sua URL pública correta.

2. Adição do data "kubernetes_service" para o Ingress Controller

data "kubernetes_service" "ingress_nginx" {
  metadata {
    name      = "ingress-nginx-controller"
    namespace = "default"
  }
  depends_on = [helm_release.ingress_nginx]
}
Explicação: Esse bloco coleta o IP externo atribuído automaticamente ao LoadBalancer do Ingress. Esse IP será usado por outras partes da aplicação (como o backend) para compor URLs públicas corretamente.

3. Atualização da imagem do frontend para versão v1.1

ANTES:
image = "toticavalcanti/auth-ui:v1.0"
DEPOIS:
image = "toticavalcanti/auth-ui:v1.1"
Explicação: A nova imagem contém melhorias e o suporte à leitura dinâmica da URL da API a partir do arquivo config.js gerado no container.

4. Adição do comando que gera dinamicamente o config.js com HEREDOC

command = ["/bin/sh", "-c"]
# Usamos a sintaxe de HEREDOC no "args" para evitar problemas de escape
args = [
  <<-EOT echo 'window._env_ = { REACT_APP_API_URL: \"${var.react_app_api_url}\" };' > /usr/share/nginx/html/config.js
    nginx -g 'daemon off;'
  EOT
]
Explicação: Esse comando roda no container do frontend ao iniciar. Ele cria o arquivo config.js que define dinamicamente a variável global REACT_APP_API_URL para o navegador.

5. Alteração do tipo do serviço auth_ui (frontend) para ClusterIP

ANTES:
type = "LoadBalancer"
DEPOIS:
type = "ClusterIP"
Explicação: O frontend não é mais exposto diretamente por um LoadBalancer. Agora ele é acessado internamente via Ingress Controller, o que é mais seguro e econômico.

devops/deployment.tf


data "kubernetes_service" "ingress_nginx" {
  metadata {
    name      = "ingress-nginx-controller"
    namespace = "default"
  }
  depends_on = [helm_release.ingress_nginx]
}

##################################
# SECRETS
##################################
resource "kubernetes_secret" "gmail_credentials" {
  metadata {
    name = "gmail-credentials"
  }
  data = {
    username = var.gmail_username
    password = var.gmail_password
  }
}

resource "kubernetes_secret" "mysql_secret" {
  metadata {
    name = "mysql-secret"
  }
  data = {
    mysql-root-password = base64encode(var.mysql_root_password)
  }
}

##################################
# VOLUMES
##################################
resource "kubernetes_persistent_volume" "mysql_pv" {
  metadata {
    name = "mysql-pv"
    labels = {
      type = "local"
      app  = "mysql"
    }
  }
  spec {
    capacity = {
      storage = "5Gi"
    }
    access_modes                     = ["ReadWriteOnce"]
    storage_class_name               = "manual"
    persistent_volume_reclaim_policy = "Retain"

    persistent_volume_source {
      host_path {
        path = "/mnt/data"
        type = "DirectoryOrCreate"
      }
    }
  }
}

resource "kubernetes_persistent_volume_claim" "mysql_pvc" {
  metadata {
    name = "mysql-pvc"
    labels = {
      app = "mysql"
    }
  }
  spec {
    access_modes = ["ReadWriteOnce"]
    resources {
      requests = {
        storage = "5Gi"
      }
    }
    storage_class_name = "manual"
    volume_name        = kubernetes_persistent_volume.mysql_pv.metadata[0].name
  }
  depends_on = [
    kubernetes_persistent_volume.mysql_pv
  ]
}

##################################
# MYSQL: DEPLOYMENT E SERVICE
##################################
resource "kubernetes_deployment" "mysql" {
  metadata {
    name = "mysql"
    labels = {
      app = "mysql"
    }
  }
  spec {
    replicas = 1
    selector {
      match_labels = {
        app = "mysql"
      }
    }
    template {
      metadata {
        labels = {
          app = "mysql"
        }
      }
      spec {
        container {
          name  = "mysql"
          image = "mysql:5.7"

          env {
            name  = "MYSQL_ROOT_PASSWORD"
            value = var.mysql_root_password
          }

          env {
            name  = "MYSQL_DATABASE"
            value = "mysql"
          }

          resources {
            limits = {
              memory = "512Mi"
              cpu    = "500m"
            }
            requests = {
              memory = "256Mi"
              cpu    = "250m"
            }
          }

          port {
            container_port = 3306
            name           = "mysql"
          }

          volume_mount {
            name       = "mysql-persistent-storage"
            mount_path = "/var/lib/mysql"
          }

          readiness_probe {
            tcp_socket {
              port = 3306
            }
            initial_delay_seconds = 15
            period_seconds        = 10
          }

          liveness_probe {
            tcp_socket {
              port = 3306
            }
            initial_delay_seconds = 20
            period_seconds        = 10
          }
        }

        volume {
          name = "mysql-persistent-storage"
          persistent_volume_claim {
            claim_name = kubernetes_persistent_volume_claim.mysql_pvc.metadata[0].name
          }
        }
      }
    }
  }
  depends_on = [kubernetes_persistent_volume_claim.mysql_pvc]
}

resource "kubernetes_service" "mysql_service" {
  metadata {
    name = "mysql-service"
    labels = {
      app = "mysql"
    }
  }
  spec {
    selector = {
      app = "mysql"
    }
    type = "ClusterIP"
    port {
      port        = 3306
      target_port = 3306
      name        = "mysql"
    }
  }
}

##################################
# BACKEND: DEPLOYMENT E SERVICE
##################################
resource "kubernetes_deployment" "auth_api" {
  metadata {
    name = "auth-api"
    labels = {
      app = "auth-api"
    }
  }
  spec {
    replicas = 1
    selector {
      match_labels = {
        app = "auth-api"
      }
    }
    template {
      metadata {
        labels = {
          app = "auth-api"
        }
      }
      spec {
        init_container {
          name  = "wait-for-mysql"
          image = "busybox:1.28"
          command = [
            "sh", "-c",
            "until nc -z mysql-service 3306; do echo waiting for mysql; sleep 2; done;"
          ]
        }

        container {
          name  = "auth-api"
          image = "toticavalcanti/fiber-auth-api:v1.0"

          env {
            name  = "MYSQL_ROOT_PASSWORD"
            value = var.mysql_root_password
          }

          env {
            name  = "DB_DSN"
            value = "root:${var.mysql_root_password}@tcp(mysql-service:3306)/mysql?parseTime=true"
          }

          env {
            name = "GMAIL_USERNAME"
            value_from {
              secret_key_ref {
                name = kubernetes_secret.gmail_credentials.metadata[0].name
                key  = "username"
              }
            }
          }

          env {
            name = "GMAIL_PASSWORD"
            value_from {
              secret_key_ref {
                name = kubernetes_secret.gmail_credentials.metadata[0].name
                key  = "password"
              }
            }
          }

          env {
            name  = "APP_URL"
            value = "http://${data.kubernetes_service.ingress_nginx.status[0].load_balancer[0].ingress[0].ip}"
          }

          port {
            container_port = 3000
            name           = "http"
          }

          resources {
            limits = {
              cpu    = "250m"
              memory = "256Mi"
            }
            requests = {
              cpu    = "100m"
              memory = "128Mi"
            }
          }

          readiness_probe {
            http_get {
              path = "/api/health"
              port = 3000
            }
            initial_delay_seconds = 15
            period_seconds        = 10
          }

          liveness_probe {
            http_get {
              path = "/api/health"
              port = 3000
            }
            initial_delay_seconds = 20
            period_seconds        = 10
          }
        }
      }
    }
  }
  depends_on = [
    kubernetes_deployment.mysql,
    kubernetes_service.mysql_service
  ]
}

resource "kubernetes_service" "auth_api" {
  metadata {
    name = "auth-api-service"
    labels = {
      app = "auth-api"
    }
  }
  spec {
    selector = {
      app = "auth-api"
    }
    type = "ClusterIP"
    port {
      port        = 3000
      target_port = 3000
      name        = "http"
    }
  }
}

##################################
# FRONTEND: DEPLOYMENT E SERVICE
##################################
resource "kubernetes_deployment" "auth_ui" {
  metadata {
    name = "auth-ui"
    labels = {
      app = "auth-ui"
    }
  }
  spec {
    replicas = 1
    selector {
      match_labels = {
        app = "auth-ui"
      }
    }
    template {
      metadata {
        labels = {
          app = "auth-ui"
        }
      }
      spec {
        container {
          name  = "auth-ui"
          image = "toticavalcanti/auth-ui:v1.1"

          env {
            name  = "REACT_APP_API_URL"
            value = var.react_app_api_url
          }

          command = ["/bin/sh", "-c"]
          # Usamos a sintaxe de HEREDOC no "args" para evitar problemas de escape
          args = [
            <<-EOT echo 'window._env_ = { REACT_APP_API_URL: \"${var.react_app_api_url}\" };' > /usr/share/nginx/html/config.js
              nginx -g 'daemon off;'
            EOT
          ]

          port {
            container_port = 80
            name           = "http"
          }

          readiness_probe {
            http_get {
              path = "/index.html"
              port = 80
            }
            initial_delay_seconds = 10
            period_seconds        = 5
            failure_threshold     = 3
            success_threshold     = 1
            timeout_seconds       = 1
          }

          liveness_probe {
            http_get {
              path = "/index.html"
              port = 80
            }
            initial_delay_seconds = 15
            period_seconds        = 10
            failure_threshold     = 3
            success_threshold     = 1
            timeout_seconds       = 1
          }

          resources {
            limits = {
              cpu    = "200m"
              memory = "256Mi"
            }
            requests = {
              cpu    = "100m"
              memory = "128Mi"
            }
          }
        }
      }
    }
  }
  depends_on = [
    kubernetes_deployment.auth_api
  ]
}

resource "kubernetes_service" "auth_ui" {
  metadata {
    name = "auth-ui-service"
    labels = {
      app = "auth-ui"
    }
  }
  spec {
    selector = {
      app = "auth-ui"
    }
    type = "ClusterIP"
    port {
      port        = 80
      target_port = 80
      name        = "http"
    }
  }
}

📁 devops/main.tf (configuração principal do cluster)

🔹 Explicando as alterações destacadas em azul

1. Adição do provider Helm

helm = {
  source  = "hashicorp/helm"
  version = "2.17.0"
}
Explicação: Foi adicionado o provedor helm para permitir a instalação de pacotes via Helm Charts, como o Ingress Controller, diretamente pelo Terraform.

2. Atualização da versão do Kubernetes

ANTES:
version = "1.31.1-do.3"
DEPOIS:
# Ajuste para a versão que desejar
version = "1.32.2-do.0"
Explicação: A versão do cluster foi atualizada para a mais recente compatível com os recursos desejados. O comentário acima ajuda a lembrar que é possível customizar conforme necessário.

3. Atualização do tamanho do nó (droplet)

ANTES:
size = "s-1vcpu-2gb"
DEPOIS:
size = "s-2vcpu-4gb"
Explicação: Foi aumentado o poder de processamento e memória dos nós do cluster, melhorando o desempenho da aplicação e suportando serviços como o Ingress NGINX com mais estabilidade.

4. Ajustes no script local-exec para exibir mensagens mais claras

ANTES:
echo "Kubeconfig salvo com sucesso!"
echo "Aguardando cluster ficar pronto..."
DEPOIS:
echo "Kubeconfig salvo. Aguardando cluster ficar pronto..."
Explicação: A mensagem foi simplificada para tornar o processo de criação do cluster mais direto no terminal. A funcionalidade permanece a mesma.

5. Adição do bloco provider "helm"

provider "helm" {
  kubernetes {
    host  = digitalocean_kubernetes_cluster.meu_cluster.endpoint
    token = digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].token
    cluster_ca_certificate = base64decode(
      digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].cluster_ca_certificate
    )
  }
}
Explicação: Esse bloco configura o provedor Helm para se conectar ao mesmo cluster Kubernetes criado via DigitalOcean. Ele permite que o Terraform aplique charts Helm (como o Ingress NGINX) de forma automatizada, sem depender de comandos manuais.

devops/main.tf


terraform {
  required_providers {
    digitalocean = {
      source  = "digitalocean/digitalocean"
      version = "~> 2.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "2.33.0"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "2.17.0"
    }
  }
}

provider "digitalocean" {
  token = var.do_token
}

# Criação do Cluster Kubernetes
resource "digitalocean_kubernetes_cluster" "meu_cluster" {
  name   = "meu-cluster"
  region = "nyc1"
  # Ajuste para a versão que desejar
  version = "1.32.2-do.0"

  node_pool {
    name       = "default-pool"
    size       = "s-2vcpu-4gb"
    node_count = 2
  }

  provisioner "local-exec" {
    command = <<-EOT
      echo "Iniciando kubeconfig..."
      doctl kubernetes cluster kubeconfig save meu-cluster
      echo "Kubeconfig salvo. Aguardando cluster ficar pronto..."
      sleep 45
      kubectl wait --for=condition=Ready nodes --all --timeout=300s
      echo "Cluster está pronto!"
    EOT
  }
}

provider "kubernetes" {
  host  = digitalocean_kubernetes_cluster.meu_cluster.endpoint
  token = digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].token
  cluster_ca_certificate = base64decode(
    digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].cluster_ca_certificate
  )
}

provider "helm" {
  kubernetes {
    host  = digitalocean_kubernetes_cluster.meu_cluster.endpoint
    token = digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].token
    cluster_ca_certificate = base64decode(
      digitalocean_kubernetes_cluster.meu_cluster.kube_config[0].cluster_ca_certificate
    )
  }
}

📁 devops/outputs.tf (ajustes para uso com Ingress)

🔹 Explicando as alterações destacadas em azul

1. Remoção do output do LoadBalancer do frontend

ANTES:
output "auth_ui_load_balancer_ip" {
  value       = try(kubernetes_service.auth_ui.status[0].load_balancer[0].ingress[0].ip, "Pending")
  description = "The external IP of the load balancer for the auth UI service (if available)"
}
DEPOIS: Vazio
<!-- Removido completamente -->
Explicação: Esse output foi removido porque o serviço do frontend deixou de usar LoadBalancer e passou a usar ClusterIP. Agora o acesso externo é feito via Ingress.

2. Adição do output do IP ou hostname do Ingress

output "ingress_controller_ip_or_hostname" {
  value = try(
    kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].ip,
    try(
      kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].hostname,
      "Pending - External IP/Hostname not yet assigned"
    )
  )
  description = "O IP ou hostname atribuído ao Ingress pela DigitalOcean"
}
Explicação: Esse output mostra qual IP ou hostname foi atribuído ao Ingress pela DigitalOcean. Ele é útil para saber como acessar o cluster externamente via navegador ou domínio.

3. Atualização do output application_url

ANTES:
value = try(
  "http://${kubernetes_service.auth_ui.status[0].load_balancer[0].ingress[0].ip}",
  "Pending - External IP not yet assigned"
)
DEPOIS:
value = try(
  "http://${kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].ip}",
  try(
    "http://${kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].hostname}",
    "Pending - External IP/Hostname not yet assigned"
  )
)
Explicação: Antes, o endereço externo da aplicação vinha direto do LoadBalancer do frontend. Agora ele vem do Ingress, que centraliza todo o tráfego da aplicação. Isso é necessário para ambientes que usam Ingress Controller.

devops/outputs.tf 


# Frontend UI Service
output "auth_ui_service_name" {
  value       = kubernetes_service.auth_ui.metadata[0].name
  description = "The name of the Kubernetes service for the auth UI"
}

output "auth_ui_service_type" {
  value       = kubernetes_service.auth_ui.spec[0].type
  description = "The type of the Kubernetes service for the auth UI"
}

# Backend API Service
output "auth_api_service_name" {
  value       = kubernetes_service.auth_api.metadata[0].name
  description = "The name of the Kubernetes service for the auth API"
}

output "auth_api_service_type" {
  value       = kubernetes_service.auth_api.spec[0].type
  description = "The type of the Kubernetes service for the auth API"
}

output "auth_api_cluster_ip" {
  value       = kubernetes_service.auth_api.spec[0].cluster_ip
  description = "The Cluster IP of the auth API service"
}

# MySQL Service
output "mysql_service_name" {
  value       = kubernetes_service.mysql_service.metadata[0].name
  description = "The name of the Kubernetes service for MySQL"
}

output "mysql_connection_string" {
  value       = "mysql://root:${var.mysql_root_password}@tcp(${kubernetes_service.mysql_service.metadata[0].name}:3306)/mysql"
  sensitive   = true
  description = "MySQL connection string (sensitive)"
}

# Exibir IP/Host do Ingress
output "ingress_controller_ip_or_hostname" {
  value = try(
    kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].ip,
    try(
      kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].hostname,
      "Pending - External IP/Hostname not yet assigned"
    )
  )
  description = "O IP ou hostname atribuído ao Ingress pela DigitalOcean"
}

# URL final da aplicação via Ingress
output "application_url" {
  value = try(
    "http://${kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].ip}",
    try(
      "http://${kubernetes_ingress_v1.app_ingress.status[0].load_balancer[0].ingress[0].hostname}",
      "Pending - External IP/Hostname not yet assigned"
    )
  )
  description = "URL para acessar a aplicação (frontend) via Ingress"
}

# URL interna para a API
output "api_internal_url" {
  value = "http://${kubernetes_service.auth_api.metadata[0].name}:3000/api"
  description = "URL interna para a API"
}

📁 devops/variables.tf (ajuste fino na URL da API)

🔹 Explicando a alteração destacada em azul

1. Ajuste no valor padrão da variável react_app_api_url

ANTES:
default = "/api"
DEPOIS:
default = "/api/"
Explicação: Esse pequeno ajuste evita erros de concatenação de URLs no frontend (por exemplo: /api/forgot funciona, mas /apiforgot não). O uso da barra final garante que o caminho base sempre esteja correto ao montar rotas como ${REACT_APP_API_URL}forgot, register, etc.

devops/variables.tf


variable "do_token" {
  type        = string
  sensitive   = true
  description = "Token de acesso para a API da DigitalOcean"
}

variable "gmail_username" {
  type        = string
  description = "Endereço do Gmail para enviar emails"
}

variable "gmail_password" {
  type        = string
  sensitive   = true
  description = "Senha do Gmail ou Senha de App"
}

variable "mysql_root_password" {
  description = "Senha do root para o MySQL"
  type        = string
}

variable "react_app_api_url" {
  description = "URL para a API do auth-api"
  type        = string
  default     = "/api/"
}

📌 Observação importante sobre as imagens Docker utilizadas

Durante este tutorial, utilizei a imagem atualizada do frontend: toticavalcanti/auth-ui:v1.1. Essa versão inclui uma correção importante onde a requisição await axios.post('forgot', ...) foi atualizada para await axios.post('/forgot', ...), garantindo que o caminho funcione corretamente com o Ingress e evite falhas no envio do formulário de recuperação de senha. Caso você deseje pular a etapa de construir as imagens por conta própria, pode utilizar diretamente as minhas imagens públicas que subi no Docker Hub:
  • toticavalcanti/fiber-auth-api:v1.0 – Backend (Go + Fiber)
  • toticavalcanti/auth-ui:v1.1 – Frontend (React com ajuste no path do forgot)
No entanto, se preferir criar suas próprias imagens Docker personalizadas, basta clonar os repositórios do projeto (ou baixá-los como ZIP), fazer as modificações desejadas, e então construir suas próprias imagens com os comandos docker build e docker push para o seu repositório do Docker Hub, conforme explicado em detalhes na aula passada, a aula 13. Fica a seu critério: usar as minhas imagens já prontas para agilizar o processo, ou customizar com liberdade total.

🧪 Teste final

Primeira coisa a fazer é logar na cloud que você vai usar, nesse caso, a Digital Ocean:
doctl auth init --access-token seu_token_da_digitalocean
Entre na pasta devops e rode os comandos do terraform:
cd devops // entrar na pasta devops
doctl kubernetes options versions
terraform init //or terraform init -upgrade
terraform apply -auto-approve
Conecte-se ao seu Cluster, pegando suas credenciais:

doctl kubernetes cluster kubeconfig save meu-cluster
Este comando salva automaticamente a configuração do cluster no seu arquivo kubeconfig padrão (geralmente localizado em ~/.kube/config). Execute os comandos:
kubectl get svc ingress-nginx-controller
Acesse no navegador: http://IP-DO-INGRESS / → frontend /api → backend (ex: /api/user)

✅ Conclusão

Agora nossa aplicação está com uma arquitetura muito mais robusta, com:
  • Tráfego centralizado via Ingress Controller
  • Redução de custos com IPs públicos
  • Pronto para suportar HTTPS com cert-manager (próxima aula!)
📦 Os arquivos desta aula estão disponíveis no repositório

Por essa aula é só, até a próxima. \o/