homelab-nomad/services/service/service_template.nomad
Ian Fijolek f5898b0283 Add workload ACL management for mysql and postgres access
Allows required jobs to access shared secrets and auto generates psks
for stunnel.

Currently supporting MySQL, Postgres, and LDAP.
2023-08-29 12:48:48 -07:00

443 lines
12 KiB
HCL

job "${name}" {
region = "global"
datacenters = ["dc1"]
type = "service"
priority = ${priority}
group "${name}" {
count = ${count}
%{~ if length(job_meta) > 0 }
meta = {
%{ for k, v in job_meta ~}
${k} = ${jsonencode(v)}
%{ endfor ~}
}
%{~ endif ~}
network {
mode = "bridge"
%{~ if service_port != null }
port "main" {
%{~ if use_wesher ~}
host_network = "wesher"
%{~ endif ~}
%{~ if service_port_static ~}
static = ${service_port}
%{~ else ~}
to = ${service_port}
%{~ endif ~}
}
%{~ endif ~}
%{~ for port in ports }
port "${port.name}" {
%{ if port.host_network != null }host_network = "${port.host_network}"%{ endif ~}
%{ if port.from != null }to = ${port.from}%{ endif ~}
%{ if port.to != null }to = ${port.to}%{ endif ~}
%{ if port.static != null }static = ${port.static}%{ endif ~}
}
%{~ endfor ~}
}
%{~ for constraint in constraints }
constraint {
attribute = "${constraint.attribute}"
operator = "${constraint.operator}"
value = "${constraint.value}"
}
%{~ endfor ~}
%{~ if length(group_meta) > 0 }
meta = {
%{~ for k, v in group_meta ~}
${k} = ${jsonencode(v)}
%{~ endfor ~}
}
%{~ endif ~}
%{~ if sticky_disk }
ephemeral_disk {
migrate = true
sticky = true
}
%{~ endif ~}
%{~ for host_volume in host_volumes }
volume "${host_volume.name}" {
type = "host"
read_only = ${host_volume.read_only}
source = "${host_volume.name}"
}
%{~ endfor ~}
%{~ if service_port != null }
service {
name = "${replace(name, "_", "-")}"
provider = "nomad"
port = "main"
tags = [
%{~ if prometheus == true ~}
"prometheus.scrape",
%{~ endif ~}
%{~ if ingress ~}
"traefik.enable=true",
"traefik.http.routers.${name}.entryPoints=websecure",
%{~ if try(ingress_rule, null) != null ~}
"traefik.http.routers.${name}.rule=${ingress_rule}",
%{~ endif ~}
%{~ for middleware in ingress_middlewares ~}
"traefik.http.routers.${name}.middlewares=${middleware}",
%{~ endfor ~}
%{~ endif ~}
%{~ for tag in service_tags ~}
"${tag}",
%{~ endfor ~}
]
}
%{~ endif ~}
%{~ for custom_service in custom_services ~}
service {
name = "${custom_service.name}"
provider = "nomad"
port = "${custom_service.port}"
tags = ${jsonencode(custom_service.tags)}
}
%{~ endfor ~}
task "${name}" {
driver = "docker"
%{~ if length(task_meta) > 0 }
meta = {
%{ for k, v in task_meta ~}
${k} = ${jsonencode(v)}
%{ endfor ~}
}
%{~ endif ~}
config {
image = "${image}"
%{~if image_pull_timeout != null ~}
image_pull_timeout = "${image_pull_timeout}"
%{~ endif ~}
%{~ if service_port != null ~}
ports = ["main"]
%{~ endif ~}
%{~ if length(try(args, [])) > 0 ~}
args = ${jsonencode(args)}
%{~ endif ~}
%{~ if length(docker_devices) > 0 ~}
devices = [
%{~ for dev in docker_devices ~}
{
host_path = "${dev.host_path}"
container_path = "${dev.container_path}"
},
%{~ endfor ~}
]
%{~ endif ~}
%{~ for template in templates ~}
%{~ if template.mount && !template.env }
mount {
type = "bind"
target = "${template.dest}"
source = "${template.dest_prefix}/${template.dest}"
}
%{~ endif ~}
%{~ endfor ~}
}
%{~ if length(env) > 0 }
env = {
%{~ for k, v in env ~}
"${k}" = ${jsonencode(v)}
%{~ endfor ~}
}
%{~ endif ~}
%{~ for volume in host_volumes }
volume_mount {
volume = "${volume.name}"
destination = "${volume.dest}"
read_only = ${volume.read_only}
}
%{~ endfor ~}
%{~ for template in templates }
template {
data = <<EOF
${template.data}
EOF
destination = "${coalesce(template.dest_prefix, "$${NOMAD_TASK_DIR}")}/${template.dest}"
%{~ if template.left_delimiter != null }
left_delimiter = "${template.left_delimiter}"
%{~ endif ~}
%{~ if template.right_delimiter != null }
right_delimiter = "${template.right_delimiter}"
%{~ endif ~}
%{~ if template.change_mode != null }
change_mode = "${template.change_mode}"
%{~ endif ~}
%{~ if template.change_signal != null }
change_signal = "${template.change_signal}"
%{~ endif ~}
%{~ if template.env != null }
env = ${template.env}
%{~ endif ~}
}
%{~ endfor ~}
%{~ if resources != null }
resources {
cpu = ${resources.cpu}
memory = ${resources.memory}
%{~ if resources.memory_max != null ~}
memory_max = ${resources.memory_max}
%{~ endif ~}
}
%{~ endif ~}
}
%{~ if mysql_bootstrap != null }
task "mysql-bootstrap" {
driver = "docker"
lifecycle {
hook = "prestart"
sidecar = false
}
config {
image = "mariadb:10"
args = [
"/usr/bin/timeout",
"2m",
"/bin/bash",
"-c",
"until /usr/bin/mysql --defaults-extra-file=$${NOMAD_SECRETS_DIR}/my.cnf < $${NOMAD_SECRETS_DIR}/bootstrap.sql; do sleep 10; done",
]
}
template {
data = <<EOF
[client]
host=127.0.0.1
port=3306
user=root
# TODO: Use via lesser scoped access
{{ with nomadVar "secrets/mysql" -}}
password={{ .mysql_root_password }}
{{ end -}}
EOF
destination = "$${NOMAD_SECRETS_DIR}/my.cnf"
}
template {
data = <<EOF
{{ with nomadVar "nomad/jobs/${name}" -}}
{{ $db_name := .${mysql_bootstrap.db_name_key} }}
CREATE DATABASE IF NOT EXISTS `{{ .${mysql_bootstrap.db_name_key} }}`
CHARACTER SET = 'utf8mb4'
COLLATE = 'utf8mb4_unicode_ci';
CREATE USER IF NOT EXISTS '{{ .${mysql_bootstrap.db_user_key} }}'@'%'
IDENTIFIED BY '{{ .${mysql_bootstrap.db_pass_key} }}';
GRANT ALL ON `{{ .${mysql_bootstrap.db_name_key} }}`.*
TO '{{ .${mysql_bootstrap.db_user_key} }}'@'%';
%{ if mysql_bootstrap.add_ro ~}
{{ with nomadService "grafana" }}{{ with nomadVar "nomad/jobs" -}}
-- Grant grafana read_only user access to db
GRANT SELECT ON `{{ $db_name }}`.* to '{{ .db_user_ro }}'@'%';
{{ end }}{{ end }}
%{~ endif }
{{ else -}}
SELECT 'NOOP';
{{ end -}}
EOF
destination = "$${NOMAD_SECRETS_DIR}/bootstrap.sql"
}
resources {
cpu = 50
memory = 50
}
}
%{~ endif ~}
%{~ if postgres_bootstrap != null }
task "postgres-bootstrap" {
driver = "docker"
lifecycle {
hook = "prestart"
sidecar = false
}
config {
image = "postgres:14"
args = [
"/usr/bin/timeout",
"2m",
"/bin/bash",
"-c",
"until /bin/bash $${NOMAD_TASK_DIR}/bootstrap.sh; do sleep 10; done",
]
}
template {
data = <<EOF
%{ if length(postgres_bootstrap.databases) > 0 ~}
%{ for db_name in postgres_bootstrap.databases }
/usr/bin/createdb ${db_name}
%{ endfor ~}
%{ else ~}
{{ with nomadVar "nomad/jobs/${name}" }}/usr/bin/createdb {{ .${postgres_bootstrap.db_name_key} }}{{ end }}
%{ endif ~}
/usr/bin/psql -X -f $${NOMAD_SECRETS_DIR}/bootstrap.sql
EOF
destination = "$${NOMAD_TASK_DIR}/bootstrap.sh"
}
template {
data = <<EOF
PGHOSTADDR=127.0.0.1
PGPORT=5432
{{ with nomadVar "secrets/postgres" }}
PGUSER={{ .superuser }}
# TODO: Passfile?
PGPASSWORD={{ .superuser_pass }}
{{ end }}
EOF
destination = "$${NOMAD_SECRETS_DIR}/db.env"
env = true
}
template {
data = <<EOF
{{ with nomadVar "nomad/jobs/${name}" -}}
DO $$
BEGIN
CREATE ROLE {{ .${postgres_bootstrap.db_user_key} }} LOGIN PASSWORD '{{ .${postgres_bootstrap.db_pass_key} }}';
%{ if length(postgres_bootstrap.databases) > 0 ~}
%{ for db_name in postgres_bootstrap.databases }
GRANT ALL ON DATABASE "${db_name}" TO {{ .${postgres_bootstrap.db_user_key} }};
%{ endfor ~}
%{ else ~}
GRANT ALL ON DATABASE "{{ .${postgres_bootstrap.db_name_key} }}" TO {{ .${postgres_bootstrap.db_user_key} }};
%{ endif ~}
EXCEPTION WHEN duplicate_object THEN RAISE NOTICE '%, skipping', SQLERRM USING ERRCODE = SQLSTATE;
END
$$;
{{ end }}
EOF
destination = "$${NOMAD_SECRETS_DIR}/bootstrap.sql"
}
resources {
cpu = 50
memory = 50
}
}
%{~ endif ~}
%{~ if use_mysql || use_redis || use_ldap || use_postgres }
task "stunnel" {
driver = "docker"
lifecycle {
hook = "prestart"
sidecar = true
}
config {
image = "alpine:3.17"
args = ["/bin/sh", "$${NOMAD_TASK_DIR}/start.sh"]
}
resources {
cpu = ${stunnel_resources.cpu}
memory = ${stunnel_resources.memory}
%{~ if stunnel_resources.memory_max != null ~}
memory_max = ${stunnel_resources.memory_max}
%{~ endif ~}
}
template {
data = <<EOF
set -e
apk add stunnel
exec stunnel {{ env "NOMAD_TASK_DIR" }}/stunnel.conf
EOF
destination = "$${NOMAD_TASK_DIR}/start.sh"
}
template {
data = <<EOF
syslog = no
foreground = yes
delay = yes
%{ if use_mysql }
[mysql_client]
client = yes
accept = 127.0.0.1:3306
{{ range nomadService 1 (env "NOMAD_ALLOC_ID") "mysql-tls" }}
connect = {{ .Address }}:{{ .Port }}
{{ end }}
PSKsecrets = {{ env "NOMAD_SECRETS_DIR" }}/mysql_stunnel_psk.txt
%{ endif ~}
%{ if use_redis }
[redis_client]
client = yes
accept = 127.0.0.1:6379
{{ range nomadService 1 (env "NOMAD_ALLOC_ID") "redis-${name}" }}
connect = {{ .Address }}:{{ .Port }}
{{ end }}
PSKsecrets = {{ env "NOMAD_SECRETS_DIR" }}/redis_stunnel_psk.txt
%{ endif }
%{ if use_ldap }
[ldap_client]
client = yes
accept = 127.0.0.1:389
{{ range nomadService 1 (env "NOMAD_ALLOC_ID") "lldap-tls" }}
connect = {{ .Address }}:{{ .Port }}
{{ end }}
PSKsecrets = {{ env "NOMAD_SECRETS_DIR" }}/ldap_stunnel_psk.txt
%{ endif ~}
%{ if use_postgres ~}
[postgres_client]
client = yes
accept = 127.0.0.1:5432
{{ range nomadService 1 (env "NOMAD_ALLOC_ID") "postgres-tls" }}
connect = {{ .Address }}:{{ .Port }}
{{ end }}
PSKsecrets = {{ env "NOMAD_SECRETS_DIR" }}/postgres_stunnel_psk.txt
%{ endif ~}
EOF
destination = "$${NOMAD_TASK_DIR}/stunnel.conf"
}
%{~ if use_mysql }
template {
data = <<EOF
{{- with nomadVar "secrets/mysql/allowed_psks/${name}" }}{{ .psk }}{{ end -}}
EOF
destination = "$${NOMAD_SECRETS_DIR}/mysql_stunnel_psk.txt"
}
%{~ endif ~}
%{~ if use_redis }
template {
data = <<EOF
{{- with nomadVar "nomad/jobs/${name}/${name}/stunnel" }}{{ .redis_stunnel_psk }}{{ end -}}
EOF
destination = "$${NOMAD_SECRETS_DIR}/redis_stunnel_psk.txt"
}
%{~ endif ~}
%{~ if use_ldap }
template {
data = <<EOF
{{- with nomadVar "secrets/ldap/allowed_psks/${name}" }}{{ .psk }}{{ end -}}
EOF
destination = "$${NOMAD_SECRETS_DIR}/ldap_stunnel_psk.txt"
}
%{~ endif ~}
%{~ if use_postgres }
template {
data = <<EOF
{{- with nomadVar "secrets/postgres/allowed_psks/${name}" }}{{ .psk }}{{ end -}}
EOF
destination = "$${NOMAD_SECRETS_DIR}/postgres_stunnel_psk.txt"
}
%{~ endif ~}
}
%{~ endif ~}
}
}