shuttles #2

Open
thomas wants to merge 27 commits from shuttles into main
32 changed files with 2715 additions and 0 deletions
Showing only changes of commit a114a5002e - Show all commits

View File

@ -0,0 +1,82 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
variable "postgres_password" {
type = string
sensitive = true
}
module "psql_db" {
source = "../postgres/tenant"
tenant_name = "affine"
admin_username = "postgres"
admin_password = var.postgres_password
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
}
module "affine" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "affine"
image = "ghcr.io/toeverything/affine:stable"
ports = ["3020:3010"]
volumes = [
"/opt/storage/data/affine/storage:/root/.affine/storage:Z",
"/opt/storage/data/affine/config:/root/.affine/config:Z"
]
environment = {
DATABASE_URL = "postgresql://affine:${module.psql_db.password}@systemd-postgres:5432/affine"
REDIS_SERVER_HOST = "systemd-valkey"
AFFINE_INDEXER_ENABLED = "false"
}
command = [
"sh",
"-c",
"\"node ./scripts/self-host-predeploy.js && node ./dist/main.js\""
]
healthcmd = "curl -f http://localhost:3010 || exit 1"
haproxy_services = [
{
name = "affine"
domain = "affine.${var.server_domain}"
port = "3020"
host = "127.0.0.1"
tls = false
}
]
depends_on_services = ["postgres.service", "valkey.service"]
}
output "app_urls" {
value = module.affine.app_urls
}
output "installed" {
value = true
depends_on = [module.affine.installed]
}

View File

@ -0,0 +1,60 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "airsonic-advanced" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "airsonic-advanced"
image = "lscr.io/linuxserver/airsonic-advanced:latest"
ports = ["4040:4040"]
volumes = [
"/opt/storage/data/airsonic-advanced:/config:Z",
"/opt/storage/media/music:/music:Z",
]
environment = {
PUID="1001"
PGID="1001"
TZ="Etc/UTC"
#JAVA_OPTS="-Dserver.address=127.0.0.1"
}
haproxy_services = [
{
name = "airsonic-advanced"
domain = "airsonic.${var.server_domain}"
port = "4040"
host = "127.0.0.1"
tls = false
},
]
}
output "app_urls" {
value = module.airsonic-advanced.app_urls
}
output "installed" {
value = true
depends_on = [module.airsonic-advanced.installed]
}

View File

@ -0,0 +1,59 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "beets" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "beets"
image = "lscr.io/linuxserver/beets:latest"
ports = ["8337:8337"]
volumes = [
"/opt/storage/data/beets/config:/config:Z",
"/opt/storage/media/music:/music:z",
"/opt/storage/media/downloads:/downloads:z"
]
environment = {
PUID = "1001"
PGID = "1001"
TZ = "Europe/Amsterdam"
}
haproxy_services = [
{
name = "beets"
domain = "beets.${var.server_domain}"
port = "8337"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.beets.app_urls
}
output "installed" {
value = true
depends_on = [module.beets.installed]
}

View File

@ -0,0 +1,98 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "deeptutor" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "deeptutor"
image = "ghcr.io/hkuds/deeptutor:latest"
ports = ["8001:8001", "3782:3782"]
volumes = [
"/opt/storage/data/deeptutor/config:/app/config:Z",
"/opt/storage/data/deeptutor/user:/app/data/user:Z",
"/opt/storage/data/deeptutor/knowledge_bases:/app/data/knowledge_bases:Z",
]
environment = {
# LLM Configuration (Required - set these in terraform.tfvars)
LLM_BINDING = "openai"
LLM_MODEL = "deepseek/deepseek-v3.2"
LLM_BINDING_API_KEY = "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"
LLM_BINDING_HOST = "https://openrouter.ai/api/v1"
DISABLE_SSL_VERIFY = "false"
# Embedding Configuration (Required for Knowledge Base)
EMBEDDING_BINDING = "openai"
EMBEDDING_MODEL = "text-embedding-3-large"
EMBEDDING_BINDING_API_KEY = "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"
EMBEDDING_BINDING_HOST = "https://openrouter.ai/api/v1"
EMBEDDING_DIM = "3072"
EMBEDDING_MAX_TOKENS = "8192"
# TTS Configuration (Optional)
TTS_MODEL = ""
TTS_API_KEY = ""
TTS_URL = ""
TTS_VOICE = "alloy"
# Web Search Configuration (Optional)
PERPLEXITY_API_KEY = ""
# Logging Configuration
RAG_TOOL_MODULE_LOG_LEVEL = "INFO"
# Service Ports
BACKEND_PORT = "8001"
FRONTEND_PORT = "3782"
# External API URL (for browser access if different from localhost)
NEXT_PUBLIC_API_BASE_EXTERNAL = "https://deeptutor-api.${var.server_domain}"
}
healthcmd = "curl -f http://localhost:8001/ || exit 1"
haproxy_services = [
{
name = "deeptutor-frontend"
domain = "deeptutor.${var.server_domain}"
port = "3782"
host = "127.0.0.1"
tls = false
},
{
name = "deeptutor-api"
domain = "deeptutor-api.${var.server_domain}"
port = "8001"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.deeptutor.app_urls
}
output "installed" {
value = true
depends_on = [module.deeptutor.installed]
}

View File

@ -0,0 +1,59 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
resource "random_password" "username" {
length = 32
special = false
}
resource "random_password" "password" {
length = 32
special = false
}
module "redis" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "documentdb"
image = "ghcr.io/documentdb/documentdb/documentdb-local:latest"
ports = ["27017:27017"]
volumes = ["/opt/storage/data/documentdb:/data:Z"]
environment = {
USERNAME = random_password.username.result
PASSWORD = random_password.password.result
}
}
output "app_urls" {
value = module.redis.app_urls
}
output "installed" {
value = true
depends_on = [module.redis.installed]
}
output "username" {
value = random_password.username.result
sensitive = true
}
output "password" {
value = random_password.password.result
sensitive = true
}

View File

@ -0,0 +1,117 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
variable "postgres_password" {
type = string
sensitive = true
}
resource "random_password" "forgejo_secret_key" {
length = 64
special = false
}
resource "random_password" "forgejo_internal_token" {
length = 64
special = false
}
resource "random_password" "forgejo_jwt_secret" {
length = 32
special = false
}
module "psql_db" {
source = "../postgres/tenant"
tenant_name = "forgejo"
admin_username = "postgres"
admin_password = var.postgres_password
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
}
module "forgejo" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "forgejo"
image = "codeberg.org/forgejo/forgejo:13.0"
ports = ["3000:3000", "2222:22"]
volumes = [
"/opt/storage/data/forgejo:/data:Z",
"/etc/localtime:/etc/localtime:ro"
]
environment = {
USER_UID = "1000"
USER_GID = "1000"
FORGEJO__database__DB_TYPE = "postgres"
FORGEJO__database__HOST = "systemd-postgres:5432"
FORGEJO__database__NAME = "forgejo"
FORGEJO__database__USER = module.psql_db.username
FORGEJO__database__PASSWD = module.psql_db.password
FORGEJO__server__DOMAIN = "forgejo.${var.server_domain}"
FORGEJO__server__ROOT_URL = "https://forgejo.${var.server_domain}/"
FORGEJO__server__SSH_DOMAIN = "forgejo.${var.server_domain}"
FORGEJO__server__SSH_PORT = "2222"
FORGEJO__security__INSTALL_LOCK = "true"
FORGEJO__security__SECRET_KEY = random_password.forgejo_secret_key.result
FORGEJO__security__INTERNAL_TOKEN = random_password.forgejo_internal_token.result
FORGEJO__oauth2__JWT_SECRET = random_password.forgejo_jwt_secret.result
}
healthcmd = "curl -f http://localhost:3000/api/v1/version || exit 1"
haproxy_services = [
{
name = "forgejo"
domain = "forgejo.${var.server_domain}"
port = "3000"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.forgejo.app_urls
}
output "installed" {
value = true
depends_on = [module.forgejo.installed]
}
output "secret_key" {
value = random_password.forgejo_secret_key.result
sensitive = true
}
output "internal_token" {
value = random_password.forgejo_internal_token.result
sensitive = true
}
output "jwt_secret" {
value = random_password.forgejo_jwt_secret.result
sensitive = true
}

View File

@ -0,0 +1,60 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "gonic" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "gonic"
image = "docker.io/sentriz/gonic:latest"
ports = ["4747:80"]
volumes = [
"/opt/storage/data/gonic:/config:Z",
"/opt/storage/media/music:/music:Z",
"/opt/storage/media/podcasts:/podcasts:Z",
"/opt/storage/media/playlists:/playlists:Z",
"/opt/storage/cache/gonic:/cache:Z",
]
environment = {
TZ="Etc/UTC"
}
haproxy_services = [
{
name = "gonic"
domain = "gonic.${var.server_domain}"
port = "4747"
host = "127.0.0.1"
tls = false
},
]
}
output "app_urls" {
value = module.gonic.app_urls
}
output "installed" {
value = true
depends_on = [module.gonic.installed]
}

View File

@ -0,0 +1,59 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "mopidy" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "mopidy"
image = "docker.io/ivdata/mopidy"
ports = ["6600:6600", "6680:6680"]
volumes = [
"/opt/storage/data/mopidy:/var/lib/mopidy:Z",
"/opt/storage/data/mopidy/mopidy.conf:/home/mopidy/.config/mopidy/mopidy.conf:Z",
"/opt/storage/media/music:/media/music:ro,Z",
]
# environment = {
# SPOTIFY_CLIENT_ID="ff4a141c-cb52-4569-9848-c0468da66bd1"
# SPOTIFY_CLIENT_SECRET="VVXVtoiY8DanFLl9BnpHvLygXCchboHoEewoALTHIaA="
# }
haproxy_services = [
{
name = "mopidy"
domain = "mopidy.${var.server_domain}"
port = "6680"
host = "127.0.0.1"
tls = false
},
]
}
output "app_urls" {
value = module.mopidy.app_urls
}
output "installed" {
value = true
depends_on = [module.mopidy.installed]
}

