feat: add kubernetes CD
This commit is contained in:
@@ -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
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user