feat: add kubernetes CD

This commit is contained in:
2026-04-05 15:58:28 -03:00
committed by GitHub
parent 18f21d310d
commit a67c0f1227
2 changed files with 530 additions and 0 deletions
+160
View File
@@ -0,0 +1,160 @@
name: Deploy to K3s
on:
workflow_dispatch:
inputs:
backend_image:
description: "Backend image tag (full image reference)"
required: false
frontend_image:
description: "Frontend image tag (full image reference)"
required: false
apply:
description: "Apply changes after plan"
required: false
default: "true"
push:
branches: [ main ]
workflow_run:
workflows: [ "Backend Build & Push", "Frontend Build & Push" ]
types: [ completed ]
jobs:
terraform-plan:
if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Terraform
uses: hashicorp/setup-terraform@v4
- name: Setup kubectl
uses: azure/setup-kubectl@v5
- name: Setup Kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Validate cluster access
run: |
kubectl cluster-info
kubectl get namespace gif-mood
- name: Determine deployment values
id: deploy-vars
run: |
backend_image='${{ github.event.inputs.backend_image }}'
frontend_image='${{ github.event.inputs.frontend_image }}'
latest_backend_tag="$(git tag --list 'api-v*' --sort=-v:refname | tail -n1)"
latest_frontend_tag="$(git tag --list 'app-v*' --sort=-v:refname | tail -n1)"
if [ -z "$latest_backend_tag" ]; then
echo "No backend tag found matching api-v*" >&2
exit 1
fi
if [ -z "$latest_frontend_tag" ]; then
echo "No frontend tag found matching app-v*" >&2
exit 1
fi
if [ -z "$backend_image" ]; then
backend_image="ghcr.io/${{ github.repository }}/backend:$latest_backend_tag"
fi
if [ -z "$frontend_image" ]; then
frontend_image="ghcr.io/${{ github.repository }}/frontend:$latest_frontend_tag"
fi
echo "Resolved backend_image=$backend_image"
echo "Resolved migrations_image=$migrations_image"
echo "Resolved frontend_image=$frontend_image"
echo "backend_image=$backend_image" >> "$GITHUB_OUTPUT"
echo "migrations_image=$migrations_image" >> "$GITHUB_OUTPUT"
echo "frontend_image=$frontend_image" >> "$GITHUB_OUTPUT"
- name: Terraform Fmt -check -diff
working-directory: terraform
run: terraform fmt -check -diff
- name: Terraform Init
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform Validate
working-directory: terraform
run: terraform validate
- name: Terraform Plan
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: |
terraform plan -out=tfplan \
-var="db_user=${{ secrets.DB_USER }}" \
-var="db_password=${{ secrets.DB_PASSWORD }}" \
-var="db_name=${{ secrets.DB_NAME }}" \
-var="security_key=${{ secrets.JWT_SECURITY_KEY }}" \
-var="mailgun_api_key=${{ secrets.MAILGUN_API_KEY }}" \
-var="backend_image=${{ steps.deploy-vars.outputs.backend_image }}" \
-var="frontend_image=${{ steps.deploy-vars.outputs.frontend_image }}"
- name: Upload plan artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: terraform/tfplan
terraform-apply:
runs-on: ubuntu-latest
needs: terraform-plan
if: ${{ github.event_name == 'push' || github.event_name == 'workflow_run' || github.event.inputs.apply == 'true' }}
environment:
name: production
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Download plan artifact
uses: actions/download-artifact@v4
with:
name: tfplan
path: terraform
- name: Setup Kubeconfig
run: |
mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
- name: Terraform Init
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: terraform init
- name: Terraform Apply
working-directory: terraform
env:
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
run: timeout 1m terraform apply tfplan
+370
View File
@@ -0,0 +1,370 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.0.0"
}
}
backend "s3" {
bucket = "tasknote"
key = "kubernetes/terraform.tfstate"
region = "auto"
endpoints = { s3 = "https://d17eb09b6bce2f90e16e800bb2a6baf9.r2.cloudflarestorage.com" }
skip_credentials_validation = true
skip_region_validation = true
skip_requesting_account_id = true
skip_metadata_api_check = true
skip_s3_checksum = true
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
}
variable "db_user" {
type = string
sensitive = true
}
variable "db_password" {
type = string
sensitive = true
}
variable "db_name" {
type = string
sensitive = true
}
variable "security_key" {
type = string
sensitive = true
}
variable "mailgun_apikey" {
type = string
sensitive = true
}
variable "cors_allowed_origins" {
type = string
default = "https://tasknote.darkroasted.vps-kinghost.net"
}
variable "root_log_level" {
type = string
default = "INFO"
}
variable "backend_image" {
type = string
default = "pull ghcr.io/rmcampos/tasknote/api:15"
}
variable "frontend_image" {
type = string
default = "ghcr.io/rmcampos/tasknote/app:app-v2026.03.17.18"
}
resource "kubernetes_namespace_v1" "tasknote" {
metadata {
name = "tasknote"
}
}
resource "kubernetes_secret_v1" "tasknote_secrets" {
metadata {
name = "tasknote-secrets"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
data = {
postgres_user = var.db_user
postgres_password = var.db_password
postgres_db = var.db_name
security_key = var.security_key
mailgun_apikey = var.mailgun_apikey
}
}
resource "kubernetes_persistent_volume_claim_v1" "tasknote_db_data" {
metadata {
name = "postgres-data-pvc"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
access_modes = ["ReadWriteOnce"]
resources {
requests = {
storage = "1Gi"
}
}
}
}
resource "kubernetes_deployment_v1" "tasknote_db" {
metadata {
name = "tasknote-db"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-db" } }
template {
metadata { labels = { app = "tasknote-db" } }
spec {
container {
image = "postgres:15.8-bookworm"
name = "postgres"
volume_mount {
name = "postgres-storage"
mount_path = "/var/lib/postgresql/data"
}
env {
name = "POSTGRES_USER"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "postgres_user"
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "postgres_password"
}
}
}
env {
name = "POSTGRES_DB"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "postgres_db"
}
}
}
port { container_port = 5432 }
}
volume {
name = "postgres-storage"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.tasknote_db_data.metadata[0].name
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_db_svc" {
metadata {
name = "tasknote-db-svc"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
selector = { app = "tasknote-db" }
port { port = 5432 }
type = "ClusterIP"
}
}
resource "kubernetes_deployment_v1" "tasknote_backend" {
metadata {
name = "tasknote-backend"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-backend" } }
template {
metadata { labels = { app = "tasknote-backend" } }
spec {
container {
image = var.backend_image
name = "backend"
env {
name = "POSTGRES_DB"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "postgres_db"
}
}
}
env {
name = "POSTGRES_HOST"
value = "tasknote-db-svc"
}
env {
name = "POSTGRES_USER"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "postgres_user"
}
}
}
env {
name = "POSTGRES_PASSWORD"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "postgres_password"
}
}
}
env {
name = "POSTGRES_PORT"
value = "5432"
}
env {
name = "CORS_ALLOWED_ORIGINS"
value = var.cors_allowed_origins
}
env {
name = "SERVER_SERVLET_CONTEXT_PATH"
value = "/"
}
env {
name = "ROOT_LOG_LEVEL"
value = var.root_log_level
}
env {
name = "SECURITY_KEY"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "security_key"
}
}
}
env {
name = "TARGET_ENV"
value = "production"
}
env {
name = "MAILGUN_APIKEY"
value_from {
secret_key_ref {
name = kubernetes_secret_v1.tasknote_secrets.metadata[0].name
key = "mailgun_apikey"
}
}
}
resources {
limits = { memory = "256Mi", cpu = "500m" }
requests = { memory = "256Mi", cpu = "250m" }
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_backend_svc" {
metadata {
name = "tasknote-backend-svc"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
selector = { app = "tasknote-backend" }
port {
port = 8585
target_port = 8585
}
}
}
resource "kubernetes_deployment_v1" "tasknote_frontend" {
metadata {
name = "tasknote-frontend"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
replicas = 1
selector { match_labels = { app = "tasknote-app" } }
template {
metadata { labels = { app = "tasknote-app" } }
spec {
container {
image = var.frontend_image
name = "frontend"
port { container_port = 5000 }
env {
name = "VITE_BACKEND_SERVER"
value = "https://tasknoteapi.darkroasted.vps-kinghost.net"
}
}
}
}
}
}
resource "kubernetes_service_v1" "tasknote_frontend_svc" {
metadata {
name = "tasknote-frontend-svc"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
}
spec {
selector = { app = "tasknote-app" }
port {
port = 5000
target_port = 5000
}
type = "ClusterIP"
}
}
# Unified Ingress for App and API
resource "kubernetes_ingress_v1" "tasknote_ingress" {
metadata {
name = "tasknote-ingress"
namespace = kubernetes_namespace_v1.tasknote.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "traefik"
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
}
}
spec {
tls {
hosts = ["tasknote.darkroasted.vps-kinghost.net", "tasknoteapi.darkroasted.vps-kinghost.net"]
secret_name = "tasknote-tls-certs"
}
rule {
host = "tasknote.darkroasted.vps-kinghost.net"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.tasknote_frontend_svc.metadata[0].name
port { number = 5000 }
}
}
}
}
}
rule {
host = "tasknoteapi.darkroasted.vps-kinghost.net"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service_v1.tasknote_backend_svc.metadata[0].name
port { number = 8585 }
}
}
}
}
}
}
}