View File

@ -0,0 +1,58 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "navidrome" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "navidrome"
image = "docker.io/deluan/navidrome:latest"
ports = ["4533:4533"]
volumes = [
"/opt/storage/data/navidrome:/data:Z",
"/opt/storage/media/music:/music:ro,Z",
]
environment = {
ND_LOGLEVEL="info"
ND_ENABLEINSIGHTSCOLLECTOR="false"
}
haproxy_services = [
{
name = "navidrome"
domain = "navidrome.${var.server_domain}"
port = "4533"
host = "127.0.0.1"
tls = false
},
]
}
output "app_urls" {
value = module.navidrome.app_urls
}
output "installed" {
value = true
depends_on = [module.navidrome.installed]
}

View File

@ -0,0 +1,111 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
# Generate random keys for esign
resource "random_password" "app_id" {
length = 12
special = false
}
resource "random_password" "master_key" {
length = 32
special = true
}
module "esign" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "esign"
image = "docker.io/esign/esignserver:main"
ports = ["8180:8080"]
volumes = ["/opt/storage/data/esign:/usr/src/app/files:Z"]
environment = {
# Node environment
NODE_ENV = "production"
# App identifiers
APP_ID = random_password.app_id.result
REACT_APP_APPID = random_password.app_id.result
appName = "Four Lights - eSign"
MASTER_KEY = random_password.master_key.result
# Frontend config
PUBLIC_URL = "https://esign.${var.server_domain}"
REACT_APP_SERVERURL = "https://esign.${var.server_domain}/api/app"
GENERATE_SOURCEMAP = "false"
# Backend API config
SERVER_URL = "https://esign.${var.server_domain}/api/app"
PARSE_MOUNT = "/app"
# MongoDB connection settings
MONGODB_URI = "mongodb://systemd-documentdb:27017/esign"
# MinIO/S3 storage configuration
DO_SPACE = "esign"
DO_ENDPOINT = "systemd-minio:9000"
DO_BASEURL = "https://esign.${var.server_domain}/minio"
DO_ACCESS_KEY_ID = "esign"
DO_SECRET_ACCESS_KEY = "esign"
DO_REGION = "us-east-1"
# Email config (commented out - configure as needed)
# SMTP_ENABLE = ""
# SMTP_HOST = ""
# SMTP_PORT = ""
# SMTP_USER_EMAIL = ""
# SMTP_PASS = ""
# Document signing (optional)
PFX_BASE64 = ""
PASS_PHRASE = ""
}
haproxy_services = [
{
name = "esign"
domain = "esign.${var.server_domain}"
port = "8180"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.esign.app_urls
}
output "app_id" {
value = random_password.app_id.result
sensitive = true
}
output "master_key" {
value = random_password.master_key.result
sensitive = true
}
output "installed" {
value = true
depends_on = [module.esign.installed]
}

View File

@ -0,0 +1,58 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "admin" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "plane-admin"
image = "docker.io/makeplane/plane-admin:v1.0.0"
ports = ["3010:3000"]
environment = {
# API settings
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api"
# Web settings
NEXT_PUBLIC_WEB_URL = "https://plane.${var.server_domain}/god-mode"
# Environment
NODE_ENV = "production"
}
haproxy_services = [
{
name = "plane-admin"
domain = "plane.${var.server_domain}/god-mode/"
port = "3010"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.admin.app_urls
}
output "installed" {
value = true
depends_on = [module.admin.installed]
}

View File

@ -0,0 +1,182 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
# Accept credentials from parent Plane module
variable "db_password" {
type = string
sensitive = true
}
variable "redis_password" {
type = string
sensitive = true
}
variable "rabbitmq_password" {
type = string
sensitive = true
}
variable "minio_access_key" {
type = string
sensitive = true
}
variable "minio_secret_key" {
type = string
sensitive = true
}
variable "secret_key" {
type = string
sensitive = true
}
module "api" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "plane-api"
image = "docker.io/makeplane/plane-backend:v1.0.0"
ports = ["3011:8000"]
volumes = [
"/opt/storage/data/plane-api:/var/www/plane:Z"
]
environment = {
# Database settings
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
POSTGRES_USER = "plane"
POSTGRES_PASSWORD = var.db_password
POSTGRES_DB = "plane"
# Redis settings
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
REDIS_HOST = "systemd-valkey"
REDIS_PORT = "6379"
# RabbitMQ settings
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
RABBITMQ_HOST = "systemd-rabbitmq"
RABBITMQ_PORT = "5672"
RABBITMQ_USER = "plane"
RABBITMQ_PASSWORD = var.rabbitmq_password
RABBITMQ_VHOST = "plane"
# MinIO/S3 settings
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
AWS_ACCESS_KEY_ID = var.minio_access_key
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
AWS_S3_BUCKET_NAME = "plane"
AWS_REGION = "us-east-1"
USE_MINIO = "1"
# Application settings
SECRET_KEY = var.secret_key
DEBUG = "0"
ENVIRONMENT = "production"
# Web settings
WEB_URL = "https://plane.${var.server_domain}/api/"
ADMIN_BASE_URL = "https://plane.${var.server_domain}/god-mode/"
SPACE_BASE_URL = "https://plane.${var.server_domain}/spaces/"
APP_BASE_URL = "https://plane.${var.server_domain}/"
LIVE_BASE_URL = "https://plane.${var.server_domain}/live/"
# File upload settings
FILE_SIZE_LIMIT = "5242880"
# Rate limiting
API_KEY_RATE_LIMIT = "60/minute"
# Workers
GUNICORN_WORKERS=3
# Email settings (optional - can be configured later)
# EMAIL_HOST = ""
# EMAIL_PORT = ""
# EMAIL_HOST_USER = ""
# EMAIL_HOST_PASSWORD = ""
# EMAIL_USE_TLS = "1"
# OpenAI/GPT settings (optional)
# OPENAI_API_KEY = ""
# GPT_ENGINE = "gpt-3.5-turbo"
}
command = ["./bin/docker-entrypoint-api.sh"]
haproxy_services = [
{
name = "plane-api"
domain = "plane.${var.server_domain}/api/"
port = "3011"
host = "127.0.0.1"
tls = false
},
{
name = "plane-auth"
domain = "plane.${var.server_domain}/auth/"
port = "3011"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.api.app_urls
}
output "installed" {
value = true
depends_on = [module.api.installed]
}
output "db_password" {
value = var.db_password
sensitive = true
}
output "redis_password" {
value = var.redis_password
sensitive = true
}
output "rabbitmq_password" {
value = var.rabbitmq_password
sensitive = true
}
output "minio_access_key" {
value = var.minio_access_key
sensitive = true
}
output "minio_secret_key" {
value = var.minio_secret_key
sensitive = true
}
output "secret_key" {
value = var.secret_key
sensitive = true
}

View File

@ -0,0 +1,112 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
variable "db_password" {
type = string
sensitive = true
}
variable "redis_password" {
type = string
sensitive = true
}
variable "rabbitmq_password" {
type = string
sensitive = true
}
variable "minio_access_key" {
type = string
sensitive = true
}
variable "minio_secret_key" {
type = string
sensitive = true
}
variable "secret_key" {
type = string
sensitive = true
}
module "beat-worker" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "plane-beat-worker"
image = "docker.io/makeplane/plane-backend:v1.0.0"
volumes = [
"/opt/storage/data/plane-beat-worker:/var/www/plane:Z"
]
environment = {
# Database settings
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
POSTGRES_USER = "plane"
POSTGRES_PASSWORD = var.db_password
POSTGRES_DB = "plane"
# Redis settings
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
REDIS_HOST = "systemd-valkey"
REDIS_PORT = "6379"
# RabbitMQ settings
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
RABBITMQ_HOST = "systemd-rabbitmq"
RABBITMQ_PORT = "5672"
RABBITMQ_USER = "plane"
RABBITMQ_PASSWORD = var.rabbitmq_password
RABBITMQ_VHOST = "plane"
# MinIO/S3 settings
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
AWS_ACCESS_KEY_ID = var.minio_access_key
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
AWS_S3_BUCKET_NAME = "plane-uploads"
AWS_REGION = "us-east-1"
USE_MINIO = "1"
# Application settings
SECRET_KEY = var.secret_key
DEBUG = "0"
ENVIRONMENT = "production"
# Web settings
WEB_URL = "https://plane.${var.server_domain}"
API_BASE_URL = "https://plane.${var.server_domain}/api"
# File upload settings
FILE_SIZE_LIMIT = "5242880"
# Rate limiting
API_KEY_RATE_LIMIT = "60/minute"
}
command = ["./bin/docker-entrypoint-beat.sh"]
}
output "installed" {
value = true
depends_on = [module.beat-worker.installed]
}

View File

@ -0,0 +1,60 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "live" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "plane-live"
image = "docker.io/makeplane/plane-live:v1.0.0"
ports = ["3012:3000"]
environment = {
# API settings
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api/"
# Web settings
NEXT_PUBLIC_WEB_URL = "https://plane.${var.server_domain}/live/"
# Environment
NODE_ENV = "production"
}
restart_policy = "never"
haproxy_services = [
{
name = "plane-live"
domain = "plane.${var.server_domain}/live/"
port = "3012"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.live.app_urls
}
output "installed" {
value = true
depends_on = [module.live.installed]
}

View File

@ -0,0 +1,239 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
# Credentials from existing services
variable "postgres_password" {
type = string
sensitive = true
}
variable "minio_server" {
type = string
sensitive = true
}
variable "minio_access_key" {
type = string
sensitive = true
}
variable "minio_secret_key" {
type = string
sensitive = true
}
variable "rabbitmq_username" {
type = string
sensitive = true
}
variable "rabbitmq_password" {
type = string
sensitive = true
}
# Generate Plane-specific credentials
resource "random_password" "redis_password" {
length = 32
special = false
}
resource "random_password" "secret_key" {
length = 64
special = true
}
module "psql_db" {
source = "../postgres/tenant"
tenant_name = "plane"
admin_username = "postgres"
admin_password = var.postgres_password
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
}
module "aqmp_db" {
source = "../rabbitmq/tenant"
tenant_name = "plane"
admin_username = var.rabbitmq_username
admin_password = var.rabbitmq_password
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
}
module "minio" {
source = "../minio/tenant"
name = "plane"
server = var.minio_server
access_key = var.minio_access_key
secret_key = var.minio_secret_key
tls = true
}
# Deploy the core API service first
module "api" {
source = "./api"
wait_on = var.wait_on
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
# Use credentials from existing services
redis_password = "" #random_password.redis_password.result
db_password = module.psql_db.password
rabbitmq_password = module.aqmp_db.password
minio_access_key = module.minio.access_key
minio_secret_key = module.minio.secret_key
secret_key = random_password.secret_key.result
}
# Run database migrations after API is ready
module "migrator" {
source = "./migrator"
wait_on = module.api.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
# Use credentials from API module
db_password = module.api.db_password
redis_password = module.api.redis_password
rabbitmq_password = module.api.rabbitmq_password
minio_access_key = module.minio.access_key
minio_secret_key = module.minio.secret_key
secret_key = module.api.secret_key
}
# Start background workers after migrations
module "worker" {
source = "./worker"
wait_on = module.migrator.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
# Use credentials from API module
db_password = module.api.db_password
redis_password = module.api.redis_password
rabbitmq_password = module.api.rabbitmq_password
minio_access_key = module.minio.access_key
minio_secret_key = module.minio.secret_key
secret_key = module.api.secret_key
}
module "beat-worker" {
source = "./beat-worker"
wait_on = module.migrator.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
# Use credentials from API module
db_password = module.api.db_password
redis_password = module.api.redis_password
rabbitmq_password = module.api.rabbitmq_password
minio_access_key = module.minio.access_key
minio_secret_key = module.minio.secret_key
secret_key = module.api.secret_key
}
# Start web interfaces after API is ready
module "web" {
source = "./web"
wait_on = module.api.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
}
module "admin" {
source = "./admin"
wait_on = module.api.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
}
module "space" {
source = "./space"
wait_on = module.api.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
}
module "live" {
source = "./live"
wait_on = module.api.installed
server_ip = var.server_ip
server_domain = var.server_domain
ssh_private_key_path = var.ssh_private_key_path
}
# Aggregate all app URLs
locals {
app_urls = {
web = module.web.app_urls
admin = module.admin.app_urls
space = module.space.app_urls
live = module.live.app_urls
api = module.api.app_urls
}
}
output "app_urls" {
value = local.app_urls
description = "All Plane application URLs"
}
output "installed" {
value = true
depends_on = [
module.api.installed,
module.migrator.installed,
module.worker.installed,
module.beat-worker.installed,
module.web.installed,
module.admin.installed,
module.space.installed,
module.live.installed
]
description = "Whether all Plane services are installed"
}
output "credentials" {
value = {
db_password = module.api.db_password
redis_password = module.api.redis_password
minio_access_key = module.api.minio_access_key
minio_secret_key = module.api.minio_secret_key
rabbitmq_password = module.api.rabbitmq_password
secret_key = module.api.secret_key
}
sensitive = true
description = "All Plane service credentials"
}
output "main_url" {
value = "https://plane.${var.server_domain}"
description = "Main Plane application URL"
}

View File

@ -0,0 +1,112 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
variable "db_password" {
type = string
sensitive = true
}
variable "redis_password" {
type = string
sensitive = true
}
variable "rabbitmq_password" {
type = string
sensitive = true
}
variable "minio_access_key" {
type = string
sensitive = true
}
variable "minio_secret_key" {
type = string
sensitive = true
}
variable "secret_key" {
type = string
sensitive = true
}
module "migrator" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "plane-migrator"
image = "docker.io/makeplane/plane-backend:v1.0.0"
volumes = [
"/opt/storage/data/plane-migrator:/var/www/plane:Z"
]
environment = {
# Database settings
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
POSTGRES_USER = "plane"
POSTGRES_PASSWORD = var.db_password
POSTGRES_DB = "plane"
# Redis settings
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
REDIS_HOST = "systemd-valkey"
REDIS_PORT = "6379"
# RabbitMQ settings
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
RABBITMQ_HOST = "systemd-rabbitmq"
RABBITMQ_PORT = "5672"
RABBITMQ_USER = "plane"
RABBITMQ_PASSWORD = var.rabbitmq_password
RABBITMQ_VHOST = "plane"
# MinIO/S3 settings
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
AWS_ACCESS_KEY_ID = var.minio_access_key
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
AWS_S3_BUCKET_NAME = "plane-uploads"
AWS_REGION = "us-east-1"
USE_MINIO = "1"
# Application settings
SECRET_KEY = var.secret_key
DEBUG = "0"
ENVIRONMENT = "production"
# Web settings
WEB_URL = "https://plane.${var.server_domain}"
API_BASE_URL = "https://plane.${var.server_domain}/api"
# File upload settings
FILE_SIZE_LIMIT = "5242880"
# Rate limiting
API_KEY_RATE_LIMIT = "60/minute"
}
command = ["./bin/docker-entrypoint-migrator.sh"]
}
output "installed" {
value = true
depends_on = [module.migrator.installed]
}

View File

@ -0,0 +1,60 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "space" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "plane-space"
image = "docker.io/makeplane/plane-space:v1.0.0"
ports = ["3013:3000"]
environment = {
# API settings
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api"
# Web settings
NEXT_PUBLIC_WEB_URL = "https://plane.${var.server_domain}/spaces"
# Environment
NODE_ENV = "production"
}
haproxy_services = [
{
name = "plane-space"
domain = "plane.${var.server_domain}/spaces/"
port = "3013"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.space.app_urls
}
output "installed" {
value = true
depends_on = [module.space.installed]
}

View File

@ -0,0 +1,64 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "web" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "plane-web"
image = "docker.io/makeplane/plane-frontend:v1.0.0"
ports = ["3014:3000"]
environment = {
# API settings
NEXT_PUBLIC_API_BASE_URL = "https://plane.${var.server_domain}/api"
# Web settings
NEXT_PUBLIC_WEB_BASE_URL = "https://plane.${var.server_domain}"
NEXT_PUBLIC_ADMIN_BASE_URL = "https://plane.${var.server_domain}/god-mode"
NEXT_PUBLIC_SPACE_BASE_URL = "https://plane.${var.server_domain}/spaces"
NEXT_PUBLIC_LIVE_BASE_URL = "https://plane.${var.server_domain}/live"
# Environment
NODE_ENV = "production"
}
haproxy_services = [
{
name = "plane-web"
domain = "plane.${var.server_domain}"
port = "3014"
host = "127.0.0.1"
tls = false
}
]
}
output "app_urls" {
value = module.web.app_urls
}
output "installed" {
value = true
depends_on = [module.web.installed]
}

View File

@ -0,0 +1,113 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
variable "db_password" {
type = string
sensitive = true
}
variable "redis_password" {
type = string
sensitive = true
}
variable "rabbitmq_password" {
type = string
sensitive = true
}
variable "minio_access_key" {
type = string
sensitive = true
}
variable "minio_secret_key" {
type = string
sensitive = true
}
variable "secret_key" {
type = string
sensitive = true
}
module "worker" {
source = "../../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
restart_policy = "never"
app_name = "plane-worker"
image = "docker.io/makeplane/plane-backend:v1.0.0"
volumes = [
"/opt/storage/data/plane-worker:/var/www/plane:Z"
]
environment = {
# Database settings
DATABASE_URL = "postgresql://plane:${var.db_password}@systemd-postgres:5432/plane"
POSTGRES_USER = "plane"
POSTGRES_PASSWORD = var.db_password
POSTGRES_DB = "plane"
PGDATA = "/var/lib/postgresql/data"
# Redis settings
REDIS_URL = "redis://:${var.redis_password}@systemd-valkey:6379/0"
REDIS_HOST = "systemd-valkey"
REDIS_PORT = "6379"
# RabbitMQ settings
RABBITMQ_URL = "amqp://plane:${var.rabbitmq_password}@systemd-rabbitmq:5672/plane"
RABBITMQ_HOST = "systemd-rabbitmq"
RABBITMQ_PORT = "5672"
RABBITMQ_USER = "plane"
RABBITMQ_PASSWORD = var.rabbitmq_password
RABBITMQ_VHOST = "plane"
# MinIO/S3 settings
AWS_S3_ENDPOINT_URL = "http://systemd-minio:9000"
AWS_ACCESS_KEY_ID = var.minio_access_key
AWS_SECRET_ACCESS_KEY = var.minio_secret_key
AWS_S3_BUCKET_NAME = "plane-uploads"
AWS_REGION = "us-east-1"
USE_MINIO = "1"
# Application settings
SECRET_KEY = var.secret_key
DEBUG = "0"
ENVIRONMENT = "production"
# Web settings
WEB_URL = "https://plane.${var.server_domain}"
API_BASE_URL = "https://plane.${var.server_domain}/api"
# File upload settings
FILE_SIZE_LIMIT = "5242880"
# Rate limiting
API_KEY_RATE_LIMIT = "60/minute"
}
command = ["./bin/docker-entrypoint-worker.sh"]
}
output "installed" {
value = true
depends_on = [module.worker.installed]
}

View File

@ -0,0 +1,48 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
resource "random_password" "password" {
length = 32
special = false
}
module "postgres" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "postgres"
image = "docker.io/postgres:17"
ports = ["5432:5432"]
volumes = ["/opt/storage/data/postgres:/var/lib/postgresql/data:Z"]
environment = {
POSTGRES_PASSWORD = random_password.password.result
}
}
output "app_urls" {
value = module.postgres.app_urls
}
output "installed" {
value = true
depends_on = [module.postgres.installed]
}
output "password" {
value = random_password.password.result
sensitive = true
}

View File

@ -0,0 +1,132 @@
# PostgreSQL Tenant Module
This module creates a PostgreSQL database and user (tenant) on a VPS-hosted PostgreSQL instance using SSH tunneling for secure access.
## Features
- Creates SSH tunnel to VPS-hosted PostgreSQL
- Creates isolated database and user for tenant
- Generates secure random password
- Outputs connection details for application use
- Supports explicit dependencies via `wait_on`
## Requirements
- SSH access to VPS with key-based authentication
- PostgreSQL admin credentials
- `terraform-ssh-tunnel` module (automatically downloaded)
- `cyrilgdn/postgresql` provider (automatically configured)
## Usage
### Basic Example
```hcl
module "app_tenant" {
source = "./quadlets/modules/postgres/tenant"
tenant_name = "myapp"
admin_username = "postgres"
admin_password = var.postgres_admin_password
server_ip = "203.0.113.10"
ssh_user = "fourlights"
# Wait for PostgreSQL service to be ready
wait_on = module.postgres.installed
}
# Use the connection details
output "app_db_connection" {
value = {
host = module.app_tenant.host
port = module.app_tenant.port
database = module.app_tenant.database
username = module.app_tenant.username
password = module.app_tenant.password
}
sensitive = true
}
```
### With Custom Port
```hcl
module "custom_tenant" {
source = "./quadlets/modules/postgres/tenant"
tenant_name = "custom_app"
admin_username = "postgres"
admin_password = var.postgres_admin_password
server_ip = "203.0.113.10"
target_port = 5433 # Custom PostgreSQL port
wait_on = module.postgres.installed
}
```
### Multiple Tenants
```hcl
module "tenant_1" {
source = "./quadlets/modules/postgres/tenant"
tenant_name = "app1"
admin_username = "postgres"
admin_password = var.postgres_admin_password
server_ip = var.vps_ip
wait_on = module.postgres.installed
}
module "tenant_2" {
source = "./quadlets/modules/postgres/tenant"
tenant_name = "app2"
admin_username = "postgres"
admin_password = var.postgres_admin_password
server_ip = var.vps_ip
wait_on = module.postgres.installed
}
```
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| tenant_name | Name of the tenant (used for database name and username) | string | - | yes |
| admin_username | PostgreSQL admin username | string | "postgres" | no |
| admin_password | PostgreSQL admin password | string | - | yes |
| server_ip | VPS server IP address | string | - | yes |
| ssh_user | SSH user for connecting to VPS | string | "fourlights" | no |
| ssh_private_key_path | Path to SSH private key | string | "~/.ssh/id_rsa" | no |
| target_port | PostgreSQL port on VPS | number | 5432 | no |
| wait_on | Resources to wait on before creating tenant | any | null | no |
## Outputs
| Name | Description | Sensitive |
|------|-------------|-----------|
| host | PostgreSQL connection host (via SSH tunnel) | no |
| port | PostgreSQL connection port (via SSH tunnel) | no |
| database | Database name | no |
| username | Database username | no |
| password | Database password | yes |
| connection_string | Full PostgreSQL connection string | yes |
| installed | Installation complete flag | no |
## How It Works
1. **SSH Tunnel**: The module uses `terraform-ssh-tunnel` to create a secure SSH tunnel from your local machine to the VPS PostgreSQL instance
2. **Provider Configuration**: The PostgreSQL provider is configured to connect through the tunnel
3. **Resource Creation**: Creates a PostgreSQL role (user) and database with appropriate permissions
4. **Password Generation**: Generates a cryptographically secure random password
5. **Outputs**: Provides connection details for your application to use
## Notes
- The SSH tunnel is created dynamically when Terraform needs to connect
- Ensure your SSH agent is running and has the appropriate key loaded
- The database and user will have the same name as `tenant_name`
- The user is granted full privileges on their database
- Password is stored in Terraform state (sensitive)

View File

@ -0,0 +1,81 @@
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
postgresql = {
source = "cyrilgdn/postgresql"
version = "~> 1.22"
}
}
}
# Generate random password for the tenant user
resource "random_password" "tenant_password" {
length = 32
special = false
min_upper = 2
min_lower = 2
min_numeric = 2
}
# Create SSH tunnel to PostgreSQL on VPS
module "db_tunnel" {
source = "flaupretre/tunnel/ssh"
version = "2.3.0"
target_host = "127.0.0.1"
target_port = var.target_port
gateway_host = var.server_ip
gateway_user = var.ssh_user
gateway_port = 22
}
provider postgresql {
alias = "tunnel"
host = module.db_tunnel.host
port = module.db_tunnel.port
username = var.admin_username
password = var.admin_password
database = "postgres"
sslmode = "disable"
superuser = false
}
#---- Database
resource postgresql_database db {
provider = postgresql.tunnel
name = var.tenant_name
owner = var.tenant_name
encoding = "UTF8"
lc_collate = "C"
lc_ctype = "C"
}
#---- DB user/role
resource postgresql_role role {
provider = postgresql.tunnel
name = var.tenant_name
login = true
password = random_password.tenant_password.result
}
#---- Grant
# SQL: grant all on <db>.* to <user>@'%';
resource postgresql_grant grant {
provider = postgresql.tunnel
object_type = "database"
database = var.tenant_name
role = var.tenant_name
privileges = ["ALL"]
}

View File

@ -0,0 +1,27 @@
output "database" {
description = "Database name"
value = var.tenant_name
}
output "username" {
description = "Database username"
value = var.tenant_name
}
output "password" {
description = "Database password"
value = random_password.tenant_password.result
sensitive = true
}
output "connection_string" {
description = "PostgreSQL connection string"
value = "postgresql://${var.tenant_name}:${random_password.tenant_password.result}@localhost:${var.target_port}/${var.tenant_name}"
sensitive = true
}
output "installed" {
description = "Installation complete flag"
value = true
depends_on = [postgresql_role.role, postgresql_grant.grant, postgresql_database.db, random_password.tenant_password]
}

View File

@ -0,0 +1,44 @@
variable "tenant_name" {
description = "Name of the tenant (used for database name and username)"
type = string
}
variable "admin_username" {
description = "PostgreSQL admin username"
type = string
default = "postgres"
}
variable "admin_password" {
description = "PostgreSQL admin password"
type = string
sensitive = true
}
variable "server_ip" {
description = "VPS server IP address"
type = string
}
variable "ssh_user" {
description = "SSH user for connecting to VPS"
type = string
default = "fourlights"
}
variable "ssh_private_key_path" {
description = "Path to SSH private key for authentication"
type = string
}
variable "target_port" {
description = "PostgreSQL port on VPS"
type = number
default = 5432
}
variable "wait_on" {
description = "Resources to wait on before creating tenant"
type = any
default = true
}

View File

@ -0,0 +1,63 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {}
resource "random_password" "api_key" {
length = 64
special = false
}
module "qdrant" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "qdrant"
image = "docker.io/qdrant/qdrant"
ports = ["6333:6333", "6334:6334"]
volumes = ["/opt/storage/data/qdrant:/qdrant/storage:z"]
environment = {
"QDRANT__SERVICE__API_KEY" = random_password.api_key.result
}
haproxy_services = [
{
name = "qdrant"
domain = "qdrant.${var.server_domain}"
port = "6333"
host = "127.0.0.1"
tls = false
#grpc_port = "6334"
},
]
}
output "app_urls" {
value = module.qdrant.app_urls
}
output "installed" {
value = true
depends_on = [module.qdrant.installed]
}
output "api_key" {
value = random_password.api_key.result
sensitive = true
}

View File

@ -0,0 +1,60 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
resource "random_password" "username" {
length = 32
special = false
}
resource "random_password" "password" {
length = 32
special = false
}
module "rabbitmq" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "rabbitmq"
image = "docker.io/rabbitmq:management"
ports = ["5672:5672", "15672:15672"]
volumes = ["/opt/storage/data/rabbitmq:/var/lib/rabbitmq:Z"]
environment = {
RABBITMQ_DEFAULT_USER = random_password.username.result
RABBITMQ_DEFAULT_PASS = random_password.password.result
}
}
output "app_urls" {
value = module.rabbitmq.app_urls
}
output "installed" {
value = true
depends_on = [module.rabbitmq.installed]
}
output "username" {
value = random_password.username.result
sensitive = true
}
output "password" {
value = random_password.password.result
sensitive = true
}

View File

@ -0,0 +1,180 @@
# RabbitMQ Tenant Module
This module creates a RabbitMQ virtual host (vhost) and user (tenant) on a VPS-hosted RabbitMQ instance using SSH tunneling for secure access to the Management API.
## Features
- Creates SSH tunnel to VPS-hosted RabbitMQ Management API
- Creates isolated virtual host and user for tenant
- Configures full permissions for user on their vhost
- Generates secure random password
- Outputs connection details for application use
- Supports explicit dependencies via `wait_on`
## Requirements
- SSH access to VPS with key-based authentication
- RabbitMQ admin credentials
- RabbitMQ Management plugin enabled
- `terraform-ssh-tunnel` module (automatically downloaded)
- `cyrilgdn/rabbitmq` provider (automatically configured)
## Usage
### Basic Example
```hcl
module "app_tenant" {
source = "./quadlets/modules/rabbitmq/tenant"
tenant_name = "myapp"
admin_username = var.rabbitmq_admin_username
admin_password = var.rabbitmq_admin_password
server_ip = "203.0.113.10"
ssh_user = "fourlights"
# Wait for RabbitMQ service to be ready
wait_on = module.rabbitmq.installed
}
# Use the connection details
output "app_mq_connection" {
value = {
host = module.app_tenant.host
port = module.app_tenant.port
vhost = module.app_tenant.vhost
username = module.app_tenant.username
password = module.app_tenant.password
}
sensitive = true
}
```
### With Custom Management Port
```hcl
module "custom_tenant" {
source = "./quadlets/modules/rabbitmq/tenant"
tenant_name = "custom_app"
admin_username = var.rabbitmq_admin_username
admin_password = var.rabbitmq_admin_password
server_ip = "203.0.113.10"
target_port = 25672 # Custom Management API port
wait_on = module.rabbitmq.installed
}
```
### Multiple Tenants
```hcl
module "tenant_1" {
source = "./quadlets/modules/rabbitmq/tenant"
tenant_name = "app1"
admin_username = var.rabbitmq_admin_username
admin_password = var.rabbitmq_admin_password
server_ip = var.vps_ip
wait_on = module.rabbitmq.installed
}
module "tenant_2" {
source = "./quadlets/modules/rabbitmq/tenant"
tenant_name = "app2"
admin_username = var.rabbitmq_admin_username
admin_password = var.rabbitmq_admin_password
server_ip = var.vps_ip
wait_on = module.rabbitmq.installed
}
```
### Using Connection String
```hcl
module "app_tenant" {
source = "./quadlets/modules/rabbitmq/tenant"
tenant_name = "myapp"
admin_username = var.rabbitmq_admin_username
admin_password = var.rabbitmq_admin_password
server_ip = var.vps_ip
wait_on = module.rabbitmq.installed
}
# Use in application configuration
resource "kubernetes_secret" "rabbitmq_credentials" {
metadata {
name = "rabbitmq-connection"
}
data = {
connection_string = module.app_tenant.connection_string
}
}
```
## Inputs
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|----------|
| tenant_name | Name of the tenant (used for vhost name and username) | string | - | yes |
| admin_username | RabbitMQ admin username | string | - | yes |
| admin_password | RabbitMQ admin password | string | - | yes |
| server_ip | VPS server IP address | string | - | yes |
| ssh_user | SSH user for connecting to VPS | string | "fourlights" | no |
| ssh_private_key_path | Path to SSH private key | string | "~/.ssh/id_rsa" | no |
| target_port | RabbitMQ Management API port on VPS | number | 15672 | no |
| wait_on | Resources to wait on before creating tenant | any | null | no |
## Outputs
| Name | Description | Sensitive |
|------|-------------|-----------|
| host | RabbitMQ connection host (VPS IP for AMQP) | no |
| port | RabbitMQ AMQP connection port (5672) | no |
| management_host | RabbitMQ Management API host (via SSH tunnel) | no |
| management_port | RabbitMQ Management API port (via SSH tunnel) | no |
| vhost | Virtual host name | no |
| username | RabbitMQ username | no |
| password | RabbitMQ password | yes |
| connection_string | Full AMQP connection string | yes |
| installed | Installation complete flag | no |
## How It Works
1. **SSH Tunnel**: The module uses `terraform-ssh-tunnel` to create a secure SSH tunnel from your local machine to the VPS RabbitMQ Management API (port 15672)
2. **Provider Configuration**: The RabbitMQ provider is configured to connect through the tunnel
3. **Resource Creation**: Creates a RabbitMQ virtual host, user, and permissions
4. **Password Generation**: Generates a cryptographically secure random password
5. **Outputs**: Provides connection details for your application to use
## Connection Details
- **Management API**: Accessed via SSH tunnel (for Terraform operations)
- **AMQP Protocol**: Direct connection to VPS on port 5672 (for application connections)
- **Virtual Host**: Isolated messaging environment for the tenant
- **Permissions**: User has full configure, write, and read permissions on their vhost
## Notes
- The SSH tunnel is only used for management operations (creating vhost/user/permissions)
- Applications connect directly to the VPS AMQP port (5672), not through the tunnel
- The virtual host and user will have the same name as `tenant_name`
- User is granted `configure=".*"`, `write=".*"`, `read=".*"` on their vhost
- Password is stored in Terraform state (sensitive)
- Ensure RabbitMQ Management plugin is enabled on the VPS instance
## Permissions Explained
The tenant user receives the following permissions on their vhost:
- **configure**: Can declare exchanges, queues, and bindings
- **write**: Can publish messages
- **read**: Can consume messages
All permissions use the `.*` regex pattern, granting full access within the vhost.

View File

@ -0,0 +1,71 @@
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "~> 3.6"
}
rabbitmq = {
source = "cyrilgdn/rabbitmq"
version = "~> 1.8"
}
}
}
# Generate random password for the tenant user
resource "random_password" "tenant_password" {
length = 32
special = false
min_upper = 2
min_lower = 2
min_numeric = 2
}
# Create SSH tunnel to RabbitMQ Management API on VPS
module "ssh_tunnel" {
source = "flaupretre/tunnel/ssh"
version = "2.2.1"
target_host = "127.0.0.1"
target_port = var.target_port
gateway_host = var.server_ip
gateway_user = var.ssh_user
# Tunnel will be created when needed
timeout = "30s"
}
# Configure RabbitMQ provider to use the SSH tunnel
provider "rabbitmq" {
endpoint = "http://${module.ssh_tunnel.host}:${module.ssh_tunnel.port}"
username = var.admin_username
password = var.admin_password
}
# Create the virtual host
resource "rabbitmq_vhost" "tenant_vhost" {
depends_on = [var.wait_on]
name = var.tenant_name
}
# Create the user
resource "rabbitmq_user" "tenant_user" {
depends_on = [var.wait_on]
name = var.tenant_name
password = random_password.tenant_password.result
tags = []
}
# Grant permissions to the user on the vhost
resource "rabbitmq_permissions" "tenant_permissions" {
user = rabbitmq_user.tenant_user.name
vhost = rabbitmq_vhost.tenant_vhost.name
permissions {
configure = ".*"
write = ".*"
read = ".*"
}
}

View File

@ -0,0 +1,47 @@
output "host" {
description = "RabbitMQ connection host (via SSH tunnel)"
value = var.server_ip
}
output "port" {
description = "RabbitMQ AMQP connection port"
value = 5672
}
output "management_host" {
description = "RabbitMQ Management API host (via SSH tunnel)"
value = module.ssh_tunnel.host
}
output "management_port" {
description = "RabbitMQ Management API port (via SSH tunnel)"
value = module.ssh_tunnel.port
}
output "vhost" {
description = "Virtual host name"
value = rabbitmq_vhost.tenant_vhost.name
}
output "username" {
description = "RabbitMQ username"
value = rabbitmq_user.tenant_user.name
}
output "password" {
description = "RabbitMQ password"
value = random_password.tenant_password.result
sensitive = true
}
output "connection_string" {
description = "RabbitMQ AMQP connection string"
value = "amqp://${rabbitmq_user.tenant_user.name}:${random_password.tenant_password.result}@${var.server_ip}:5672/${rabbitmq_vhost.tenant_vhost.name}"
sensitive = true
}
output "installed" {
description = "Installation complete flag"
value = true
depends_on = [rabbitmq_vhost.tenant_vhost, rabbitmq_user.tenant_user, rabbitmq_permissions.tenant_permissions]
}

View File

@ -0,0 +1,44 @@
variable "tenant_name" {
description = "Name of the tenant (used for vhost name and username)"
type = string
}
variable "admin_username" {
description = "RabbitMQ admin username"
type = string
}
variable "admin_password" {
description = "RabbitMQ admin password"
type = string
sensitive = true
}
variable "server_ip" {
description = "VPS server IP address"
type = string
}
variable "ssh_user" {
description = "SSH user for connecting to VPS"
type = string
default = "fourlights"
}
variable "ssh_private_key_path" {
description = "Path to SSH private key for authentication"
type = string
default = "~/.ssh/id_rsa"
}
variable "target_port" {
description = "RabbitMQ Management API port on VPS"
type = number
default = 15672
}
variable "wait_on" {
description = "Resources to wait on before creating tenant"
type = any
default = null
}

View File

@ -0,0 +1,97 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
variable "server_url" {
type = string
default = "https://mail.binarysunset.dev"
}
# tmail-web reads its configuration from a file at
# /usr/share/nginx/html/assets/env.file inside the container. Setting
# SERVER_URL as a container environment variable does NOT work (see
# https://github.com/linagora/tmail-flutter/issues/4240); the file must
# be injected. We write it to the host and bind-mount it into the
# container.
locals {
env_file_host_path = "/opt/storage/data/tmail-web/env.file"
env_file_container_path = "/usr/share/nginx/html/assets/env.file"
env_file_content = "SERVER_URL=${var.server_url}\n"
}
resource "null_resource" "tmail_env_file" {
triggers = {
content = local.env_file_content
server_ip = var.server_ip
host_path = local.env_file_host_path
}
provisioner "remote-exec" {
inline = [
"mkdir -p $(dirname ${local.env_file_host_path})",
"cat > ${local.env_file_host_path} << 'EOF'",
local.env_file_content,
"EOF",
]
connection {
type = "ssh"
host = var.server_ip
user = "fourlights"
agent = true
agent_identity = var.ssh_private_key_path
}
}
}
module "tmail-web" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "tmail-web"
image = "docker.io/linagora/tmail-web:latest"
ports = ["8080:80"]
volumes = ["${local.env_file_host_path}:${local.env_file_container_path}:ro"]
environment = {}
haproxy_services = [
{
name = "tmail-web"
domain = "mail.${var.server_domain}"
port = "8080"
host = "127.0.0.1"
tls = false
}
]
healthcmd = "curl -f http://localhost:80 || exit 1"
depends_on = [null_resource.tmail_env_file]
}
output "app_urls" {
value = module.tmail-web.app_urls
}
output "installed" {
value = true
depends_on = [module.tmail-web.installed]
}

View File

@ -0,0 +1,58 @@
variable "wait_on" {
type = any
description = "Resources to wait on"
default = true
}
variable "server_ip" {
type = string
}
variable "ssh_private_key_path" {
type = string
}
variable "server_domain" {
type = string
}
module "zot" {
source = "../quadlet-app"
wait_on = var.wait_on
server_ip = var.server_ip
ssh_private_key_path = var.ssh_private_key_path
app_name = "zot"
image = "ghcr.io/project-zot/zot:latest"
ports = ["5000:5000"]
volumes = [
"/opt/storage/data/zot/config.json:/etc/zot/config.json:Z",
"/opt/storage/data/zot:/var/lib/registry:Z",
]
environment = {
ND_LOGLEVEL="info"
ND_ENABLEINSIGHTSCOLLECTOR="false"
}
haproxy_services = [
{
name = "registry"
domain = "registry.${var.server_domain}"
port = "5000"
host = "127.0.0.1"
tls = false
},
]
}
output "app_urls" {
value = module.zot.app_urls
}
output "installed" {
value = true
depends_on = [module.zot.installed]
}