job "cloudbeaver" { datacenters = ["dc1"] region = "global" group "cloudbeaver" { shutdown_delay = "6s" network { mode = "bridge" } volume "data" { source = "cloudbeaver-data" type = "csi" access_mode = "single-node-writer" attachment_mode = "file-system" } service { name = "cloudbeaver" port = 8978 meta { alloc = "${NOMAD_ALLOC_INDEX}" datacenter = "${NOMAD_DC}" group = "${NOMAD_GROUP_NAME}" job = "${NOMAD_JOB_NAME}" namespace = "${NOMAD_NAMESPACE}" node = "${node.unique.name}" region = "${NOMAD_REGION}" } connect { sidecar_service { proxy { upstreams { destination_name = "postgres" local_bind_port = 5432 # Work arround, see https://github.com/hashicorp/nomad/issues/18538 destination_type = "service" } } } sidecar_task { config { args = [ "-c", "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json", "-l", "${meta.connect.log_level}", "--concurrency", "${meta.connect.proxy_concurrency}", "--disable-hot-restart" ] } resources { cpu = 50 memory = 64 } } } tags = [ "traefik.enable=true", "traefik.http.routers.cloudbeaver.entrypoints=https", "traefik.http.routers.cloudbeaver.rule=Host(`cloudbeaver.example.org`)", "traefik.http.middlewares.csp-cloudbeaver.headers.contentsecuritypolicy=default-src 'self';font-src 'self' data:;img-src 'self' data:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.routers.cloudbeaver.middlewares=security-headers@file,rate-limit-std@file,forward-proto@file,inflight-std@file,hsts@file,compression@file,csp-cloudbeaver", ] } # wait for required services tp be ready before starting the main task task "wait-for" { driver = "docker" user = 1053 config { image = "danielberteaud/wait-for:24.3-1" readonly_rootfs = true pids_limit = 20 } lifecycle { hook = "prestart" } env { SERVICE_0 = "master.postgres.service.consul" } resources { cpu = 10 memory = 10 memory_max = 30 } } task "cloudbeaver" { driver = "docker" config { image = "danielberteaud/cloudbeaver:24.0.1-1" pids_limit = 100 readonly_rootfs = true mount { type = "tmpfs" target = "/tmp" tmpfs_options { size = 3000000 } } } vault { policies = ["cloudbeaver"] env = false disable_file = true change_mode = "noop" } env { CLOUDBEAVER_WEB_CONFIG = "/local/cloudbeaver.conf" } # Use a template block instead of env {} so we can fetch values from vault template { data = <<_EOT LANG=fr_FR.utf8 TZ=Europe/Paris _EOT destination = "secrets/.env" perms = 400 env = true } # Postgres DB config template { data = <<_EOT PGDATABASE=cloudbeaver PGHOST=127.0.0.1 PGPORT=5432 PGUSER={{ with secret "database/creds/cloudbeaver" }}{{ .Data.username }}{{ end }} PGPASSWORD={{ with secret "database/creds/cloudbeaver" }}{{ .Data.password }}{{ end }} _EOT destination = "secrets/.db.env" perms = 0400 env = true } # Main cloudbeaver configuration template { data = <<_EOT { "app": { "adminCredentialsSaveEnabled": false, "anonymousAccessAllowed": false, "anonymousUserTeam": "user", "authenticationEnabled": true, "defaultNavigatorSettings": { "hideFolders": false, "hideSchemas": false, "mergeEntities": false, "showOnlyEntities": false, "showSystemObjects": true, "showUtilityObjects": false }, "disabledDrivers": [ "sqlite:sqlite_jdbc", "h2:h2_embedded", "h2:h2_embedded_v2" ], "enabledAuthProviders": [ "local" ], "enabledDrivers": [], "forwardProxy": true, "plugins": {}, "publicCredentialsSaveEnabled": false, "resourceManagerEnabled": true, "resourceQuotas": { "dataExportFileSizeLimit": 1000000000, "resourceManagerFileSizeLimit": 500000, "sqlBinaryPreviewMaxLength": 261120, "sqlMaxRunningQueries": 100, "sqlResultSetMemoryLimit": 2000000, "sqlResultSetRowsLimit": 100000, "sqlTextPreviewMaxLength": 4096 }, "supportsCustomConnections": true }, "server": { "contentRoot": "web", "database": { "driver": "postgres-jdbc", "initialDataConfiguration": "/secrets/initial-data.conf", "password": "$${PGPASSWORD}", "pool": { "maxConnections": 100, "maxIdleConnections": 10, "minIdleConnections": 4, "validationQuery": "SELECT 1" }, "url": "jdbc:postgresql://$${PGHOST}:$${PGPORT}/$${PGDATABASE}", "user": "$${PGUSER}" }, "develMode": false, "driversLocation": "drivers", "expireSessionAfterPeriod": 600000, "productConfiguration": "conf/product.conf", "rootURI": "/", "serverHost": "127.0.0.1", "serverName": "cloudbeaver", "serverPort": 8978, "serverURL": "https://cloudbeaver.example.org", "serviceURI": "/api/", "sm": { "blockLoginPeriod": "$${CLOUDBEAVER_BLOCK_PERIOD:300}", "enableBruteForceProtection": true, "expiredAuthAttemptInfoTtl": 60, "maxFailedLogin": "$${CLOUDBEAVER_MAX_FAILED_LOGINS:5}", "minimumLoginTimeout": "$${CLOUDBEAVER_MINIMUM_LOGIN_TIMEOUT:1}", "passwordPolicy": { "minLength": "$${CLOUDBEAVER_POLICY_MIN_LENGTH:8}", "minNumberCount": "$${CLOUDBEAVER_POLICY_MIN_NUMBER_COUNT:1}", "minSymbolCount": "$${CLOUDBEAVER_POLICY_MIN_SYMBOL_COUNT:0}", "requireMixedCase": "$${CLOUDBEAVER_POLICY_REQUIRE_MIXED_CASE:true}" } }, "workspaceLocation": "/data" } } _EOT destination = "local/cloudbeaver.conf" } # Logback config template { data = <<_EOT %d{dd-MM-yyyy HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n _EOT destination = "local/logback.xml" } # Initial config (admin user and teams) template { data = <<_EOT { adminName: "cloudbeaver", adminPassword: "{{ with secret "kv/service/cloudbeaver" }}{{ .Data.data.initial_admin_pwd }}{{ end }}", teams: [ { subjectId: "admin", teamName: "Admin", description: "Administrative access. Has all permissions.", permissions: [ "admin" ] }, { subjectId: "user", teamName: "User", description: "All users, including anonymous.", permissions: [ ] } ] } _EOT destination = "secrets/initial-data.conf" uid = 100000 gid = 108978 perms = 440 } # Mount persistent volume in /data volume_mount { volume = "data" destination = "/data" } resources { cpu = 100 memory = 384 } } } }