diff --git a/README.md b/README.md index 479c706..eb0f657 100644 --- a/README.md +++ b/README.md @@ -1,335 +1,1184 @@ -# Terraform deploy +# Terraform Kubernetes Deployer -## variables +A Terraform module that deploys containerised applications on Kubernetes with a single, unified variable block. It handles namespaces, deployments, services, persistent volumes, databases, Redis, backups, secrets (via Bitwarden Secrets Manager), reverse-proxy nginx config, and more — all from one `kubernetes` map in your `tfvars` file. -### name -Define the name -***optional*** -(string) +--- -### image = string -Define the image tag +## How it works +Every application is declared as a key inside the `kubernetes` map. The key name is used as the default namespace, deployment name, and service selector. Terraform then automatically creates: -### pvc = -***optional*** -specifiy the pvc details -## name = string -Name of the PVC +- A **Kubernetes namespace** (unless you attach to an existing one with `connected_namespace`) +- A **Deployment** for the application +- A **Service** (if `service_port` is specified) +- **PersistentVolumeClaims** (if `pvc` is specified) +- A **database Deployment + Service + PVC + backup CronJob** (if `database` is specified) +- A **Redis Deployment + Service + PVC** (if `redis` is specified) +- **Kubernetes Secrets / ConfigMaps** imported from Bitwarden Secrets Manager (if `secret_import` / `config_map_import` is specified) +- An **nginx reverse-proxy config** entry (if `domain_name` is specified) +- Any additional raw Kubernetes manifests (if `extra_manifest` is specified) +--- -## pvc = optional(bool, false) -***PVC - Hostpath - Configmap shouldn't be specifiy at the same time*** -Boolean to specify if it is a PVC -[Example](https://gitea.tryton.geeraert.eu/pi-ux-ce/Terraform_kube#volume-as-pvc) -#### config_map = optional(bool, false) -Boolean to specify if it is a configmap -[Example](https://gitea.tryton.geeraert.eu/pi-ux-ce/Terraform_kube#volume-as-configmap) -#### host_path = optional(string) -Boolean to specify if it is a hostpath -[Example](https://gitea.tryton.geeraert.eu/pi-ux-ce/Terraform_kube#volume-as-hostpath) -#### pvc_size = optional(string) -#### mount_path = string -#### storage_class_name = optional(string, "nfs-storage-intelligent") -This is the storage class, by default it will use nfs-storage-intelligent but it can be changed to something more suitable -### environment = optional(map(object({ -### name = optional(string) -### value = optional(string) -### value_from = optional(object({ -### secret_key_ref = object({ -### name = string -### key = optional(string, null) -### replicas = optional(number, 1) -### namespace = optional(string) -### service_port = optional(number) -### service_name = optional(string) -### liveness_tcp_socket = optional(number) -### liveness_http_get = optional(string) -### liveness_http_get_path = optional(string) -### database = optional(map(string)) -### redis = optional(map(string)) -### enabled = optional(bool, true) -### secret_ref = optional(list(string)) -### config_map_ref = optional(list(string)) -### requirements = optional(map(string), { -### limits-cpu = "800m" -### limits-memory = "500Mi" -### requests-cpu = "200m" -### requests-memory = "200Mi" -### environment_variables = optional(map(string), { name = "value" }) -not in use anymore +## Prerequisites -### Name -Name of the Deployment -Not required , the object name will be used if not specified +- Terraform `> 0.8.0` +- Bitwarden Secrets Manager provider (`bitwarden/bitwarden-secrets` `0.5.4-pre`) +- A running Kubernetes cluster +- NFS storage classes: `nfs-storage-intelligent` (default) and `nfs-storage-fastaccess` (used for backups and fast-access volumes) +- MetalLB (required only when using `service_type = "LoadBalancer"`) +- A Stakater Reloader or equivalent (used for the nginx ConfigMap annotation) +--- +## Full variable reference +```hcl +kubernetes = { + = { + # --- Identity --- + name = optional(string) # Override deployment name (defaults to map key) + image = string # REQUIRED. Docker image + namespace = optional(string) # Override namespace (defaults to map key) + connected_namespace = optional(string, "") # Attach to an existing namespace instead of creating one + enabled = optional(bool, true) # Disable everything (deployments, PVCs, services, secrets) + replicas = optional(number, 1) # Number of pod replicas -## examples + # --- Networking --- + service_port = optional(list(number)) # Expose these ports via a ClusterIP (or other) service + service_type = optional(string, "ClusterIP") # "ClusterIP" | "LoadBalancer" | "NodePort" + service_name = optional(string) # Override service name + domain_name = optional(string) # Generate an nginx reverse-proxy config for this subdomain + dns_nameservers = optional(string) # Custom DNS server for the pod (e.g. "1.1.1.1") -### enabled + # --- Compute & Security --- + command = optional(list(string)) # Override container entrypoint + requirements = optional(map(string)) # CPU/memory limits & requests + security_context = optional(bool, false) # Run container as privileged + service_account_name = optional(string) # Attach a ServiceAccount to the pod -Disable the whole object , including PVC , secrets , deployments and services -If you just want to remove the pod , you should consider set the replica option to 0 + # --- Liveness probes --- + liveness_tcp_socket = optional(number) # TCP port to probe + liveness_http_get = optional(string) # HTTP port to probe + liveness_http_get_path = optional(string) # HTTP path (defaults to "/") -``` - enabled = false + # --- Environment --- + environment = optional(map(object({ # Per-variable env config (value or secret ref) + value = optional(string) + value_from = optional(object({ + secret_key_ref = object({ + name = string + key = optional(string, null) + }) + })) + }))) + + # --- Secrets & ConfigMaps --- + secret_ref = optional(list(string)) # Mount entire Kubernetes Secrets as env + config_map_ref = optional(list(string)) # Mount entire Kubernetes ConfigMaps as env + secret_import = optional(list(string)) # Pull Bitwarden secret IDs → create K8s Secrets + config_map_import = optional(list(string)) # Pull Bitwarden secret IDs → create K8s ConfigMaps + extra_manifest = optional(list(string)) # Paths to raw YAML manifests to apply + + # --- Volumes --- + pvc = optional(map(object({ + name = string + pvc = optional(bool, false) # Create/use a PVC + config_map = optional(bool, false) # Mount a ConfigMap as a volume + secret = optional(bool, false) # Mount a Secret as a volume + host_path = optional(string) # Mount a host path + pvc_size = optional(string) # PVC size (e.g. "2Gi") + storage_class_name = optional(string, "nfs-storage-intelligent") + annotations_fileType = optional(string) # PVC annotation + existing_pvc = optional(bool, false) # Reuse an existing PVC (skip creation) + mount_path = string # REQUIRED. Where to mount in container + sub_path = optional(string) # Sub-path inside the volume + }))) + + # --- Database (auto-provisions DB deployment, service, PVC, backup CronJob, Bitwarden secrets) --- + database = optional(map(string)) + # database keys: + # engine - "postgres" | "mysql" | "sqlserver" (default: postgres) + # image - DB container image (default: postgres:latest) + # port - DB port (default: 5432) + # mountPath - DB data mount path (default: /var/lib/postgresql/) + # username - DB username (default: map key) + # name - DB name (default: map key) + # host - External DB hostname (skips creating DB deployment) + # map_name - Custom env var name for DB name in the mapped secret + # map_host - Custom env var name for DB host in the mapped secret + # map_password - Custom env var name for DB password in the mapped secret + # map_username - Custom env var name for DB username in the mapped secret + # shunt - "true" to skip creating the DB deployment (use when the container IS the DB) + database_args = optional(list(string)) # Args passed to the DB container + database_command = optional(list(string)) # Command override for DB container + + # --- Redis (auto-provisions Redis deployment, service, PVC) --- + redis = optional(map(string)) + # redis keys: + # image - Redis image (default: docker.io/library/redis:7) + } +} ``` -### replicas +--- -Set the number of replicas , can be used tyo keep the ressources and only remove the pods +## Feature guide -``` -replicas = 0 -``` +### 1. Minimal deployment +The smallest possible entry — just an image. Terraform will create a namespace called `myapp`, a deployment, and nothing else. -### namespace - -Specify a different namespace , by default it will take the name of the object otherwise - -``` - namespace = "testnginx" -``` - -### image - -Specify the image to use , mandatory - -``` +```hcl +kubernetes = { + myapp = { image = "nginx:latest" + } +} ``` -### Database +--- -Specifying the object (even empty) will automatically create all the objects related to the database (service,deployment,...) +### 2. Enabling and disabling an application -``` - database = {} -``` -#### Custom images version +Use `enabled = false` to destroy all resources for an application while keeping it in the tfvars file for reference. To keep the namespace, PVCs, and secrets but simply stop the pods, set `replicas = 0` instead. -You can select a custom image, by default it will use postgresql , if you do change the image , check if teh port is the same , otgherwise it will use postgresql defaut port - -``` - database = { - "image" = "lscr.io/linuxserver/mariadb" - - } +**Minimal — disable everything:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + enabled = false + } +} ``` -#### Custom database port - -You can select a custom database port , by default it will use postgresql , remember to update the secret for that -The database service will be automatically updated to reflect this port as well - -``` - database = { - "image" = "lscr.io/linuxserver/mariadb" - "port" = 3306 - - } +**Stop pods only (keep storage):** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + replicas = 0 + } +} ``` -#### Custom mountpath +--- -You can select a custom mount path, by default it will use postgresql data folder (/var/lib/postgresql/data) +### 3. Service / networking -``` - database = { - "mountPath" = "/config" - } -``` -### redis +Add `service_port` to expose the application via a Kubernetes Service. Multiple ports are supported. The default service type is `ClusterIP`. -Specifying the object (even empty) will automatically create all the objects related to Redis (service,deployment,...) - -``` - redis = {} -``` -#### Custom images version - -You can select a custom image, by default it will use redis version 7 (docker.io/library/redis:7) - -``` -redis = { - image = "docker.io/library/redis:7" - } +**Minimal — single port:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + service_port = [80] + } +} ``` - - - -### Extra manifest - -You can add some extra manifest using this command -``` - extra_manifest = ["/opt/kubernetes/Bookstack_kube/Bookstack.configmap.yaml","/opt/kubernetes/Bookstack_kube/Bookstack.secret.yaml"] +**Full — multiple ports, LoadBalancer with MetalLB:** +```hcl +kubernetes = { + myapp = { + image = "analogic/poste.io:latest" + service_port = [25, 80, 110, 143, 443, 465, 587, 993] + service_type = "LoadBalancer" + } +} ``` -### environment +> When `service_type = "LoadBalancer"`, the service is automatically annotated with `metallb.universe.tf/ip-allocated-from-pool: first-pool`. -#### Simple environment variables +--- -You can specifiy a variable through the environment object +### 4. Namespace management + +By default, Terraform creates a namespace matching the map key. Use `namespace` to give it a different name, or `connected_namespace` to deploy into an existing namespace without creating a new one. + +**Minimal — custom namespace name:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + namespace = "webservers" + } +} ``` + +**Full — attach to an existing namespace (no new namespace created):** +```hcl +kubernetes = { + celery-worker = { + image = "myrepo/django:latest" + connected_namespace = "django" + command = ["celery", "-A", "core", "worker"] + secret_ref = ["django-bitwarden-credentials"] + } +} +``` + +> `connected_namespace` is useful for sidecars or worker processes that share the namespace of a primary application. + +--- + +### 5. Environment variables + +Two styles are supported: a direct `value`, or a reference to a key inside a Kubernetes Secret (`value_from`). + +**Minimal — direct value:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" environment = { - PORT = { - value = "80" + NGINX_PORT = { value = "80" } + } + } +} +``` + +**Full — mix of direct values and secret references:** +```hcl +kubernetes = { + paperless = { + image = "ghcr.io/paperless-ngx/paperless-ngx:2.18.4" + service_port = [8000] + environment = { + PAPERLESS_URL = { + value = "https://paperless.example.com" } - } -``` -#### environment_variables from a secret - -``` - environment = { - MYSQL_ROOT_PASSWORD = { + PAPERLESS_TIKA_ENABLED = { + value = "1" + } + PAPERLESS_REDIS = { + value = "redis://paperless-redis-svc:6379" + } + POSTGRES_USER = { value_from = { secret_key_ref = { - name = "bookstack-credentials" - key = "DB_PASS" + name = "paperless-credentials" + key = "POSTGRES_USER" + } + } + } + POSTGRES_PASSWORD = { + value_from = { + secret_key_ref = { + name = "paperless-credentials" + key = "POSTGRES_PASSWORD" } } } } + } +} ``` -### Secret reference - -You can have your secret fully mounted instead of specifiying all the variables - secret_ref = ["secret1"] - -### Volumes -#### Volume as PVC - -``` - pvc = { - paperless-data = { - pvc = true - name = "paperless-data" - pvc_size = "2Mi" - mount_path = "/usr/src/paperless/data" - } - } - -``` -#### Volume as Hostpath - -``` - pvc = { - export = { - # pvc = true - host_path = "/tmp" - name = "export" - pvc_size = "2Mi" - mount_path = "/usr/src/paperless/export" - } - } - -``` -#### Volume as configmap - -``` - pvc = { - media = { - pvc = false - config_map = true - name = "gitea-runner-configfile" - pvc_size = "2Mi" - mount_path = "/tmp/usr/src/paperless/media" - } - - } - -``` - - -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -### [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -### [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin https://gitea.tryton.geeraert.eu/pi-ux-ce/Bitwarden_kube.git -git branch -M main -git push -uf origin main -``` - -## Integrate with your tools - -### [ ] [Set up project integrations](https://gitlab.tryton.geeraert.eu/pierre/bitwarden_kube/-/settings/integrations) - -## Collaborate with your team - -### [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -### [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -### [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -### [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -### [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) - -## Test and Deploy - -Use the built-in continuous integration in GitLab. - -### [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -### [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -### [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -### [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -### [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) - --- -# Editing this README +### 6. Secrets and ConfigMaps as environment -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want ### this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +Mount an entire Kubernetes Secret or ConfigMap as environment variables (all keys become env vars). -## Suggestions for a good README +**Minimal — mount a single secret:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + secret_ref = ["my-app-credentials"] + } +} +``` -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +**Full — multiple secrets and configmaps:** +```hcl +kubernetes = { + bookstack = { + image = "linuxserver/bookstack:latest" + service_port = [80, 443] + secret_ref = ["bookstack-credentials", "bookstack-extra-secret"] + config_map_ref = ["bookstack-config", "global-config"] + database = { + engine = "mysql" + image = "lscr.io/linuxserver/mariadb" + port = 3306 + } + } +} +``` -## Name -Choose a self-explaining name for your project. +--- -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +### 7. Importing secrets and ConfigMaps from Bitwarden Secrets Manager -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +Use `secret_import` or `config_map_import` with Bitwarden secret UUIDs. Terraform fetches the secret from Bitwarden Secrets Manager and creates the corresponding Kubernetes resource. The Bitwarden secret's `note` field becomes the Kubernetes resource name, the `key` field (split by `__`) provides the namespace, and the `value` field contains YAML-encoded key-value pairs. -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +**Minimal — import one secret:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + secret_import = ["9fc5a725-75f5-4bab-b595-b474017146c3"] + } +} +``` -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +**Full — import multiple secrets and configmaps:** +```hcl +kubernetes = { + fail2ban = { + image = "gitea.example.com/myorg/fail2ban:latest" + config_map_import = [ + "9d8ef71a-de74-478b-aa86-b3ee0160dc43", + "50814edb-d190-41cb-8950-b3ee01622dcf", + ] + secret_import = ["d1c2b8c9-71a0-4e47-9d9d-b402018376b6"] + environment = { + PUID = { value = "1000" } + TZ = { value = "Etc/UTC" } + } + } +} +``` -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +--- -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +### 8. Extra raw Kubernetes manifests -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +Apply additional Kubernetes YAML files alongside the Terraform-managed resources. Useful for resources Terraform doesn't manage natively (custom ServiceAccounts, Roles, CronJobs, etc.). -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + extra_manifest = ["/opt/kubernetes/myapp/secret.yaml"] + } +} +``` -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +**Full — multiple manifests:** +```hcl +kubernetes = { + fail2ban = { + image = "gitea.example.com/myorg/fail2ban:latest" + extra_manifest = [ + "/opt/kubernetes/fail2ban/fail2ban.RoleBinding.yaml", + "/opt/kubernetes/fail2ban/fail2ban.Role.yaml", + "/opt/kubernetes/fail2ban/fail2ban.ServiceAccount.yaml", + ] + service_account_name = "kubectl-service-account" + security_context = true + } +} +``` -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +--- -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +### 9. Volumes -## License -For open source projects, say how it is licensed. +The `pvc` block supports four volume types. You can declare multiple volumes per application. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. +#### 9a. PVC (Persistent Volume Claim) + +Terraform creates the PVC automatically. Default storage class is `nfs-storage-intelligent`. + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + pvc = { + data = { + name = "myapp-data" + pvc = true + pvc_size = "5Gi" + mount_path = "/data" + } + } + } +} +``` + +**Full — multiple PVCs with custom storage class, sub-path, and annotation:** +```hcl +kubernetes = { + stash = { + image = "stashapp/stash:v0.30.1" + service_port = [9999] + pvc = { + data = { + name = "stash-data" + pvc = true + pvc_size = "500Gi" + mount_path = "/data" + storage_class_name = "nfs-storage-fastaccess" + annotations_fileType = "Video" + } + metadata = { + name = "stash-metadata" + pvc = true + pvc_size = "10Gi" + mount_path = "/metadata" + sub_path = "stash" + } + config = { + name = "stash-config" + pvc = true + pvc_size = "1Gi" + mount_path = "/root/.stash" + } + } + } +} +``` + +#### 9b. Host path + +Mount a directory from the Kubernetes node directly into the container. No PVC is created. + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + pvc = { + node-data = { + host_path = "/data/myapp" + name = "node-data" + mount_path = "/data" + } + } + } +} +``` + +**Full — mixing PVC and host path:** +```hcl +kubernetes = { + jellyfin = { + image = "jellyfin/jellyfin:latest" + service_port = [8096] + pvc = { + config = { + name = "jellyfin-config" + pvc = true + pvc_size = "10Gi" + mount_path = "/config" + } + media = { + host_path = "/data/media" + name = "jellyfin-media" + mount_path = "/data" + } + } + } +} +``` + +#### 9c. ConfigMap as volume + +Mount a Kubernetes ConfigMap as a directory inside the container. + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + pvc = { + nginx-conf = { + config_map = true + name = "my-nginx-config" + mount_path = "/etc/nginx/conf.d" + } + } + } +} +``` + +**Full — fail2ban jail and filter configs from ConfigMaps:** +```hcl +kubernetes = { + fail2ban = { + image = "gitea.example.com/myorg/fail2ban:latest" + pvc = { + fail2ban-jail = { + config_map = true + name = "fail2ban-nginx-http-auth-jail" + mount_path = "/config/fail2ban/jail.d/" + } + fail2ban-filter = { + config_map = true + name = "fail2ban-nginx-unauthorized-filter" + mount_path = "/config/fail2ban/filter.d/" + } + fail2ban-logs = { + host_path = "/data/kubernetes/reverse-proxy/reverse-proxy-log/" + name = "nginx-fail2ban-logs" + mount_path = "/remotelogs/nginx/" + } + fail2ban-config = { + pvc = true + pvc_size = "2Gi" + name = "fail2ban-config" + mount_path = "/config" + } + } + } +} +``` + +#### 9d. Secret as volume + +Mount a Kubernetes Secret as a directory inside the container. + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + pvc = { + my-secret-vol = { + secret = true + name = "my-tls-secret" + mount_path = "/certs" + } + } + } +} +``` + +**Full — kubeconfig secret mounted alongside a host path:** +```hcl +kubernetes = { + django = { + image = "myrepo/django:latest" + service_port = [8000] + namespace = "django" + pvc = { + kubeconfig-secret = { + secret = true + name = "kubeconfig" + mount_path = "/kubeconfig" + } + stash-data = { + host_path = "/ssd/stash/data-pvc/" + name = "stash-django-pvc" + mount_path = "/stash" + } + } + } +} +``` + +#### 9e. Reusing an existing PVC + +Set `existing_pvc = true` to mount a PVC that already exists without Terraform trying to create it. Useful when two applications share storage. + +```hcl +kubernetes = { + echo = { + image = "ealen/echo-server:latest" + service_port = [80] + namespace = "echo" + pvc = { + shared-config = { + name = "echo-config" + pvc = true + pvc_size = "10Mi" + mount_path = "/app/configs" + } + } + } + + echo2 = { + image = "ealen/echo-server:latest" + service_port = [80] + connected_namespace = "echo" + pvc = { + shared-config = { + name = "echo-config" + pvc = true + pvc_size = "10Mi" + mount_path = "/app/configs" + existing_pvc = true # do not recreate; reuse what echo created + } + } + } +} +``` + +--- + +### 10. Database + +Adding a `database` block automatically provisions: a DB deployment, a DB service (`-db-svc`), a DB PVC (`-db-pvc`), a backup PVC (`-db-backup`), a daily backup CronJob, and a Kubernetes Secret (`-db-terraform`) containing all common DB env vars in multiple naming conventions (so most apps just work without extra config). + +The generated secret (`-db-terraform`) contains: `DB_DATABASE_NAME`, `DB_DATABASE`, `POSTGRES_DB`, `MYSQL_DATABASE`, `PGDATABASE`, `DB_HOST`, `PGHOST`, `POSTGRES_HOST`, `DB_PASSWORD`, `MYSQL_PASSWORD`, `MYSQL_ROOT_PASSWORD`, `POSTGRES_PASSWORD`, `PGPASSWORD`, `DB_USERNAME`, `POSTGRES_USER`, `MYSQL_USER`, `PGUSER`, `DATABASE_URL`. + +#### Minimal — default PostgreSQL: +```hcl +kubernetes = { + myapp = { + image = "myrepo/myapp:latest" + database = {} + } +} +``` + +#### Full — MySQL with custom image, port, credentials, and custom env var mapping: +```hcl +kubernetes = { + bookstack = { + image = "linuxserver/bookstack:latest" + service_port = [80] + database = { + engine = "mysql" + image = "lscr.io/linuxserver/mariadb" + port = 3306 + mountPath = "/config" + username = "bookstack_user" + name = "bookstackapp" + map_name = "DB_DATABASE" + map_host = "DB_HOST" + map_password = "DB_PASS" + map_username = "DB_USER" + } + secret_ref = ["bookstack-credentials"] + config_map_ref = ["bookstack-config"] + } +} +``` + +#### Using an external database (no DB deployment created) + +Set `host` to point to an external hostname. Terraform will still generate the Bitwarden secrets and the Kubernetes secret, but will not create a DB deployment or service. + +```hcl +kubernetes = { + myapp = { + image = "myrepo/myapp:latest" + database = { + host = "my-external-postgres.example.com" + port = 5432 + name = "myapp_db" + } + } +} +``` + +#### Database with args (e.g. MongoDB replica set) + +```hcl +kubernetes = { + overleaf = { + image = "tuetenk0pp/sharelatex-full:latest" + service_port = [80] + database = { + image = "mongo:8.0" + port = 27017 + mountPath = "/data/db" + } + database_args = ["--replSet", "overleaf"] + redis = { image = "redis:7.4" } + } +} +``` + +#### Database `shunt` — container is itself the DB + +When the application container IS the database (e.g. Bitwarden's bundled MSSQL), set `shunt = "true"` to skip creating a separate DB deployment while still generating backup jobs and Bitwarden secrets. + +```hcl +kubernetes = { + bitwarden-mssql = { + connected_namespace = "bitwarden" + image = "ghcr.io/bitwarden/mssql:2026.1.0" + service_port = [1433, 5000] + database = { + shunt = "true" + engine = "sqlserver" + username = "sa" + host = "bitwarden-mssql-svc" + name = "bitwarden_vault" + } + } +} +``` + +--- + +### 11. Redis + +Adding a `redis` block automatically provisions a Redis deployment (`-redis`), service (`-redis-svc`), and PVC (`-redis-pvc`). + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "myrepo/myapp:latest" + database = {} + redis = {} + } +} +``` + +**Full — custom Redis image:** +```hcl +kubernetes = { + nextcloud = { + image = "nextcloud:29.0.3-apache" + service_port = [80] + namespace = "next" + database = { image = "docker.io/library/postgres:14" } + redis = { image = "docker.io/library/redis:7" } + environment = { + REDIS_HOST = { value = "nextcloud-redis-svc" } + NEXTCLOUD_ADMIN_USER = { value = "admin" } + } + } +} +``` + +--- + +### 12. Liveness probes + +Two probe types are supported. Only one should be used per application. + +**Minimal — TCP socket:** +```hcl +kubernetes = { + myapp = { + image = "redis:latest" + service_port = [6379] + liveness_tcp_socket = 6379 + } +} +``` + +**Full — HTTP GET with custom path:** +```hcl +kubernetes = { + myapp = { + image = "myrepo/myapp:latest" + service_port = [8080] + liveness_http_get = 8080 + liveness_http_get_path = "/healthz" + requirements = { + limits-cpu = "500m" + limits-memory = "512Mi" + requests-cpu = "100m" + requests-memory = "128Mi" + } + } +} +``` + +--- + +### 13. Resource requirements (CPU / memory) + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + requirements = { + limits-cpu = "500m" + limits-memory = "512Mi" + requests-cpu = "50m" + requests-memory = "128Mi" + } + } +} +``` + +--- + +### 14. Custom entrypoint command + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "myrepo/myapp:latest" + command = ["python", "manage.py", "runserver"] + } +} +``` + +**Full — Gotenberg with multiple flags:** +```hcl +kubernetes = { + gotenberg = { + image = "gotenberg/gotenberg:8" + service_port = [3000] + connected_namespace = "paperless" + command = [ + "gotenberg", + "--chromium-disable-javascript=true", + "--chromium-allow-list=file:///tmp/.*", + "--chromium-auto-start", + ] + requirements = { + limits-cpu = "1" + limits-memory = "1000Mi" + requests-cpu = "500m" + requests-memory = "512Mi" + } + environment = { + CHROMIUM_PAPER_WIDTH = { value = "595" } + CHROMIUM_PAPER_HEIGHT = { value = "842" } + } + } +} +``` + +--- + +### 15. Security context (privileged) + +Set `security_context = true` to run the container as privileged. Required for applications that manipulate network interfaces or kernel modules (e.g. VPN clients, fail2ban). + +**Minimal:** +```hcl +kubernetes = { + gluetun = { + image = "qmcgaw/gluetun:latest" + service_port = [8888] + security_context = true + } +} +``` + +**Full — Gluetun WireGuard VPN with HTTP proxy:** +```hcl +kubernetes = { + gluetun = { + image = "qmcgaw/gluetun:latest" + service_port = [8888] + security_context = true + dns_nameservers = "1.1.1.1" + secret_import = ["5527ba11-07e3-415b-8d1f-b3ee0154eb56"] + environment = { + VPN_SERVICE_PROVIDER = { value = "protonvpn" } + VPN_TYPE = { value = "wireguard" } + SERVER_COUNTRIES = { value = "Netherlands" } + FREE_ONLY = { value = "on" } + HTTPPROXY = { value = "on" } + HTTPPROXY_LISTENING_ADDRESS = { value = "0.0.0.0:8888" } + FIREWALL_INPUT_PORTS = { value = "8888,8080,9696" } + WIREGUARD_PRIVATE_KEY = { + value_from = { + secret_key_ref = { + name = "gluetun-wireguard-secret" + key = "WIREGUARD_PRIVATE_KEY" + } + } + } + } + requirements = { + limits-cpu = "500m" + limits-memory = "512Mi" + requests-cpu = "50m" + requests-memory = "64Mi" + } + } +} +``` + +--- + +### 16. Custom DNS nameservers + +Override the DNS resolver for the pod. Useful for containers that route through a VPN. + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + dns_nameservers = "1.1.1.1" + } +} +``` + +--- + +### 17. Service account + +Attach a Kubernetes ServiceAccount to the pod. The ServiceAccount must already exist (or be created via `extra_manifest`). + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + service_account_name = "my-service-account" + } +} +``` + +**Full — fail2ban with RBAC manifests and service account:** +```hcl +kubernetes = { + fail2ban = { + image = "gitea.example.com/myorg/fail2ban:latest" + service_account_name = "kubectl-service-account" + security_context = true + extra_manifest = [ + "/opt/kubernetes/fail2ban/fail2ban.RoleBinding.yaml", + "/opt/kubernetes/fail2ban/fail2ban.Role.yaml", + "/opt/kubernetes/fail2ban/fail2ban.ServiceAccount.yaml", + ] + config_map_import = [ + "9d8ef71a-de74-478b-aa86-b3ee0160dc43", + "50814edb-d190-41cb-8950-b3ee01622dcf", + ] + environment = { + PUID = { value = "1000" } + TZ = { value = "Etc/UTC" } + } + pvc = { + config = { + pvc = true + pvc_size = "2Gi" + name = "fail2ban-config" + mount_path = "/config" + } + logs = { + host_path = "/data/kubernetes/reverse-proxy/reverse-proxy-log/" + name = "nginx-fail2ban-logs" + mount_path = "/remotelogs/nginx/" + } + } + } +} +``` + +--- + +### 18. Nginx reverse proxy (domain_name) + +Setting `domain_name` generates an nginx upstream + server block in the `extra-sites-terraform` ConfigMap in the `reverse-proxy` namespace. The generated config proxies `.tryton.geeraert.eu` to `-svc..svc:`. This requires Stakater Reloader (or equivalent) to pick up the ConfigMap change. + +**Minimal:** +```hcl +kubernetes = { + myapp = { + image = "nginx:latest" + service_port = [80] + domain_name = "myapp" + } +} +``` + +**Full — Paperless with domain, database, and Redis:** +```hcl +kubernetes = { + paperless = { + image = "ghcr.io/paperless-ngx/paperless-ngx:2.18.4" + service_port = [8000] + domain_name = "paperless" + database = { + image = "postgres:15.6" + mountPath = "/var/lib/postgresql/data" + } + redis = { image = "redis:7.4.1" } + environment = { + PAPERLESS_URL = { value = "https://paperless.example.com" } + PAPERLESS_REDIS = { value = "redis://paperless-redis-svc:6379" } + PAPERLESS_TIKA_ENABLED = { value = "1" } + PAPERLESS_TIKA_GOTENBERG_ENDPOINT = { value = "http://gotenberg-svc.paperless.svc:3000" } + PAPERLESS_TIKA_ENDPOINT = { value = "http://tika-svc.paperless.svc:9998" } + } + pvc = { + data = { + pvc = true + name = "paperless-data" + pvc_size = "50Gi" + mount_path = "/usr/src/paperless/data" + } + media = { + pvc = true + name = "paperless-media" + pvc_size = "200Gi" + mount_path = "/usr/src/paperless/media" + } + consume = { + pvc = true + name = "paperless-consume" + pvc_size = "10Gi" + mount_path = "/usr/src/paperless/consume" + } + } + } +} +``` + +--- + +## Complete full example + +The following deploys a full-featured application with every available option set. It demonstrates: custom namespace, multiple ports, LoadBalancer service, PostgreSQL with custom image and credential mapping, Redis, environment variables (direct + secret refs), mounted secrets and configmaps, a host path volume, liveness probe, resource limits, custom entrypoint, privileged security context, custom DNS, service account, extra manifests, and Bitwarden secret imports. + +```hcl +kubernetes = { + my-full-app = { + # --- Identity --- + name = "my-full-app" + image = "myrepo/myapp:1.2.3" + namespace = "myapp-namespace" + enabled = true + replicas = 2 + + # --- Networking --- + service_port = [8080, 8443] + service_type = "LoadBalancer" + domain_name = "myapp" + dns_nameservers = "1.1.1.1" + + # --- Entrypoint & security --- + command = ["python", "manage.py", "runserver", "0.0.0.0:8080"] + security_context = true + service_account_name = "myapp-service-account" + + # --- Liveness --- + liveness_http_get = 8080 + liveness_http_get_path = "/healthz" + + # --- Resources --- + requirements = { + limits-cpu = "1000m" + limits-memory = "1Gi" + requests-cpu = "200m" + requests-memory = "256Mi" + } + + # --- Database --- + database = { + engine = "postgres" + image = "docker.io/library/postgres:16" + port = 5432 + mountPath = "/var/lib/postgresql/data" + username = "myapp_user" + name = "myapp_db" + map_name = "APP_DB_NAME" + map_host = "APP_DB_HOST" + map_password = "APP_DB_PASS" + map_username = "APP_DB_USER" + } + + # --- Redis --- + redis = { image = "docker.io/library/redis:7" } + + # --- Environment --- + environment = { + APP_ENV = { value = "production" } + APP_URL = { value = "https://myapp.example.com" } + APP_SECRET_KEY = { + value_from = { + secret_key_ref = { + name = "myapp-credentials" + key = "SECRET_KEY" + } + } + } + DB_PASSWORD = { + value_from = { + secret_key_ref = { + name = "myapp-db-terraform" + key = "DB_PASSWORD" + } + } + } + } + + # --- Bulk secret/configmap mounts --- + secret_ref = ["myapp-credentials", "myapp-extra-secret"] + config_map_ref = ["myapp-config", "global-config"] + + # --- Bitwarden imports --- + secret_import = ["9fc5a725-75f5-4bab-b595-b474017146c3"] + config_map_import = ["9d8ef71a-de74-478b-aa86-b3ee0160dc43"] + + # --- Extra manifests --- + extra_manifest = [ + "/opt/kubernetes/myapp/myapp-role.yaml", + "/opt/kubernetes/myapp/myapp-rolebinding.yaml", + ] + + # --- Volumes --- + pvc = { + app-data = { + pvc = true + name = "myapp-data" + pvc_size = "50Gi" + mount_path = "/app/data" + storage_class_name = "nfs-storage-fastaccess" + } + app-config = { + pvc = true + name = "myapp-config" + pvc_size = "1Gi" + mount_path = "/app/config" + sub_path = "prod" + } + node-media = { + host_path = "/data/media" + name = "node-media" + mount_path = "/media" + } + nginx-conf = { + config_map = true + name = "myapp-nginx-config" + mount_path = "/etc/nginx/conf.d" + } + tls-certs = { + secret = true + name = "myapp-tls" + mount_path = "/certs" + } + } + } +} +``` + +--- + +## Database backup + +When a `database` block is present, a daily backup CronJob is automatically created (`-db-backup`, runs at 00:01 UTC). Backups are written to the `-db-backup` PVC (storage class: `nfs-storage-fastaccess`, 1Gi). The backup command and image are selected based on the `engine` field: + +| Engine | Image | Command | +|-------------|-------------------------------|-------------------------------------------------------| +| `postgres` | `alpine/psql` | `pg_dump \| gzip -9 > /backup/-DB-.sql.gz` | +| `mysql` | `mysql` | `mysqldump ... \| gzip -9 > /backup/-DB-.sql.gz` | +| `sqlserver` | `mcr.microsoft.com/mssql-tools` | `sqlcmd BACKUP DATABASE ... TO DISK` | + +--- + +## Naming conventions + +Given a map key of `myapp` with no overrides: + +| Resource | Name | +|---------------------------|-----------------------------------| +| Namespace | `myapp` | +| Deployment | `myapp` | +| Service | `myapp-svc` | +| DB Deployment | `myapp-db` | +| DB Service | `myapp-db-svc` | +| DB PVC | `myapp-db-pvc` | +| DB Backup PVC | `myapp-db-backup` | +| DB Backup CronJob | `myapp-db-backup` | +| DB Secret (all var names) | `myapp-db-terraform` | +| DB Secret (mapped names) | `myapp-db-mapped-terraform` | +| Redis Deployment | `myapp-redis` | +| Redis Service | `myapp-redis-svc` | +| Redis PVC | `myapp-redis-pvc` | +| PVC (from `pvc` block) | `-pvc` |