feat(infra): add full k8s stack mirroring docker-compose setup
Terraform now deploys a complete k8s environment: - cert-manager with Let's Encrypt (staging + prod issuers) - Redis deployment with persistent storage - App deployment (2 replicas, rolling updates) - Traefik ingress with SSL, HSTS, HTTP→HTTPS redirect Ready for switchover by forwarding ports 80/443 to k3s VM. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
31e0c2bfee
commit
c16b70090f
|
|
@ -40,3 +40,23 @@ provider "registry.terraform.io/hashicorp/kubernetes" {
|
||||||
"zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db",
|
"zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
provider "registry.terraform.io/hashicorp/null" {
|
||||||
|
version = "3.2.4"
|
||||||
|
constraints = "~> 3.2"
|
||||||
|
hashes = [
|
||||||
|
"h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=",
|
||||||
|
"zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2",
|
||||||
|
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||||
|
"zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43",
|
||||||
|
"zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a",
|
||||||
|
"zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991",
|
||||||
|
"zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f",
|
||||||
|
"zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e",
|
||||||
|
"zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615",
|
||||||
|
"zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442",
|
||||||
|
"zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5",
|
||||||
|
"zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f",
|
||||||
|
"zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,260 @@
|
||||||
|
# Main application deployment
|
||||||
|
|
||||||
|
resource "kubernetes_secret" "app_env" {
|
||||||
|
metadata {
|
||||||
|
name = "app-env"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
# Add sensitive env vars here
|
||||||
|
# DATABASE_URL = var.database_url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_config_map" "app_config" {
|
||||||
|
metadata {
|
||||||
|
name = "app-config"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
NODE_ENV = "production"
|
||||||
|
PORT = "3000"
|
||||||
|
HOSTNAME = "0.0.0.0"
|
||||||
|
NEXT_TELEMETRY_DISABLED = "1"
|
||||||
|
REDIS_URL = "redis://redis:6379"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "app" {
|
||||||
|
metadata {
|
||||||
|
name = "abaci-app"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "abaci-app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = var.app_replicas
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "abaci-app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
strategy {
|
||||||
|
type = "RollingUpdate"
|
||||||
|
rolling_update {
|
||||||
|
max_surge = 1
|
||||||
|
max_unavailable = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "abaci-app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "app"
|
||||||
|
image = var.app_image
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
env_from {
|
||||||
|
config_map_ref {
|
||||||
|
name = kubernetes_config_map.app_config.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env_from {
|
||||||
|
secret_ref {
|
||||||
|
name = kubernetes_secret.app_env.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "256Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "512Mi"
|
||||||
|
cpu = "1000m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/api/health"
|
||||||
|
port = 3000
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 30
|
||||||
|
period_seconds = 10
|
||||||
|
timeout_seconds = 5
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
readiness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/api/health"
|
||||||
|
port = 3000
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
timeout_seconds = 3
|
||||||
|
failure_threshold = 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [kubernetes_deployment.redis]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "app" {
|
||||||
|
metadata {
|
||||||
|
name = "abaci-app"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = {
|
||||||
|
app = "abaci-app"
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
port = 80
|
||||||
|
target_port = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "ClusterIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ingress with SSL via cert-manager
|
||||||
|
resource "kubernetes_ingress_v1" "app" {
|
||||||
|
metadata {
|
||||||
|
name = "abaci-app"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
annotations = {
|
||||||
|
"cert-manager.io/cluster-issuer" = var.use_staging_certs ? "letsencrypt-staging" : "letsencrypt-prod"
|
||||||
|
"traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
|
||||||
|
# HSTS headers
|
||||||
|
"traefik.ingress.kubernetes.io/router.middlewares" = "${kubernetes_namespace.abaci.metadata[0].name}-hsts@kubernetescrd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
ingress_class_name = "traefik"
|
||||||
|
|
||||||
|
tls {
|
||||||
|
hosts = [var.app_domain]
|
||||||
|
secret_name = "abaci-tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
host = var.app_domain
|
||||||
|
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
path_type = "Prefix"
|
||||||
|
|
||||||
|
backend {
|
||||||
|
service {
|
||||||
|
name = kubernetes_service.app.metadata[0].name
|
||||||
|
port {
|
||||||
|
number = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [null_resource.cert_manager_issuers]
|
||||||
|
}
|
||||||
|
|
||||||
|
# HSTS middleware
|
||||||
|
resource "kubernetes_manifest" "hsts_middleware" {
|
||||||
|
manifest = {
|
||||||
|
apiVersion = "traefik.io/v1alpha1"
|
||||||
|
kind = "Middleware"
|
||||||
|
metadata = {
|
||||||
|
name = "hsts"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
spec = {
|
||||||
|
headers = {
|
||||||
|
stsSeconds = 63072000
|
||||||
|
stsIncludeSubdomains = true
|
||||||
|
stsPreload = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP to HTTPS redirect
|
||||||
|
resource "kubernetes_ingress_v1" "app_http_redirect" {
|
||||||
|
metadata {
|
||||||
|
name = "abaci-app-http-redirect"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
annotations = {
|
||||||
|
"traefik.ingress.kubernetes.io/router.entrypoints" = "web"
|
||||||
|
"traefik.ingress.kubernetes.io/router.middlewares" = "${kubernetes_namespace.abaci.metadata[0].name}-redirect-https@kubernetescrd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
ingress_class_name = "traefik"
|
||||||
|
|
||||||
|
rule {
|
||||||
|
host = var.app_domain
|
||||||
|
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
path_type = "Prefix"
|
||||||
|
|
||||||
|
backend {
|
||||||
|
service {
|
||||||
|
name = kubernetes_service.app.metadata[0].name
|
||||||
|
port {
|
||||||
|
number = 80
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Redirect middleware
|
||||||
|
resource "kubernetes_manifest" "redirect_https_middleware" {
|
||||||
|
manifest = {
|
||||||
|
apiVersion = "traefik.io/v1alpha1"
|
||||||
|
kind = "Middleware"
|
||||||
|
metadata = {
|
||||||
|
name = "redirect-https"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
spec = {
|
||||||
|
redirectScheme = {
|
||||||
|
scheme = "https"
|
||||||
|
permanent = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
# cert-manager for automatic Let's Encrypt SSL certificates
|
||||||
|
|
||||||
|
resource "helm_release" "cert_manager" {
|
||||||
|
name = "cert-manager"
|
||||||
|
repository = "https://charts.jetstack.io"
|
||||||
|
chart = "cert-manager"
|
||||||
|
namespace = "cert-manager"
|
||||||
|
create_namespace = true
|
||||||
|
version = "v1.14.4"
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "installCRDs"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "global.leaderElection.namespace"
|
||||||
|
value = "cert-manager"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ClusterIssuers need to be applied after cert-manager CRDs are installed
|
||||||
|
# Using local-exec since kubernetes_manifest validates CRDs at plan time
|
||||||
|
|
||||||
|
resource "null_resource" "cert_manager_issuers" {
|
||||||
|
depends_on = [helm_release.cert_manager]
|
||||||
|
|
||||||
|
provisioner "local-exec" {
|
||||||
|
command = <<-EOT
|
||||||
|
export KUBECONFIG=${pathexpand(var.kubeconfig_path)}
|
||||||
|
|
||||||
|
# Wait for cert-manager webhook to be ready
|
||||||
|
kubectl wait --for=condition=Available deployment/cert-manager-webhook -n cert-manager --timeout=120s
|
||||||
|
|
||||||
|
# Apply ClusterIssuers
|
||||||
|
cat <<EOF | kubectl apply -f -
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-prod
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-v02.api.letsencrypt.org/directory
|
||||||
|
email: ${var.letsencrypt_email}
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-prod-key
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
|
---
|
||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: letsencrypt-staging
|
||||||
|
spec:
|
||||||
|
acme:
|
||||||
|
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
||||||
|
email: ${var.letsencrypt_email}
|
||||||
|
privateKeySecretRef:
|
||||||
|
name: letsencrypt-staging-key
|
||||||
|
solvers:
|
||||||
|
- http01:
|
||||||
|
ingress:
|
||||||
|
class: traefik
|
||||||
|
EOF
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
||||||
|
triggers = {
|
||||||
|
email = var.letsencrypt_email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,23 +18,3 @@ resource "kubernetes_namespace" "abaci" {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Example: Redis deployment (optional - can use this instead of Docker Redis)
|
|
||||||
# Uncomment when ready to migrate Redis to k3s
|
|
||||||
#
|
|
||||||
# resource "helm_release" "redis" {
|
|
||||||
# name = "redis"
|
|
||||||
# repository = "https://charts.bitnami.com/bitnami"
|
|
||||||
# chart = "redis"
|
|
||||||
# namespace = kubernetes_namespace.abaci.metadata[0].name
|
|
||||||
#
|
|
||||||
# set {
|
|
||||||
# name = "architecture"
|
|
||||||
# value = "standalone"
|
|
||||||
# }
|
|
||||||
#
|
|
||||||
# set {
|
|
||||||
# name = "auth.enabled"
|
|
||||||
# value = "false"
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,49 @@ output "namespace" {
|
||||||
value = kubernetes_namespace.abaci.metadata[0].name
|
value = kubernetes_namespace.abaci.metadata[0].name
|
||||||
}
|
}
|
||||||
|
|
||||||
output "cluster_info" {
|
output "app_service" {
|
||||||
description = "k3s cluster information"
|
description = "App service details"
|
||||||
value = {
|
value = {
|
||||||
kubeconfig = var.kubeconfig_path
|
name = kubernetes_service.app.metadata[0].name
|
||||||
namespace = var.namespace
|
namespace = kubernetes_service.app.metadata[0].namespace
|
||||||
|
port = 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
output "redis_service" {
|
||||||
|
description = "Redis service details"
|
||||||
|
value = {
|
||||||
|
name = kubernetes_service.redis.metadata[0].name
|
||||||
|
namespace = kubernetes_service.redis.metadata[0].namespace
|
||||||
|
url = "redis://redis.${kubernetes_namespace.abaci.metadata[0].name}.svc.cluster.local:6379"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "ingress_info" {
|
||||||
|
description = "Ingress information"
|
||||||
|
value = {
|
||||||
|
domain = var.app_domain
|
||||||
|
tls_secret = "abaci-tls"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output "switchover_checklist" {
|
||||||
|
description = "Steps to switch traffic from Docker to k8s"
|
||||||
|
value = <<-EOT
|
||||||
|
To switch traffic from Docker Compose to k8s:
|
||||||
|
|
||||||
|
1. Ensure k8s pods are healthy:
|
||||||
|
kubectl get pods -n abaci
|
||||||
|
|
||||||
|
2. Update port forwarding on router:
|
||||||
|
- Forward ports 80 and 443 to k3s-node VM (192.168.86.37)
|
||||||
|
- Or update DNS to point to VM's public IP
|
||||||
|
|
||||||
|
3. Verify SSL certificate is issued:
|
||||||
|
kubectl get certificate -n abaci
|
||||||
|
|
||||||
|
4. Test the site via k8s
|
||||||
|
|
||||||
|
5. To rollback: revert port forwarding to NAS
|
||||||
|
EOT
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
# Redis deployment for session storage and Socket.IO
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "redis" {
|
||||||
|
metadata {
|
||||||
|
name = "redis"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "redis"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "redis"
|
||||||
|
image = "redis:7-alpine"
|
||||||
|
|
||||||
|
args = ["redis-server", "--appendonly", "yes"]
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 6379
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "128Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "256Mi"
|
||||||
|
cpu = "500m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "redis-data"
|
||||||
|
mount_path = "/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
exec {
|
||||||
|
command = ["redis-cli", "ping"]
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 5
|
||||||
|
period_seconds = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
readiness_probe {
|
||||||
|
exec {
|
||||||
|
command = ["redis-cli", "ping"]
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 5
|
||||||
|
period_seconds = 5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "redis-data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.redis_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_persistent_volume_claim" "redis_data" {
|
||||||
|
metadata {
|
||||||
|
name = "redis-data"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
access_modes = ["ReadWriteOnce"]
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
storage = "1Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
storage_class_name = "local-path" # k3s default storage class
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_until_bound = false # local-path uses WaitForFirstConsumer
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "redis" {
|
||||||
|
metadata {
|
||||||
|
name = "redis"
|
||||||
|
namespace = kubernetes_namespace.abaci.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = {
|
||||||
|
app = "redis"
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
port = 6379
|
||||||
|
target_port = 6379
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "ClusterIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Copy this to terraform.tfvars and fill in values
|
||||||
|
|
||||||
|
# Required
|
||||||
|
letsencrypt_email = "your-email@example.com"
|
||||||
|
|
||||||
|
# Optional overrides
|
||||||
|
# app_domain = "abaci.one"
|
||||||
|
# app_image = "ghcr.io/antialias/soroban-abacus-flashcards:latest"
|
||||||
|
# app_replicas = 2
|
||||||
|
# use_staging_certs = true # Set to true when testing to avoid rate limits
|
||||||
|
|
@ -9,3 +9,32 @@ variable "namespace" {
|
||||||
type = string
|
type = string
|
||||||
default = "abaci"
|
default = "abaci"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "app_domain" {
|
||||||
|
description = "Domain name for the application"
|
||||||
|
type = string
|
||||||
|
default = "abaci.one"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_image" {
|
||||||
|
description = "Docker image for the application"
|
||||||
|
type = string
|
||||||
|
default = "ghcr.io/antialias/soroban-abacus-flashcards:latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "app_replicas" {
|
||||||
|
description = "Number of app replicas"
|
||||||
|
type = number
|
||||||
|
default = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "letsencrypt_email" {
|
||||||
|
description = "Email for Let's Encrypt certificate notifications"
|
||||||
|
type = string
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "use_staging_certs" {
|
||||||
|
description = "Use Let's Encrypt staging (for testing, avoids rate limits)"
|
||||||
|
type = bool
|
||||||
|
default = false
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,9 @@ terraform {
|
||||||
source = "hashicorp/helm"
|
source = "hashicorp/helm"
|
||||||
version = "~> 2.12"
|
version = "~> 2.12"
|
||||||
}
|
}
|
||||||
|
null = {
|
||||||
|
source = "hashicorp/null"
|
||||||
|
version = "~> 3.2"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue