Treafik woz 'ere

This commit is contained in:
Greyscale 2024-11-26 10:50:53 +01:00
parent ef40638cd6
commit 618ca06093
Signed by: grey
GPG key ID: DDB392AE64B32D89
57 changed files with 891 additions and 199 deletions

View file

@ -7,3 +7,15 @@ variable "labels" {
default = {} default = {}
description = "A map of labels to apply to the service" description = "A map of labels to apply to the service"
} }
variable "network_name" {
type = string
description = "Override the automatically selected name of the network"
default = null
}
variable "subnet" {
type = string
description = "The subnet to use for the network."
default = null //"172.16.0.0/16"
}

View file

@ -1,9 +1,12 @@
locals { locals {
network_name = var.stack_name // Concat up the network name
network_name = var.network_name != null ? "${var.stack_name}-${var.network_name}" : var.stack_name
// Attach labels
labels = merge(var.labels, { labels = merge(var.labels, {
"com.docker.stack.namespace" = var.stack_name "com.docker.stack.namespace" = var.stack_name
"ooo.grey.network.stack" = var.stack_name "ooo.grey.network.stack" = var.stack_name
"ooo.grey.network.name" = local.network_name "ooo.grey.network.name" = local.network_name
#"ooo.grey.network.created" = timestamp() "ooo.grey.network.subnet" = local.subnet
}) })
} }

View file

@ -1,6 +1,13 @@
resource "docker_network" "instance" { resource "docker_network" "instance" {
name = local.network_name name = local.network_name
driver = "overlay" driver = "overlay"
attachable = true
ipam_driver = "default"
ipam_config {
aux_address = {}
subnet = local.subnet
gateway = local.gateway
}
# Attach labels # Attach labels
dynamic "labels" { dynamic "labels" {
@ -10,4 +17,8 @@ resource "docker_network" "instance" {
value = labels.value value = labels.value
} }
} }
lifecycle {
create_before_destroy = false
}
} }

View file

@ -1,3 +1,9 @@
output "network" { output "network" {
value = docker_network.instance.id value = docker_network.instance.id
}
output "name" {
value = docker_network.instance.name
}
output "id" {
value = docker_network.instance.id
} }

14
docker/network/subnet.tf Normal file
View file

@ -0,0 +1,14 @@
resource "random_integer" "upper_mid_byte" {
min = 18
max = 31
}
resource "random_integer" "lower_mid_byte" {
min = 10
max = 254
}
locals {
// Generate a subnet
subnet = var.subnet != null ? var.subnet : "172.${random_integer.upper_mid_byte.result}.${random_integer.lower_mid_byte.result}.0/24"
// Calculate the gateway from the subnet
gateway = cidrhost(local.subnet, 1)
}

View file

@ -5,6 +5,10 @@ terraform {
source = "kreuzwerker/docker" source = "kreuzwerker/docker"
version = "~>3.0" version = "~>3.0"
} }
random = {
source = "hashicorp/random"
version = "~>3.3"
}
} }
} }

View file

@ -1,17 +1,19 @@
/*
resource "docker_image" "mirror" { resource "docker_image" "mirror" {
count = var.mirror != null ? 1 : 0 count = local.enable_mirror ? 1 : 0
name = data.docker_registry_image.image.name name = data.docker_registry_image.image.name
pull_triggers = [data.docker_registry_image.image.sha256_digest] pull_triggers = [data.docker_registry_image.image.sha256_digest]
force_remove = false force_remove = false
} }
resource "docker_tag" "mirror" { resource "docker_tag" "mirror" {
count = var.mirror != null ? 1 : 0 count = local.enable_mirror ? 1 : 0
source_image = docker_image.mirror[0].name source_image = docker_image.mirror[0].name
target_image = var.mirror target_image = var.mirror
} }
resource "docker_registry_image" "mirror" { resource "docker_registry_image" "mirror" {
count = var.mirror != null ? 1 : 0 count = local.enable_mirror ? 1 : 0
depends_on = [docker_tag.mirror[0]] depends_on = [docker_tag.mirror[0]]
name = docker_tag.mirror[0].target_image name = docker_tag.mirror[0].target_image
keep_remotely = true keep_remotely = true
} }
*/

View file

@ -19,6 +19,11 @@ variable "restart_policy" {
condition = var.restart_policy == "any" || var.restart_policy == "on-failure" || var.restart_policy == "none" condition = var.restart_policy == "any" || var.restart_policy == "on-failure" || var.restart_policy == "none"
} }
} }
variable "restart_delay" {
type = string
default = "0s"
description = "The delay before restarting the service."
}
variable "one_shot" { variable "one_shot" {
type = bool type = bool
default = false default = false
@ -53,9 +58,12 @@ variable "volumes" {
description = "A map of volume names to create and mount. The key is the volume name, and the value is the mount point." description = "A map of volume names to create and mount. The key is the volume name, and the value is the mount point."
} }
variable "remote_volumes" { variable "remote_volumes" {
type = map(string) type = map(object({
id = string
driver = string
}))
default = {} default = {}
description = "A map of remote volumes to mount into the container." description = "A map of remote volumes to mount into the container. The key is the source, and the value is the target."
} }
variable "mounts" { variable "mounts" {
type = map(string) type = map(string)
@ -76,8 +84,8 @@ variable "ports" {
default = [] default = []
description = "A map of port mappings to expose on the host. The key is the host port, and the value is the container port." description = "A map of port mappings to expose on the host. The key is the host port, and the value is the container port."
validation { validation {
error_message = "Host Ports must be between 1024 and 65535." error_message = "Host Ports must be between 1 and 65535."
condition = alltrue([for port in var.ports : port.host >= 1024 && port.host <= 65535]) condition = alltrue([for port in var.ports : port.host >= 1 && port.host <= 65535])
} }
validation { validation {
error_message = "Container Ports must be between 1 and 65535." error_message = "Container Ports must be between 1 and 65535."
@ -105,6 +113,11 @@ variable "parallelism" {
type = number type = number
description = "The number of instances to run." description = "The number of instances to run."
} }
variable "parallelism_per_node" {
default = 0
type = number
description = "The maximum number of instances to run per node. 0 means no limit."
}
variable "update_waves" { variable "update_waves" {
default = 3 default = 3
type = number type = number
@ -144,4 +157,32 @@ variable "converge_timeout" {
default = "2m" default = "2m"
type = string type = string
description = "The timeout for the service to converge." description = "The timeout for the service to converge."
}
variable "traefik" {
default = null
type = object({
domain = string
port = optional(number)
})
description = "Whether to enable traefik for the service."
}
variable "limit_cpu" {
default = null
type = number
description = "The CPU limit for the service."
}
variable "limit_ram_mb" {
default = null
type = number
description = "The RAM limit for the service, measured in megabytes."
}
variable "reserved_cpu" {
default = null
type = number
description = "The CPU reservation for the service."
}
variable "reserved_ram_mb" {
default = null
type = number
description = "The RAM reservation for the service, measured in megabytes."
} }

27
docker/service/labels.tf Normal file
View file

@ -0,0 +1,27 @@
locals {
# Define service labels en-masse
labels = merge(var.labels, {
"com.docker.stack.namespace" = var.stack_name
"com.docker.stack.image" = data.docker_registry_image.image.name
"ooo.grey.service.stack" = var.stack_name
"ooo.grey.service.name" = var.service_name
"ooo.grey.service.image" = data.docker_registry_image.image.name
"ooo.grey.service.image.digest" = data.docker_registry_image.image.sha256_digest
}, local.traefik_labels)
# Calculate the traefik labels to use if enabled
traefik_labels = merge(
(var.traefik == null ? {
"traefik.enable" = "false"
} : {
"traefik.enable" = "true"
"traefik.http.routers.${local.service_name}.rule" = "Host(`${var.traefik.domain}`)"
"traefik.http.routers.${local.service_name}.entrypoints" = "websecure"
"traefik.http.routers.${local.service_name}.tls.certresolver" = "default"
}),
(try(var.traefik.port, null) == null ? {} : {
"traefik.http.services.${local.service_name}.loadbalancer.server.port" = var.traefik.port
})
)
}

View file

@ -5,16 +5,9 @@ locals {
substr(var.service_name, 0, 63 - 1 - 20), substr(var.service_name, 0, 63 - 1 - 20),
]) ])
# Define service labels en-masse enable_mirror = false // var.mirror != null
labels = merge(var.labels, {
"com.docker.stack.namespace" = var.stack_name
"com.docker.stack.image" = data.docker_registry_image.image.name
"ooo.grey.service.stack" = var.stack_name
"ooo.grey.service.name" = var.service_name
"ooo.grey.service.image" = data.docker_registry_image.image.name
"ooo.grey.service.image.digest" = data.docker_registry_image.image.sha256_digest
})
# Calculate the docker image to use # Calculate the docker image to use
image = var.mirror != null ? "${docker_registry_image.mirror[0].name}@${docker_registry_image.mirror[0].sha256_digest}" : "${data.docker_registry_image.image.name}@${data.docker_registry_image.image.sha256_digest}" #image = local.enable_mirror ? "${docker_registry_image.mirror[0].name}@${docker_registry_image.mirror[0].sha256_digest}" : "${data.docker_registry_image.image.name}@${data.docker_registry_image.image.sha256_digest}"
image = "${data.docker_registry_image.image.name}@${data.docker_registry_image.image.sha256_digest}"
} }

View file

@ -0,0 +1,4 @@
data "docker_network" "networks" {
count = var.networks != null ? length(var.networks) : 0
name = var.networks[count.index]
}

View file

@ -17,9 +17,10 @@ resource "docker_service" "instance" {
dynamic "mounts" { dynamic "mounts" {
for_each = var.volumes for_each = var.volumes
content { content {
target = mounts.value source = docker_volume.volume[mounts.key].id
source = docker_volume.volume[mounts.key].id target = mounts.value
type = "volume" type = "volume"
read_only = false # Nice assumption bro.
} }
} }
@ -27,9 +28,10 @@ resource "docker_service" "instance" {
dynamic "mounts" { dynamic "mounts" {
for_each = var.remote_volumes for_each = var.remote_volumes
content { content {
target = mounts.value source = mounts.value.id
source = mounts.key target = mounts.key
type = "volume" type = "volume"
read_only = false # Nice assumption bro.
} }
} }
@ -37,8 +39,8 @@ resource "docker_service" "instance" {
dynamic "mounts" { dynamic "mounts" {
for_each = var.mounts for_each = var.mounts
content { content {
target = mounts.value
source = mounts.key source = mounts.key
target = mounts.value
type = "bind" type = "bind"
read_only = false # Nice assumption bro. read_only = false # Nice assumption bro.
} }
@ -50,7 +52,7 @@ resource "docker_service" "instance" {
content { content {
config_id = module.config[configs.key].id config_id = module.config[configs.key].id
config_name = module.config[configs.key].name config_name = module.config[configs.key].name
file_name = configs.value file_name = configs.key
} }
} }
@ -83,27 +85,41 @@ resource "docker_service" "instance" {
# Apply the networks # Apply the networks
dynamic "networks_advanced" { dynamic "networks_advanced" {
for_each = var.networks for_each = data.docker_network.networks
content { content {
name = networks_advanced.value name = networks_advanced.value.id
} }
} }
# Apply restart policy # Apply restart policy
restart_policy { restart_policy {
condition = var.one_shot ? "none" : var.restart_policy condition = var.one_shot ? "none" : var.restart_policy
delay = "0s" delay = var.restart_delay
window = "0s" window = "0s"
max_attempts = 0 max_attempts = 0
} }
# Apply the placement constraints
placement { placement {
constraints = var.placement_constraints max_replicas = var.parallelism_per_node
constraints = var.placement_constraints
platforms { platforms {
architecture = var.processor_architecture architecture = var.processor_architecture
os = var.operating_system os = var.operating_system
} }
} }
# Apply the resource limits and reservations
resources {
limits {
memory_bytes = var.limit_ram_mb != null ? 1024 * 1024 * var.limit_ram_mb : 0
nano_cpus = var.limit_cpu != null ? (1000000000 / 100) * var.limit_cpu : 0
}
reservation {
memory_bytes = var.reserved_ram_mb != null ? 1024 * 1024 * var.reserved_ram_mb : 0
nano_cpus = var.reserved_cpu != null ? (1000000000 / 100) * var.reserved_cpu : 0
}
}
} }
# Global deploy # Global deploy

View file

@ -19,7 +19,8 @@ variable "service_name" {
description = "The name of the service to create." description = "The name of the service to create."
} }
variable "placement_constraints" { variable "placement_constraints" {
default = ["node.role == manager"] default = []
type = list(string) type = list(string)
description = "Docker Swarm placement constraints" description = "Docker Swarm placement constraints"
} }

View file

@ -0,0 +1,6 @@
output "docker_service" {
value = module.service.docker_service
}
output "network" {
value = module.network.network
}

View file

@ -1,14 +1,17 @@
module "network" { module "network" {
source = "../network" source = "../network"
name = "docker-socket-proxy" network_name = "docker-socket-proxy"
stack_name = var.stack_name stack_name = var.stack_name
} }
module "service" { module "service" {
source = "../service" source = "../service"
image = "${var.docker_socket_proxy_image}:${var.docker_socket_proxy_version}" image = "${var.docker_socket_proxy_image}:${var.docker_socket_proxy_version}"
command = ["/docker-entrypoint.sh", "sockd-username"] stack_name = var.stack_name
stack_name = var.stack_name service_name = var.service_name
service_name = var.service_name placement_constraints = concat(["node.role == manager"], var.placement_constraints)
global = true
networks = [module.network.network]
mounts = { "/var/run/docker.sock" = "/var/run/docker.sock" }
environment_variables = { environment_variables = {
SWARM = 1 SWARM = 1
SERVICES = 1 SERVICES = 1
@ -17,18 +20,4 @@ module "service" {
NODES = 1 NODES = 1
NETWORKS = 1 NETWORKS = 1
} }
placement_constraints = var.placement_constraints
global = true
networks = [module.network.network.id]
mounts = [
{
target = "/var/run/docker.sock"
source = "/var/run/docker.sock"
read_only = false
type = "bind"
}
]
} }
output "network" {
value = module.network.network
}

View file

@ -1,3 +1,6 @@
output "source" { output "source" {
value = docker_volume.volume.id value = docker_volume.volume.id
}
output "volume" {
value = docker_volume.volume
} }

View file

@ -13,9 +13,7 @@ module "forgejo_actions_runner" {
forgejo_RUNNER_REGISTRATION_TOKEN = var.forgejo_token forgejo_RUNNER_REGISTRATION_TOKEN = var.forgejo_token
CONFIG_FILE = "/config.yaml" CONFIG_FILE = "/config.yaml"
} }
mounts = { mounts = { "/var/run/docker.sock" = "/var/run/docker.sock" }
"/var/run/docker.sock" = "/var/run/docker.sock"
}
configs = { configs = {
forgejo-config = yamlencode({ forgejo-config = yamlencode({
name_prefix = ["forgejo-config", var.stack_name, var.service_name] name_prefix = ["forgejo-config", var.stack_name, var.service_name]

View file

@ -0,0 +1,55 @@
data "docker_registry_image" "frigate" {
name = "ghcr.io/blakeblackshear/frigate:stable"
}
resource "docker_container" "frigate" {
image = "${data.docker_registry_image.frigate.name}@${data.docker_registry_image.frigate.sha256_digest}"
name = local.container_name
restart = "unless-stopped"
privileged = "true"
shm_size = var.shm_size_mb
network_mode = "bridge"
env = [
"FRIGATE_RTSP_PASSWORD=${var.frigate_rtsp_password}"
]
dynamic "devices" {
for_each = var.devices
content {
host_path = devices.value.host_path
container_path = devices.value.container_path
permissions = devices.value.permissions
}
}
dynamic "volumes" {
for_each = var.volumes
content {
container_path = volumes.value
host_path = volumes.key
read_only = false
}
}
dynamic "ports" {
for_each = var.ports
content {
internal = ports.value.container
external = ports.value.host
protocol = ports.value.protocol
}
}
dynamic "networks_advanced" {
for_each = var.networks
content {
name = networks_advanced.value
}
}
dynamic "labels" {
for_each = local.labels
content {
label = labels.key
value = labels.value
}
}
lifecycle {
create_before_destroy = false
}
}

View file

@ -0,0 +1,82 @@
variable "stack_name" {
type = string
description = "The name of the stack to deploy the service to."
}
variable "shm_size_mb" {
default = 256
type = number
description = "The size of the shared memory segment in MB"
}
variable "networks" {
type = list(string)
default = []
description = "A list of network names to attach the service to."
}
variable "frigate_rtsp_password" {
type = string
description = "The password to use for the RTSP streams"
default = ""
}
variable "devices" {
type = list(object({
host_path = string
container_path = string
permissions = optional(string, "rwm")
}))
description = "The devices to mount into the container"
}
variable "volumes" {
type = map(string)
default = {}
description = "A map of volume names to create and mount. The key is the volume name, and the value is the mount point."
}
variable "ports" {
type = list(object({
host = number
container = number
protocol = optional(string, "tcp")
}))
default = [
{
container = 5000
host = 5000
protocol = "tcp"
},
{
container = 1935
host = 1935
protocol = "tcp"
},
{
container = 8554
host = 8554
protocol = "tcp"
},
{
container = 8555
host = 8555
protocol = "tcp"
},
{
container = 8555
host = 8555
protocol = "udp"
}
]
}
variable "traefik" {
default = null
type = object({
domain = string
port = optional(number, 5000)
})
description = "Whether to enable traefik for the service."
}
variable "labels" {
type = map(string)
default = {}
description = "A map of labels to apply to the service"
}

View file

@ -0,0 +1,24 @@
locals {
container_name = "frigate"
# Define service labels en-masse
labels = merge({
"com.docker.stack.namespace" = var.stack_name
"com.docker.stack.image" = data.docker_registry_image.frigate.name
"ooo.grey.service.stack" = var.stack_name
"ooo.grey.service.name" = local.container_name
"ooo.grey.service.image" = data.docker_registry_image.frigate.name
"ooo.grey.service.image.digest" = data.docker_registry_image.frigate.sha256_digest
}, local.traefik_labels, var.labels)
# Calculate the traefik labels to use if enabled
traefik_labels = var.traefik != null ? {
"traefik.enable" = "true"
"traefik.http.routers.${local.container_name}.rule" = "Host(`${var.traefik.domain}`)"
"traefik.http.routers.${local.container_name}.entrypoints" = "websecure"
"traefik.http.routers.${local.container_name}.tls.certresolver" = "default"
"traefik.http.services.${local.container_name}.loadbalancer.server.port" = 5000
} : {
"traefik.enable" = "false"
}
}

View file

@ -0,0 +1,3 @@
output "endpoint" {
value = try("https://${var.traefik.domain}", "unknown")
}

View file

@ -0,0 +1,11 @@
terraform {
required_version = "~> 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View file

@ -12,9 +12,7 @@ module "gitea_actions_runner" {
GITEA_RUNNER_REGISTRATION_TOKEN = var.gitea_token GITEA_RUNNER_REGISTRATION_TOKEN = var.gitea_token
CONFIG_FILE = "/config.yaml" CONFIG_FILE = "/config.yaml"
} }
mounts = { mounts = { "/var/run/docker.sock" = "/var/run/docker.sock" }
"/var/run/docker.sock" = "/var/run/docker.sock"
}
configs = { configs = {
gitea-config = { gitea-config = {
name_prefix = ["gitea-config", var.stack_name, var.service_name] name_prefix = ["gitea-config", var.stack_name, var.service_name]

View file

@ -15,7 +15,5 @@ module "github_actions_runner" {
EPHEMERAL = true EPHEMERAL = true
DISABLE_AUTO_UPDATE = "disable_updates" DISABLE_AUTO_UPDATE = "disable_updates"
} }
mounts = { mounts = { "/var/run/docker.sock" = "/var/run/docker.sock" }
"/var/run/docker.sock" = "/var/run/docker.sock"
}
} }

View file

@ -0,0 +1,13 @@
module "homeassistant" {
source = "../../docker/service"
stack_name = var.stack_name
service_name = "homeassistant"
image = var.default_image
environment_variables = merge({ TZ = "Europe/London" }, var.environment_variables)
mounts = var.mounts
networks = var.networks
placement_constraints = var.placement_constraints
ports = [{ host = 8123, container = 8123 }]
traefik = var.traefik
converge_timeout = "5m"
}

View file

@ -0,0 +1,35 @@
variable "stack_name" {
default = "homeassistant"
type = string
description = "The name of the stack to create."
}
variable "default_image" {
default = "ghcr.io/home-assistant/home-assistant:stable"
type = string
description = "The image to use for the homeassistant service"
}
variable "environment_variables" {
type = map(string)
default = {}
description = "A map of environment variables to set in the container."
}
variable "mounts" {
type = map(string)
default = {}
description = "A map of host paths to container paths to mount. The key is the host path, and the value is the container path."
}
variable "networks" {
type = list(string)
default = []
description = "A list of network names to attach the service to."
}
variable "placement_constraints" {
default = []
type = list(string)
description = "Docker Swarm placement constraints"
}
variable "traefik" {
}

View file

@ -0,0 +1,3 @@
output "docker_service" {
value = module.homeassistant.docker_service
}

View file

@ -0,0 +1,15 @@
terraform {
required_version = "~> 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}

24
products/minio/inputs.tf Normal file
View file

@ -0,0 +1,24 @@
variable "stack_name" {
default = "mitmproxy"
type = string
description = "The name of the stack to create."
}
variable "networks" {
type = list(string)
default = []
description = "A list of network names to attach the service to."
}
variable "traefik" {
default = null
type = object({
domain = string
port = optional(number)
})
description = "Whether to enable traefik for the service."
}
variable "placement_constraints" {
default = []
type = list(string)
description = "Docker Swarm placement constraints"
}

64
products/minio/minio.tf Normal file
View file

@ -0,0 +1,64 @@
resource "random_pet" "minio_admin_user" {
length = 2
separator = ""
}
resource "random_password" "minio_admin_password" {
length = 32
special = false
}
variable "domain" {
type = string
description = "The domain to use for the service."
}
variable "mounts" {
type = map(string)
default = {}
description = "A map of host paths to container paths to mount. The key is the host path, and the value is the container path."
}
module "minio" {
source = "../../docker/service"
stack_name = "minio"
service_name = "minio"
image = "quay.io/minio/minio:latest"
command = ["minio", "server", "/data", ]
environment_variables = {
MINIO_ADDRESS = "0.0.0.0:9000"
MINIO_CONSOLE_ADDRESS = "0.0.0.0:9001"
MINIO_ROOT_USER = random_pet.minio_admin_user.id
MINIO_ROOT_PASSWORD = random_password.minio_admin_password.result
MINIO_SERVER_URL = "https://s3.grey.ooo"
MINIO_BROWSER_REDIRECT_URL = "https://s3.grey.ooo/ui/"
MINIO_BROWSER_REDIRECT = true
MINIO_API_ROOT_ACCESS = "on"
}
mounts = var.mounts
networks = concat(["loadbalancer-traefik"], var.networks)
placement_constraints = var.placement_constraints
labels = {
"traefik.enable" = "true"
// API redirect
"traefik.http.routers.minio_api.rule" = "Host(`${var.domain}`) && !PathPrefix(`/ui`)"
#"traefik.http.routers.minio_api.service" = "minio_api"
"traefik.http.routers.minio_api.entrypoints" = "websecure"
"traefik.http.routers.minio_api.tls.certresolver" = "default"
"traefik.http.services.minio_api.loadbalancer.server.port" = "9000"
// UI redirect
"traefik.http.routers.minio_ui.rule" = "Host(`${var.domain}`) && PathPrefix(`/ui`)"
#"traefik.http.routers.minio_ui.service" = "minio_ui"
"traefik.http.routers.minio_ui.entrypoints" = "websecure"
"traefik.http.routers.minio_ui.tls.certresolver" = "default"
"traefik.http.services.minio_ui.loadbalancer.server.port" = "9001"
}
}
output "minio" {
value = {
endpoint = "https://${var.domain}/ui/"
auth = {
username = module.minio.docker_service.task_spec[0].container_spec[0].env.MINIO_ROOT_USER
password = nonsensitive(module.minio.docker_service.task_spec[0].container_spec[0].env.MINIO_ROOT_PASSWORD)
}
}
}

View file

@ -0,0 +1,3 @@
output "docker_service" {
value = module.minio.docker_service
}

View file

@ -0,0 +1,15 @@
terraform {
required_version = "~> 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}

View file

@ -0,0 +1,25 @@
variable "stack_name" {
default = "mitmproxy"
type = string
description = "The name of the stack to create."
}
variable "networks" {
type = list(string)
default = []
description = "A list of network names to attach the service to."
}
variable "traefik" {
default = null
type = object({
domain = string
port = optional(number, 8081)
})
description = "Whether to enable traefik for the service."
}
variable "placement_constraints" {
default = []
type = list(string)
description = "Docker Swarm placement constraints"
}

View file

@ -0,0 +1,30 @@
module "mitmproxy" {
source = "../../docker/service"
stack_name = var.stack_name
service_name = "mitmproxy"
networks = var.networks
image = "ghcr.io/benzine-framework/mitmproxy"
command = [
"mitmweb",
"--web-host", "0.0.0.0",
"--web-port", "8081",
#"--listen-host", "0.0.0.0",
#"--listen-port", "8080",
#"--ssl-insecure",
]
healthcheck = ["CMD-SHELL", " curl -I http://localhost:8081 || exit 1"]
placement_constraints = var.placement_constraints
traefik = var.traefik
ports = [
{
protocol = "tcp"
container = 8080
host = 4080
},
{
protocol = "tcp"
container = 8081
host = 4081
}
]
}

View file

@ -0,0 +1,3 @@
output "docker_service" {
value = module.mitmproxy.docker_service
}

View file

@ -0,0 +1,11 @@
terraform {
required_version = "~> 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
}
}

View file

@ -26,4 +26,8 @@ variable "placement_constraints" {
variable "networks" { variable "networks" {
type = list(string) type = list(string)
default = [] default = []
}
variable "domain" {
type = string
description = "The domain to use for the service's traefik configuration."
} }

View file

@ -17,6 +17,7 @@ module "pgbackweb" {
service_name = var.service_name service_name = var.service_name
networks = concat([module.network.network], var.networks) networks = concat([module.network.network], var.networks)
placement_constraints = var.placement_constraints placement_constraints = var.placement_constraints
traefik = { domain = var.domain }
} }
module "postgres" { module "postgres" {
source = "../postgres" source = "../postgres"

View file

@ -1,16 +1,33 @@
variable "docker" { variable "stack_name" {
type = object({ default = "loadbalancer"
name = string type = string
stack_name = optional(string) description = "The name of the stack to create."
networks = list(object({
name = string
id = string
}))
})
} }
variable "portainer" { variable "traefik" {
default = null
type = object({ type = object({
version = string domain = string
logo = optional(string) port = optional(number, 9000)
}) })
description = "Whether to enable traefik for the service."
} }
variable "placement_constraints" {
default = []
type = list(string)
description = "Docker Swarm placement constraints"
}
variable "portainer_version" {
default = "sts"
type = string
description = "The version of the portainer image to use."
}
variable "portainer_logo" {
default = null
type = string
description = "The URL of the logo to use for the portainer service."
}
variable "should_mount_local_docker_socket" {
type = bool
default = false
}

View file

@ -4,6 +4,6 @@ output "portainer" {
username = "admin" # Sorry, this is hardcoded in the portainer image username = "admin" # Sorry, this is hardcoded in the portainer image
password = nonsensitive(random_password.password.result) password = nonsensitive(random_password.password.result)
} }
service_name = docker_service.portainer.name service_name = module.portainer.docker_service.name
} }
} }

View file

@ -8,99 +8,31 @@ resource "htpasswd_password" "hash" {
password = random_password.password.result password = random_password.password.result
salt = random_password.salt.result salt = random_password.salt.result
} }
data "docker_registry_image" "portainer_app" { module "vol_portainer" {
name = "portainer/portainer-ce:${var.portainer.version}" source = "../../../docker/volume"
stack_name = var.stack_name
volume_name = "portainer"
} }
resource "docker_volume" "portainer" { module "portainer" {
name = var.docker.name source = "../../../docker/service"
} stack_name = var.stack_name
resource "docker_service" "portainer" { service_name = "portainer"
name = var.docker.name image = "portainer/portainer-ce:${var.portainer_version}"
mode { command = [
replicated { "/portainer",
replicas = 1 //"--edge-compute",
} "--logo", coalesce(var.portainer_logo),
} "--admin-password", htpasswd_password.hash.bcrypt,
task_spec { ]
container_spec { remote_volumes = {
image = "${data.docker_registry_image.portainer_app.name}@${data.docker_registry_image.portainer_app.sha256_digest}" "/data" = module.vol_portainer.volume
command = [ }
"/portainer", traefik = var.traefik
//"--edge-compute", mounts = var.should_mount_local_docker_socket ? { "/var/run/docker.sock" = "/var/run/docker.sock" } : {}
"--logo", coalesce(var.portainer.logo), networks = ["loadbalancer-traefik"]
"--admin-password", htpasswd_password.hash.bcrypt, start_first = false
] placement_constraints = concat([
#mounts { "node.role == manager",
# target = "/data" "node.platform.os == linux",
# source = "/portainer" ], var.placement_constraints)
# read_only = false
# type = "bind"
#}
mounts {
target = "/data"
source = docker_volume.portainer.name
type = "volume"
read_only = false
}
#mounts {
# target = "/var/run/docker.sock"
# source = "/var/run/docker.sock"
# read_only = false
# type = "bind"
#}
labels {
label = "com.docker.stack.namespace"
value = var.docker.stack_name
}
}
dynamic "networks_advanced" {
for_each = var.docker.networks
content {
name = networks_advanced.value.id
}
}
restart_policy {
condition = "on-failure"
delay = "3s"
max_attempts = 4
window = "10s"
}
placement {
constraints = [
"node.role == manager",
"node.platform.os == linux",
]
}
}
#endpoint_spec {
# ports {
# target_port = 9000
# publish_mode = "ingress"
# published_port = 9000
# }
# ports {
# target_port = 8000
# publish_mode = "ingress"
# published_port = 8000
# }
#}
update_config {
# Portainer gets super fuckin' upset if you start a second instance while the first is holding the db lock
order = "stop-first"
}
labels {
label = "com.docker.stack.namespace"
value = var.docker.stack_name
}
labels {
label = "com.docker.stack.image"
value = replace(data.docker_registry_image.portainer_app.name, "/:.*/", "")
}
lifecycle {
ignore_changes = [
# MB: This is a hack because terraform keeps detecting a "change" in the placement->platform constraint that doesn't exist.
task_spec[0].placement[0].platforms
]
}
} }

View file

@ -0,0 +1,18 @@
variable "timezone" {
type = string
description = "The timezone to use for the service."
default = "Europe/London"
}
variable "traefik" {
default = null
type = object({
domain = string
port = optional(number)
})
description = "Whether to enable traefik for the service."
}
variable "placement_constraints" {
default = []
type = list(string)
description = "Docker Swarm placement constraints"
}

View file

@ -0,0 +1,3 @@
output "docker_service" {
value = module.smokeping.docker_service
}

View file

@ -0,0 +1,15 @@
module "smokeping" {
source = "../../docker/service"
stack_name = "smokeping"
service_name = "smokeping"
image = "linuxserver/smokeping:latest"
volumes = { "smokeping" = "/data" }
environment_variables = {
PUID = 1000
PGID = 1000
TZ = var.timezone
}
traefik = var.traefik
networks = ["loadbalancer-traefik"]
placement_constraints = var.placement_constraints
}

View file

@ -0,0 +1,16 @@
terraform {
required_version = "~> 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}

View file

@ -42,3 +42,11 @@ variable "extra_environment_variables" {
default = {} default = {}
description = "Extra environment variables to pass to the service." description = "Extra environment variables to pass to the service."
} }
variable "traefik" {
default = null
type = object({
domain = string
port = optional(number)
})
description = "Whether to enable traefik for the service."
}

View file

@ -1,24 +0,0 @@
variable "nginx_hostname" {
type = string
default = null
}
variable "acme_certificate" {
type = object({
private_key_pem = string
certificate_pem = string
issuer_pem = string
})
default = null
}
module "nginx_config" {
count = var.nginx_hostname != null ? 1 : 0
source = "../nginx/site-available"
service_name = module.service.service_name
hostname = var.nginx_hostname
upstream_host = "${module.service.service_name}:8080"
config_prefix = module.service.service_name
certificate = var.acme_certificate
}
output "nginx_files" {
value = var.nginx_hostname != null ? module.nginx_config[0].files : []
}

View file

@ -7,7 +7,7 @@ output "statping" {
port = module.postgres.ports[0] port = module.postgres.ports[0]
} }
statping = { statping = {
instance = var.nginx_hostname != null ? "https://${var.nginx_hostname}" : null instance = try("https://${var.traefik.domain}", "unknown")
} }
} }
} }

View file

@ -15,7 +15,7 @@ module "service" {
image = "${var.statping_image}:${var.statping_version}" image = "${var.statping_image}:${var.statping_version}"
stack_name = var.stack_name stack_name = var.stack_name
service_name = "statping" service_name = "statping"
networks = concat([module.network.network, ], var.networks) networks = concat([module.network.network, "loadbalancer-traefik"], var.networks)
environment_variables = merge({ environment_variables = merge({
VIRTUAL_HOST = "localhost" VIRTUAL_HOST = "localhost"
VIRTUAL_PORT = "8080" VIRTUAL_PORT = "8080"
@ -29,4 +29,5 @@ module "service" {
}, var.extra_environment_variables) }, var.extra_environment_variables)
placement_constraints = var.placement_constraints placement_constraints = var.placement_constraints
dns_nameservers = var.dns_nameservers dns_nameservers = var.dns_nameservers
traefik = var.traefik
} }

View file

@ -0,0 +1,5 @@
module "docker_socket_proxy" {
source = "../../docker/socket-proxy"
stack_name = var.stack_name
placement_constraints = var.placement_constraints
}

14
products/traefik/hello.tf Normal file
View file

@ -0,0 +1,14 @@
module "traefik_hello" {
count = var.hello_service_domain != null ? 1 : 0
source = "../../docker/service"
stack_name = var.stack_name
service_name = "hello"
image = "traefik/whoami"
parallelism = 3
placement_constraints = var.placement_constraints
networks = [module.traefik_network.network, ]
traefik = {
domain = var.hello_service_domain
port = 80
}
}

View file

@ -0,0 +1,43 @@
variable "stack_name" {
default = "loadbalancer"
type = string
description = "The name of the stack to create."
}
variable "placement_constraints" {
default = []
type = list(string)
description = "Docker Swarm placement constraints"
}
variable "acme_use_staging" {
type = bool
default = false
description = "Whether to use the Let's Encrypt staging server."
}
variable "acme_email" {
description = "The email address to use for the ACME certificate."
type = string
}
variable "traefik_service_domain" {
type = string
default = null
}
variable "hello_service_domain" {
type = string
default = null
}
variable "log_level" {
type = string
default = "INFO"
description = "The log level to use for traefik."
}
variable "access_log" {
type = bool
default = false
description = "Whether to enable access logging."
}
variable "redirect_to_ssl" {
type = bool
default = true
description = "Whether to redirect HTTP to HTTPS."
}

View file

@ -0,0 +1,6 @@
module "traefik_network" {
source = "../../docker/network"
stack_name = var.stack_name
network_name = "traefik"
subnet = "172.16.0.0/22"
}

View file

@ -0,0 +1,6 @@
output "docker_service" {
value = module.traefik.docker_service
}
output "docker_network" {
value = module.traefik_network
}

View file

@ -0,0 +1,13 @@
terraform {
required_version = "~> 1.6"
required_providers {
docker = {
source = "kreuzwerker/docker"
version = "~>3.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.5"
}
}
}

View file

@ -0,0 +1,69 @@
module "traefik_certs_volume" {
source = "../../docker/volume"
stack_name = var.stack_name
volume_name = "traefik_certs"
}
module "traefik" {
source = "../../docker/service"
depends_on = [module.docker_socket_proxy]
stack_name = var.stack_name
service_name = "traefik"
image = "traefik:v3.2"
networks = [module.traefik_network.network, module.docker_socket_proxy.network, ]
mounts = { "/goliath/letsencrypt" = "/certs" }
placement_constraints = var.placement_constraints
converge_enable = false // @todo add healthcheck
command = [
"/usr/local/bin/traefik",
"--api.insecure=true",
"--api.dashboard=true",
"--log.level=${var.log_level}",
"--accesslog=${var.access_log ? "true" : "false"}",
"--ping=true",
# Confirm Docker Provider
"--providers.docker=true",
"--providers.docker.exposedbydefault=false",
"--providers.docker.network=${module.traefik_network.name}",
"--providers.docker.endpoint=http://${module.docker_socket_proxy.docker_service.name}:2375",
# Confirm Swarm Provider
"--providers.swarm=true",
"--providers.swarm.exposedByDefault=false",
"--providers.swarm.network=${module.traefik_network.name}",
"--providers.swarm.endpoint=http://${module.docker_socket_proxy.docker_service.name}:2375",
# Configure HTTP and redirect to HTTPS
"--entrypoints.web.address=:80",
# Configure HTTPS
"--entrypoints.websecure.address=:443",
var.redirect_to_ssl ? "--entrypoints.web.http.redirections.entrypoint.to=websecure" : "",
var.redirect_to_ssl ? "--entrypoints.web.http.redirections.entrypoint.scheme=https" : "",
# Configure the acme provider
"--certificatesresolvers.default.acme.tlschallenge=true",
var.acme_use_staging ? "--certificatesresolvers.default.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory" : "",
"--certificatesresolvers.default.acme.email=${var.acme_email}",
"--certificatesresolvers.default.acme.storage=/certs/acme.json",
]
traefik = var.traefik_service_domain != null ? {
domain = var.traefik_service_domain
port = 8080
} : null
ports = [
{
host = 80
container = 80
},
{
host = 443
container = 443
},
{
host = 8080
container = 8080
},
]
}

View file

@ -0,0 +1,3 @@
output "docker_service" {
value = module.watchtower.docker_service
}