Terraform module for deploying n8n on AWS.
Deploys the production-grade multi-main setup: multiple n8n main instances, dedicated worker pods, external PostgreSQL (RDS), Redis (ElastiCache), and S3 for shared file storage. An n8n Enterprise license is required.
The module expects a pre-existing VPC. If your parent domain is hosted in Route 53, pass route53_zone_id and the module issues the ACM certificate and creates the DNS alias record itself. A single terraform apply brings up n8n end to end with no manual DNS steps. If your DNS is elsewhere, pass a pre-validated certificate_arn instead.
Pre-release module
The
terraform-aws-n8nmodule is in pre-release. Expect breaking changes at any time before the first stable release.
Users and inbound webhooks hit an Application Load Balancer (managed by the AWS Load Balancer Controller) that fronts the EKS cluster. Inside the cluster the n8n Helm chart runs three separate deployments: main instance pods (leader election, UI/editor, REST API), webhook processor pods for inbound triggers, and worker pods that run job executions and scale on Redis queue depth via KEDA. State lives in managed services outside the cluster: RDS PostgreSQL for workflow state, ElastiCache Redis for leader election and the worker queue, and S3 for binary data. ACM issues the TLS certificate for the ALB. The cluster
also ships the EBS CSI driver and a default encrypted gp3 StorageClass, so
PersistentVolumeClaims from workloads deployed beside n8n bind out of the box
(n8n itself needs no volumes).
module "n8n" {
source = "n8n-io/n8n/aws"
version = "~> 0.2.0"
aws_region = "us-east-1"
cluster_name = "n8n-cluster"
n8n_domain = "n8n.example.com"
n8n_license_key = var.n8n_license_key
# Pre-existing VPC — bring your own.
vpc_id = module.vpc.vpc_id
private_subnets = module.vpc.private_subnets
public_subnets = module.vpc.public_subnets
vpc_cidr_block = module.vpc.vpc_cidr_block
# EKS node group autoscaling bounds (defaults shown). Driven by Cluster
# Autoscaler — see note below; you pay for `node_min` nodes 24/7.
node_min = 3
node_max = 6
# DNS — set exactly one:
# 1. Parent domain in Route53 → module handles ACM + alias record.
route53_zone_id = "Z0123456789ABCDEFGHIJ"
# 2. DNS elsewhere → bring your own pre-validated cert.
# certificate_arn = aws_acm_certificate_validation.n8n.certificate_arn
}The module declares required_providers but does not configure them. Callers must configure aws, kubernetes, and helm providers. kubernetes and helm are configured against the cluster this module creates — see examples/small/providers.tf for the standard wiring.
node_min and node_max are the EKS node group's autoscaling bounds. node_min is your steady-state floor — you pay for those nodes 24/7 even when idle. node_max is a hard ceiling: if peak workload needs more nodes than allowed, pods stay Pending. Defaults fit the small example only; see the Examples table for production sizing.
For a full end-to-end example including the VPC, see examples/small/ (Route 53), examples/cloudflare/, or examples/godaddy/. If terraform apply fails on a helm_release (most often due to a Helm 4 cache layout issue or a webhook race on first install), see docs/troubleshooting.md.
This module is open source software, maintained by the n8n Solutions team independently of n8n's enterprise products. While the n8n Support team provides dedicated support for the enterprise offerings, this module isn't included.
Bug reports and feature requests: open a GitHub issue. We triage on a best-effort basis; there is no SLA.
Security issues: see SECURITY.md for the disclosure process. Do not open public issues for security findings.
General n8n questions (not specific to this module): use the n8n community forum.
This module is pre-1.0. We use minor versions (0.1, 0.2, …) as the breaking-change boundary and patches (0.1.0, 0.1.1, …) for additive or bug-fix changes.
| Across | What may change |
|---|---|
0.MINOR.PATCH → 0.MINOR.PATCH+1 |
Bug fixes, new optional inputs, new outputs, new resources whose absence wouldn't affect existing callers. No removed or renamed inputs/outputs. No changed defaults that move infra. No changed resource addresses. |
0.MINOR → 0.MINOR+1 |
Anything else, including removed inputs, renamed inputs, default changes that force resource replacement, refactored resource addresses, and bumped provider version floors. Each such change is called out in CHANGELOG.md with an upgrade note. |
Pin with version = "~> 0.2.0" to auto-receive 0.2.x patches without
accidentally crossing a 0.2 → 0.3 boundary. Note the three-component
constraint: ~> 0.2.0 resolves to >= 0.2.0, < 0.3.0, whereas the
two-component ~> 0.2 would resolve to >= 0.2, < 1.0 and let you cross
minor boundaries unintentionally. To upgrade across minor lines, retype
the constraint (e.g. version = "~> 0.3.0") and read the release notes.
This contract goes away at 1.0.0 in favor of standard SemVer.
This module ships against specific provider majors. Notably:
- AWS provider:
~> 6.0. Upgrading from a v0.1.x deployment (which pinnedaws ~> 5.0) requires a one-timeterraform plan -refresh-onlyfollowed byterraform apply -refresh-onlyto settle AWS provider 6.0's per-resourceregionattribute into state before applying other changes. Callers who must stay on AWS provider 5.x should pin this module to~> 0.1.0. - Helm provider:
~> 3.0. The 3.x release is a Plugin Framework rewrite;helm_releasedrift detection is stricter, so the firstterraform planafter upgrading from v0.1.x may show in-place diffs on existing releases. Callers who must stay on Helm provider 2.x should pin this module to~> 0.1.0. - Kubernetes provider:
~> 2.0. - Terraform CLI:
>= 1.9. - n8n Helm chart: default
1.10.0. Other chart versions can be selected vian8n_chart_version. - n8n application image: defaults to the chart's floating
stabletag; production deployments should pin a specific version vian8n_image_tag(e.g."1.2.3") to avoid crossing major-version boundaries on an unplanned pod reschedule. - EKS: validated on Kubernetes
1.35. - PostgreSQL: validated on RDS
16.9.
v0.2.0 intentionally does not cover the following. Each item is
documented here so that issues filed against them can be triaged
quickly; several are candidates for future minor releases (see
ROADMAP.md).
-
VPC creation. The module requires a pre-existing VPC with both public and private subnets tagged for EKS/ALB. The examples provision one with
terraform-aws-modules/vpc/aws, but that VPC is not managed by this module. Rationale: VPCs are organization-shaped, not service-shaped. -
Multi-region / cross-region deployments. One module instance = one region = one EKS cluster = one n8n deployment. Cross-region replication of the database or S3 binary storage is the caller's problem. Rationale: pre-1.0 surface area; AWS provider 6.x's per-resource
regionargument is the natural foundation for this in a future minor. -
GovCloud, AWS China, and Outposts. The module uses generic AWS APIs that probably work in these partitions, but it has not been validated. Endpoint differences (e.g. EKS Pod Identity GA dates per region) may break things.
-
Air-gapped / private-image deployments. The module pulls images from public registries: the n8n chart from
ghcr.io/n8n-io, plus KEDA / Cluster Autoscaler / AWS Load Balancer Controller / metrics-server charts from their respective upstreams. Replacing all of these with ECR mirrors is possible but the module exposes no inputs for image-registry overrides today. -
Backup/DR automation beyond RDS snapshots. The module enables RDS automated backups (defaulting to RDS's own defaults). It does not automate restore drills, cross-region snapshot copy, S3 versioning policy, or n8n encryption-key escrow. The
n8n_encryption_keyoutput is emitted exactly once at apply time; backing it up is the operator's job and is the single most important thing they will forget. -
Bundled observability. The module installs KEDA (for worker autoscaling) and metrics-server (for HPA on mains/webhooks) because they are load-bearing for the autoscaling story. It does not install Prometheus, Grafana, Loki, OpenSearch, Datadog Agent, or any log shipper.
n8n_metrics_enabledexposes the metrics endpoint; scrape configuration is the caller's monitoring stack. Rationale: observability stacks are deeply opinionated per-org; bundling one is more harmful than helpful.
Five runnable examples ship with the module: three sizing tiers (small, medium, large) on Route 53, plus two DNS-variant examples (cloudflare, godaddy) at small sizing. Sizing decisions for medium and large are derived from internal load testing.
| Dimension | small (default) | medium | large |
|---|---|---|---|
| Target scale | Dev / small team | ~5–15M exec/day | ~50–60+M exec/day |
| Avg req/s | ~10–30 | ~60–175 | ~350–960 |
| Node type | t3.xlarge (4 vCPU, 16 GB) | m6i.2xlarge (8 vCPU, 32 GB) | m7i.4xlarge (16 vCPU, 64 GB) |
| Nodes desired / min / max | 3 / 3 / 6 | 5 / 5 / 15 | 10 / 10 / 50 |
| Total vCPU (desired) | 12 | 40 | 160 |
| Private subnets | 2× /24 (254 IPs each) | 2× /24 | 2× /20 (4,094 IPs each) |
| VPC CNI tuning | default | default | WARM_ENI_TARGET=0 |
| Database | RDS db.t3.small (2 vCPU, 2 GB) | RDS db.m6g.2xlarge (8 vCPU, 32 GB) | Aurora PostgreSQL I/O-Optimized |
| DB instances | 1 writer (Multi-AZ standby) | 1 writer (Multi-AZ standby) | 1 writer + 1 reader |
| DB storage | 50 GB gp2 | 200 GB gp3 | Aurora auto-scales to 128 TB |
| DB IOPS ceiling | 150 baseline / 3,000 burst | 3,000 baseline (gp3) | None — I/O-Optimized |
| PgBouncer | No | No | Yes — 2 replicas |
| Redis | cache.t3.medium | cache.r6g.large | cache.r6g.large |
| Webhook pods min / max | 2 / 50 | 5 / 50 | 30 / 80 |
| Worker pods min / max | 1 / 10 | 5 / 40 | 20 / 160 |
| Worker concurrency | 10 | 20 | 40 |
| Execution concurrency limit | 100 | 200 | 2,000 |
| Webhook memory limit | 1 Gi | 2 Gi | 4 Gi |
| Webhook memory request | 512 Mi | 512 Mi | 1 Gi |
| Pruning retention | 10k records / 14 days | 500k records / 7 days | 5M records / 24h |
| Est. cost / month (on-demand) | ~$440 | ~$2,000 | ~$21,000+ |
| Est. cost / month (1-yr reserved) | ~$285 | ~$1,300 | ~$13,600 |
The DNS-variant examples (cloudflare, godaddy) are sizing-equivalent to small — they only swap the DNS provider for cert validation and the alias record.
aws_kms_key.db is created with deletion_window_in_days = 7 (the AWS
minimum), so a terraform destroy schedules the key for deletion 7 days out
rather than removing it immediately. Two operational consequences:
- Cost: ~$1/month prorated, ~$0.23 per destroy cycle. Negligible but non-zero.
- Repeat applies inside the window:
aws_kms_alias.dbusesname_prefix(not a fixedname), so apply → destroy → apply works cleanly within the 7-day window — each apply gets a fresh alias suffix. If you need to recover the scheduled-for-deletion key for any reason, runaws kms cancel-key-deletion --key-id <key-id>and import it back into state withterraform import aws_kms_key.db[0] <key-id>.
Set n8n_metrics_enabled = true to expose n8n's built-in Prometheus endpoint.
When on, the module appends N8N_METRICS=true to the main pod's extraEnv;
n8n serves metrics on its existing HTTP listener — path /metrics on
port 5678 (the same port and n8n-main Service the chart already
publishes for the UI/API), so no additional ports or Services are needed.
The pinned n8n Helm chart version (see n8n_chart_version) exposes no
top-level metrics or serviceMonitor block of its own — verified via
helm show values oci://ghcr.io/n8n-io/n8n-helm-chart/n8n --version <ver> —
so this toggle is intentionally env-var-only. Wiring the actual scrape is
left to your monitoring stack: add Prometheus scrape annotations to the
n8n-main Service via your own Kubernetes resource, or create a
ServiceMonitor CR if you run the Prometheus Operator.
Set n8n_otel_enabled = true to turn on n8n's built-in workflow and node
tracing. The module wires the N8N_OTEL_* environment variables onto the
Helm release's config.extraEnv block, which the chart applies to every
n8n container — main, worker, and webhook processor. This matches the
queue-mode requirement in the
n8n OpenTelemetry docs:
the env vars must be set on every instance for trace context to propagate
between them.
The collector (e.g. an OpenTelemetry Collector deployment, or Jaeger's
OTLP receiver) is intentionally out of scope for this module. Deploy
one separately — either with a sibling Terraform module, the upstream
open-telemetry/opentelemetry-collector Helm chart, or your existing
observability platform — then point n8n_otel_exporter_otlp_endpoint at
its base URL (n8n appends /v1/traces itself, so don't include it).
Minimal opt-in:
module "n8n" {
# ...other inputs...
n8n_otel_enabled = true
n8n_otel_exporter_otlp_endpoint = "http://otel-collector.observability.svc.cluster.local:4318"
}Individual tuning variables (n8n_otel_exporter_otlp_headers,
n8n_otel_exporter_service_name, n8n_otel_traces_sample_rate,
n8n_otel_traces_include_node_spans, n8n_otel_traces_inject_outbound,
n8n_otel_traces_production_only) all default to null. When an individual value is null the corresponding
env var is omitted entirely and n8n's own default applies — only set the
values you actually need to override.
When n8n_otel_enabled = false (the default), none of the N8N_OTEL_*
env vars are emitted and n8n's OpenTelemetry SDK is not loaded.
n8n_otel_exporter_otlp_headers is marked sensitive because it typically
carries collector authentication tokens.
Set n8n_log_streaming_managed_by_env = true to provision n8n's
log streaming destinations
declaratively from Terraform instead of the UI. The module JSON-encodes
n8n_log_streaming_destinations into N8N_LOG_STREAMING_DESTINATIONS and
sets N8N_LOG_STREAMING_MANAGED_BY_ENV=true on every n8n container. n8n
then reapplies the destinations on every startup and locks the Log
Streaming UI controls read-only — Terraform becomes the source of truth.
Requires n8n >= 2.19.0 and an Enterprise license that includes the log
streaming entitlement (the module already requires n8n_license_key).
module "n8n" {
# ...other inputs...
n8n_log_streaming_managed_by_env = true
n8n_log_streaming_destinations = [
{
type = "webhook"
label = "Audit events"
enabled = true
subscribedEvents = ["n8n.audit", "n8n.workflow"]
url = "https://hooks.example.com/n8n"
method = "POST"
},
]
}Each destination is an object with type set to webhook, syslog, or
sentry plus the type-specific fields from the
n8n docs.
The variable is marked sensitive (webhook headers and Sentry DSNs
typically carry credentials), but the rendered value still lands in
Terraform state and the pod environment in plaintext — restrict access
accordingly. Setting n8n_log_streaming_managed_by_env back to false
keeps the last applied destinations but restores UI write access.
| Name | Version |
|---|---|
| terraform | >= 1.9 |
| aws | ~> 6.0 |
| helm | ~> 3.0 |
| kubernetes | ~> 2.0 |
| random | ~> 3.0 |
| time | ~> 0.12 |
| Name | Version |
|---|---|
| aws | ~> 6.0 |
| helm | ~> 3.0 |
| kubernetes | ~> 2.0 |
| random | ~> 3.0 |
| time | ~> 0.12 |
No modules.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| aws_region | AWS region to deploy into (e.g. us-east-1, eu-west-1, ap-southeast-1). Must match the region the AWS provider is configured for. | string |
n/a | yes |
| certificate_arn | ARN of a pre-validated ACM certificate for n8n_domain. Use this for Cloudflare, GoDaddy, or any DNS provider other than Route53 — the respective examples (examples/cloudflare, examples/godaddy) issue the certificate and pass its ARN here. Set exactly one of certificate_arn or route53_zone_id. | string |
null |
no |
| cluster_name | Name for the EKS cluster. Keep to 14 characters or fewer — the module derives an ElastiCache cluster ID of <cluster_name>-redis, and AWS caps ElastiCache IDs at 20 chars. |
string |
"n8n-cluster" |
no |
| create_database | When true (the default), the module creates and manages an Amazon RDS PostgreSQL instance. Set to false to use an external database (e.g. Amazon Aurora created by the caller) — db_host and db_password must then be supplied. Kept as a static boolean rather than db_host == null because count expressions cannot depend on values computed at apply time. |
bool |
true |
no |
| db_allocated_storage | Allocated storage for RDS in GB | number |
50 |
no |
| db_engine_version | PostgreSQL engine version for the RDS instance. Must be a version available from aws rds describe-db-engine-versions --engine postgres in the target region — RDS deprecates and removes minor versions over time, and supported versions vary by region. Bump as needed without forking. |
string |
"16.9" |
no |
| db_host | External database host. Required when create_database = false. Ignored otherwise. Use this to pass in an Amazon Aurora cluster endpoint or any external PostgreSQL host. | string |
null |
no |
| db_instance_class | RDS instance class (db.t3.small ~$25/month, db.t3.medium for higher load) | string |
"db.t3.small" |
no |
| db_multi_az | Deploy RDS in Multi-AZ mode for automatic failover (recommended for production) | bool |
true |
no |
| db_password | Password for the external database specified by db_host. Required when create_database = false. Ignored otherwise (the module generates a random password for its managed RDS instance). | string |
null |
no |
| db_postgresdb_pool_size | Number of TypeORM connection pool slots per n8n pod. Each pod holds this many persistent PostgreSQL connections. Rule of thumb: pool_size >= worker_concurrency / 4. With PgBouncer in transaction mode a lower value (5) is sufficient; without PgBouncer use a value matching concurrency (10-20). | number |
10 |
no |
| db_postgresdb_ssl_enabled | Whether n8n connects to the database over SSL. Set to true (the default) for direct connections to RDS or Aurora — they use the AWS CA which Node.js doesn't trust by default, so the connection still negotiates SSL but skips certificate verification. Set to false when n8n connects to an in-cluster connection pooler (e.g. PgBouncer) that handles SSL on its upstream leg — the pod-to-pod traffic stays inside the cluster network. | bool |
true |
no |
| db_storage_encrypted | When true (the default), encrypt the RDS instance's storage, Performance Insights data, and the postgresql CloudWatch log group with a module-created Customer Managed KMS Key (aws_kms_key.db). Clears Checkov findings CKV_AWS_16, CKV_AWS_354, and CKV_AWS_158. Flipping this from false to true on an existing RDS instance forces a replacement — AWS does not support enabling storage encryption in place, so the upgrade path is snapshot → restore into a new encrypted instance. Set to false in your tfvars to preserve current behavior on pre-existing unencrypted deployments. The CMK rotates annually and uses a 7-day deletion window (AWS minimum). Ignored when create_database = false. | bool |
true |
no |
| kubernetes_version | Kubernetes version for the EKS cluster | string |
"1.35" |
no |
| n8n_chart_version | n8n Helm chart version to deploy | string |
"1.10.0" |
no |
| n8n_community_packages_prevent_loading | Prevent installed community packages from being loaded at runtime. Maps to N8N_COMMUNITY_PACKAGES_PREVENT_LOADING. When true, n8n leaves the community-packages management surface in place but skips loading the package code, which is useful for locking an instance down without uninstalling. Leave false (the default) for community nodes to load and execute. n8n defaults this to false; when false the env var is omitted entirely so n8n's own default applies. | bool |
false |
no |
| n8n_domain | Fully-qualified domain name for n8n (e.g. n8n.example.com). Must match the CN / SAN on the certificate provided via certificate_arn. | string |
n/a | yes |
| n8n_execution_concurrency_limit | Maximum concurrent production executions (-1 to disable) | number |
100 |
no |
| n8n_execution_timeout | Default execution timeout in seconds (-1 to disable) | number |
7200 |
no |
| n8n_execution_timeout_max | Maximum execution timeout users can configure in seconds | number |
7200 |
no |
| n8n_extra_env | Additional environment variables to inject into all n8n pods (main, worker, and webhook-processor) via the Helm chart's config.extraEnv list. Each entry is an object with name and value string attributes. config.extraEnv is appended last in every container's env list, so by Kubernetes' last-wins rule any name here overrides the chart's value for that name. To prevent silently breaking the deployment, an entry is rejected at plan time when its name collides with a connection, identity, storage, license, or topology variable the module manages: any name starting with DB_, QUEUE_, N8N_RUNNERS_, N8N_EXTERNAL_STORAGE_S3_, N8N_MULTI_MAIN_, or AWS_, plus names like N8N_ENCRYPTION_KEY, N8N_LICENSE_ACTIVATION_KEY, N8N_HOST, WEBHOOK_URL, and EXECUTIONS_MODE. Use the dedicated module inputs for those. Do not put secret values here, because they render into the Helm release and are stored in plaintext in Terraform state; instead pass a *_FILE companion (e.g. a name ending in _FILE) pointing at a mounted Kubernetes secret, or use n8n credentials. Example: [{name = "N8N_DEFAULT_LOCALE", value = "de"}]. | list(object({ |
[] |
no |
| n8n_helm_timeout | Seconds Terraform waits for the n8n Helm release to converge. Increase for large deployments where rolling out 50+ pods (workers + webhook processors + main) exceeds the default. 600s is fine for the default/medium examples; large deployments at 250+ pods need ~1800s. | number |
600 |
no |
| n8n_image_tag | n8n application image tag to deploy (e.g. "2.27.4"). When it is null (the default), the Helm chart's own default applies — currently the floating stable tag, which resolves to whatever n8n version is latest at the time each pod starts. Pin this to a concrete version for reproducible, incremental upgrades and to avoid crossing major-version boundaries (e.g. the n8n 2.0 breaking changes) on an unplanned pod reschedule. See https://docs.n8n.io/2-0-breaking-changes/ for the n8n 2.x migration guide. |
string |
null |
no |
| n8n_license_key | n8n Enterprise license activation key. Get one at https://n8n.io/pricing | string |
n/a | yes |
| n8n_log_level | n8n log level. Maps to the N8N_LOG_LEVEL environment variable. One of: silent, error, warn, info, debug, verbose. | string |
"info" |
no |
| n8n_log_output | n8n log output destination(s). Maps to the N8N_LOG_OUTPUT environment variable. Comma-separated subset of: console, file (e.g. "console", "file", "console,file"). Note: this variable does NOT control log format — setting an invalid value (e.g. "json") leaves Winston with no transport and silently drops all logs. To emit JSON-formatted logs, configure n8n's logging block separately; this env var only selects destinations. | string |
"console" |
no |
| n8n_log_streaming_destinations | List of log streaming destination objects, JSON-encoded into N8N_LOG_STREAMING_DESTINATIONS. Each entry must set type to webhook, syslog, or sentry, plus the type-specific fields documented at https://docs.n8n.io/log-streaming/#configure-using-environment-variables (common fields: label, enabled, subscribedEvents, anonymizeAuditMessages, circuitBreaker). Typed as any because the three destination shapes differ structurally. Marked sensitive because webhook headers and Sentry DSNs typically carry credentials — note the value is still injected as a literal env var: it is persisted in plaintext in Terraform state and visible in the pod environment (kubectl describe / printenv). Ignored when n8n_log_streaming_managed_by_env = false. | any |
[] |
no |
| n8n_log_streaming_managed_by_env | Manage n8n's Enterprise log streaming destinations from environment variables instead of the UI. Maps to N8N_LOG_STREAMING_MANAGED_BY_ENV. When true, n8n applies n8n_log_streaming_destinations on every startup and locks the Log Streaming UI controls read-only. When false (the default), no log streaming env vars are emitted and destinations stay UI-managed; flipping back to false keeps the last applied destinations but restores UI write access. Requires n8n >= 2.19.0 and an Enterprise license that includes log streaming. See https://docs.n8n.io/log-streaming/ for the underlying n8n contract. | bool |
false |
no |
| n8n_main_cpu_limit | CPU limit for n8n main pods (e.g. 2000m, 1000m) | string |
"2000m" |
no |
| n8n_main_cpu_request | CPU request for n8n main pods (e.g. 1000m, 500m) | string |
"1000m" |
no |
| n8n_main_hpa_cpu_threshold | Target average CPU utilization (%) that triggers scaling of n8n main pods. | number |
60 |
no |
| n8n_main_hpa_max_replicas | Maximum replicas for n8n main pods. HPA will not scale above this. | number |
20 |
no |
| n8n_main_hpa_min_replicas | Minimum replicas for n8n main pods. HPA will not scale below this. | number |
2 |
no |
| n8n_main_memory_limit | Memory limit for n8n main pods (e.g. 4Gi, 2Gi) | string |
"4Gi" |
no |
| n8n_main_memory_request | Memory request for n8n main pods (e.g. 2Gi, 1Gi) | string |
"2Gi" |
no |
| n8n_metrics_enabled | Enable n8n's built-in Prometheus metrics endpoint. When true, the module appends N8N_METRICS=true to the n8n Helm release's config.extraEnv, which the chart applies to every n8n container (main, worker, webhook processor). n8n exposes /metrics on its existing HTTP port (5678) — the same port and service the chart already publishes for the UI/API. The n8n Helm chart at the currently pinned version (see n8n_chart_version) exposes no top-level metrics / serviceMonitor block of its own, so this toggle is intentionally env-var-only. Scrape configuration (Prometheus scrape annotations or a ServiceMonitor CR) is left to the caller's monitoring stack — in practice the main pod's Service is the meaningful scrape target. Defaults to false; when false the env var is omitted entirely so n8n's own defaults apply. | bool |
false |
no |
| n8n_otel_enabled | Master switch for n8n's OpenTelemetry workflow + node tracing. When true, the module sets N8N_OTEL_ENABLED=true on all n8n containers (main, worker, webhook processor) via the Helm release's config.extraEnv block. When false (the default), no OpenTelemetry env vars are emitted and the SDK is not loaded. The OpenTelemetry collector / Jaeger receiver is out of scope for this module — deploy it separately and point n8n_otel_exporter_otlp_endpoint at it. See https://docs.n8n.io/hosting/logging-monitoring/opentelemetry/ for the underlying n8n contract. | bool |
false |
no |
| n8n_otel_exporter_otlp_endpoint | Base URL of the OTLP HTTP endpoint to export traces to (e.g. http://otel-collector.observability.svc.cluster.local:4318 for an in-cluster collector). When set, maps to N8N_OTEL_EXPORTER_OTLP_ENDPOINT. n8n appends /v1/traces to this value internally, so point at the base URL, not the traces path. Leave null to use n8n's default (http://localhost:4318), which only works if a sidecar collector is colocated in each n8n pod (this module does not deploy one). Ignored when n8n_otel_enabled = false. | string |
null |
no |
| n8n_otel_exporter_otlp_headers | Comma-separated list of key=value pairs sent as HTTP headers with each OTLP request (e.g. 'authorization=Bearer ,x-tenant=acme'). Use this for collector authentication or multi-tenant routing. Maps to N8N_OTEL_EXPORTER_OTLP_HEADERS. Leave null to send no extra headers. Marked sensitive so the value is redacted from CLI and plan output, but note it is still injected as a literal env var: it is persisted in plaintext in Terraform state and visible in the pod environment (kubectl describe / printenv). The chart's config.extraEnv does not support secretKeyRef, so restrict access to state and the n8n namespace accordingly. Ignored when n8n_otel_enabled = false. | string |
null |
no |
| n8n_otel_exporter_service_name | Value of the service.name resource attribute on exported spans. Maps to N8N_OTEL_EXPORTER_SERVICE_NAME. Leave null to use n8n's default ('n8n'). Set this to differentiate multiple n8n deployments sending traces to the same collector (e.g. 'n8n-prod', 'n8n-staging'). Ignored when n8n_otel_enabled = false. | string |
null |
no |
| n8n_otel_traces_include_node_spans | Whether to emit a node.execute span for each node execution. Maps to N8N_OTEL_TRACES_INCLUDE_NODE_SPANS. Leave null to use n8n's default (true — one span per node per execution). Set to false to export workflow-level spans only — a common volume-reduction lever for workflows with many small nodes. Ignored when n8n_otel_enabled = false. | bool |
null |
no |
| n8n_otel_traces_inject_outbound | Whether n8n's HTTP-helper-based nodes (HTTP Request and similar) inject W3C traceparent / tracestate headers into outbound requests. Maps to N8N_OTEL_TRACES_INJECT_OUTBOUND. Leave null to use n8n's default (true — propagate context to downstream services). Set to false when calling external systems that misbehave on unexpected headers, or when you don't want trace context leaving your boundary. Ignored when n8n_otel_enabled = false. | bool |
null |
no |
| n8n_otel_traces_production_only | Whether to export traces for production workflow executions only. Maps to N8N_OTEL_TRACES_PRODUCTION_ONLY. Leave null to use n8n's default (true — only production executions are traced). Set to false to also trace manual/test executions run from the editor, which helps while developing instrumentation but is noisy in production. Ignored when n8n_otel_enabled = false. | bool |
null |
no |
| n8n_otel_traces_sample_rate | Fraction of traces to export, between 0 and 1 inclusive. Maps to N8N_OTEL_TRACES_SAMPLE_RATE. n8n uses a trace-ID-ratio sampler, so the same trace ID is either fully sampled or fully dropped across all spans. Leave null to use n8n's default (1.0 — every trace exported). Lower for high-volume installs where the collector or backend can't handle every workflow execution as a trace. Ignored when n8n_otel_enabled = false. | number |
null |
no |
| n8n_personalization_enabled | Whether n8n asks users personalization survey questions and tailors content/recommendations based on the answers. Maps to N8N_PERSONALIZATION_ENABLED. When false, sets N8N_PERSONALIZATION_ENABLED=false on all n8n pods (main, worker, webhook processor) via config.extraEnv. Defaults to true, matching n8n's own default — note that explicitly setting true emits no env var (n8n's default already applies). Set to false to skip the personalization survey, e.g. on shared or ephemeral instances. | bool |
true |
no |
| n8n_prestop_sleep | Seconds the preStop hook sleeps before SIGTERM is sent, giving the load balancer time to drain the pod. MINIMUM — do not lower below 10. | number |
10 |
no |
| n8n_pruning_max_age | Maximum age of execution records to retain, in hours (336 = 14 days) | number |
336 |
no |
| n8n_pruning_max_count | Maximum number of execution records to retain (0 = no limit) | number |
10000 |
no |
| n8n_reinstall_missing_packages | Reinstall community packages that are recorded in the database but missing from a pod's local filesystem at startup. Maps to N8N_REINSTALL_MISSING_PACKAGES. n8n stores installed community packages on the pod's filesystem, which is ephemeral in EKS, so a rescheduled or newly scaled-up worker comes up without them and nodes installed via the UI fail to load on that pod. Enabling this makes every pod (main, worker, and webhook-processor) reinstall the recorded packages on boot, which is what lets community nodes work reliably in queue mode. n8n defaults this to false; when false the env var is omitted entirely so n8n's own default applies. | bool |
false |
no |
| n8n_task_runner_auto_shutdown_timeout | Seconds of inactivity before the runner process shuts down. Set to 0 to disable. | number |
15 |
no |
| n8n_task_runner_cpu_limit | CPU limit for task runner sidecar containers (e.g. 1, 2000m) | string |
"1" |
no |
| n8n_task_runner_cpu_request | CPU request for task runner sidecar containers (e.g. 200m, 500m) | string |
"200m" |
no |
| n8n_task_runner_memory_limit | Memory limit for task runner sidecar containers (e.g. 1Gi, 2Gi) | string |
"1Gi" |
no |
| n8n_task_runner_memory_request | Memory request for task runner sidecar containers (e.g. 512Mi, 1Gi) | string |
"512Mi" |
no |
| n8n_task_runner_python_enabled | Enable the native Python runner (beta). Required for Python code execution in workflows. | bool |
true |
no |
| n8n_task_runner_request_timeout | Seconds n8n waits for a task runner to accept a Code node task. Wired to the N8N_RUNNERS_TASK_REQUEST_TIMEOUT env var on the main pod. Increase if Code nodes fail with 'task request timed out' under high concurrency (many parallel Code nodes competing for the single runner sidecar). | number |
300 |
no |
| n8n_task_runners_enabled | Enable task runner sidecars for isolated JavaScript and Python code execution | bool |
true |
no |
| n8n_templates_enabled | Enable n8n's workflow templates and template suggestions. Maps to N8N_TEMPLATES_ENABLED. When false, sets N8N_TEMPLATES_ENABLED=false on all n8n pods (main, worker, webhook processor) via config.extraEnv. Defaults to true, matching n8n's own default — note that explicitly setting true emits no env var (n8n's default already applies). Set to false to hide the templates library, e.g. when enforcing curated internal workflows. | bool |
true |
no |
| n8n_termination_grace_period | Seconds Kubernetes waits after SIGTERM before force-killing pods. MINIMUM — do not lower below 60. Workers need time to finish in-flight executions before being terminated. | number |
60 |
no |
| n8n_timezone | Timezone for n8n (e.g. UTC, America/New_York, Europe/London) | string |
"UTC" |
no |
| n8n_webhook_cpu_limit | CPU limit for n8n webhook processor pods (e.g. 800m, 1000m) | string |
"800m" |
no |
| n8n_webhook_cpu_request | CPU request for n8n webhook processor pods (e.g. 300m, 500m) | string |
"300m" |
no |
| n8n_webhook_hpa_cpu_threshold | Target average CPU utilization (%) that triggers scaling of n8n webhook pods. | number |
65 |
no |
| n8n_webhook_hpa_max_replicas | Maximum replicas for n8n webhook processor pods. HPA will not scale above this. | number |
50 |
no |
| n8n_webhook_hpa_min_replicas | Minimum replicas for n8n webhook processor pods. HPA will not scale below this. | number |
2 |
no |
| n8n_webhook_memory_limit | Memory limit for n8n webhook processor pods (e.g. 1Gi, 2Gi) | string |
"1Gi" |
no |
| n8n_webhook_memory_request | Memory request for n8n webhook processor pods (e.g. 512Mi, 1Gi) | string |
"512Mi" |
no |
| n8n_webhook_url | Public HTTPS base URL used for webhook callbacks (e.g. https://webhooks.example.com). Defaults to https://<n8n_domain> when not set. Override when webhooks are served from a different host than the n8n UI. | string |
null |
no |
| n8n_worker_concurrency | Number of jobs each worker pod can process simultaneously | number |
10 |
no |
| n8n_worker_cpu_limit | CPU limit for n8n worker pods (e.g. 1000m, 2000m) | string |
"1000m" |
no |
| n8n_worker_cpu_request | CPU request for n8n worker pods (e.g. 500m, 1000m) | string |
"500m" |
no |
| n8n_worker_keda_jobs_per_replica | Number of waiting jobs per worker replica used as the KEDA scaling threshold. KEDA targets ceil(queue_depth / jobs_per_replica) replicas. | number |
5 |
no |
| n8n_worker_keda_max_replicas | Maximum worker replicas KEDA may scale to. | number |
10 |
no |
| n8n_worker_keda_min_replicas | Minimum worker replicas. KEDA keeps at least this many workers running even when the queue is empty. | number |
1 |
no |
| n8n_worker_memory_limit | Memory limit for n8n worker pods (e.g. 2Gi, 4Gi) | string |
"2Gi" |
no |
| n8n_worker_memory_request | Memory request for n8n worker pods (e.g. 1Gi, 2Gi) | string |
"1Gi" |
no |
| namespace | Kubernetes namespace to deploy n8n into | string |
"n8n" |
no |
| node_desired | Desired number of worker nodes at startup | number |
3 |
no |
| node_instance_type | EC2 instance type for EKS worker nodes. t3.xlarge (4 vCPU, 16GB) is the recommended minimum for multi-main — the 6 n8n pods (main × 2, worker × 2, webhook × 2) request ~3,600m CPU at minimum replicas, leaving t3.medium nodes with insufficient headroom for HPA to scale. | string |
"t3.xlarge" |
no |
| node_max | Maximum number of worker nodes | number |
6 |
no |
| node_min | Minimum number of worker nodes | number |
3 |
no |
| private_subnets | IDs of private subnets (one per AZ, minimum two AZs). RDS, ElastiCache, and EKS nodes attach here. | list(string) |
n/a | yes |
| public_subnets | IDs of public subnets (one per AZ, minimum two AZs). The ALB attaches here. | list(string) |
n/a | yes |
| redis_node_type | ElastiCache node type (cache.t3.medium ~$25/month) | string |
"cache.t3.medium" |
no |
| route53_zone_id | Route53 hosted zone ID for the parent of n8n_domain (e.g. the zone for example.com if n8n_domain = n8n.example.com). When set, the module issues a DNS-validated ACM certificate and creates the alias A-record automatically — single terraform apply, no manual DNS steps. Leave null and pass certificate_arn instead. Set exactly one of certificate_arn or route53_zone_id. | string |
null |
no |
| tags | Additional AWS tags to apply to all resources this module creates. Merged on top of the built-in ManagedBy/Project tags. | map(string) |
{} |
no |
| vpc_cidr_block | CIDR block of the VPC — used by the RDS and Redis security groups to allow intra-VPC traffic. | string |
n/a | yes |
| vpc_id | ID of the VPC n8n will deploy into. Must contain both public and private subnets with the EKS/ALB subnet tags applied. | string |
n/a | yes |
| Name | Description |
|---|---|
| alb_hostname | ALB hostname. When route53_zone_id is set, the module already creates the alias record — this output is informational. When certificate_arn is used, create a CNAME: your domain → this value. |
| aws_region | AWS region |
| cluster_certificate_authority_data | Base64-encoded EKS cluster CA certificate — pass to kubernetes/helm providers as cluster_ca_certificate (after base64decode). |
| cluster_endpoint | EKS cluster API endpoint — pass to the kubernetes/helm providers as host. |
| cluster_name | EKS cluster name |
| db_password | Database password — module-managed when create_database = true, or the value of var.db_password when using an external database. Retrieve with: terraform output -raw db_password |
| kubectl_config_command | Command to configure kubectl for this cluster |
| n8n_encryption_key | n8n encryption key — back this up in a password manager. Losing it makes all stored credentials unreadable. |
| n8n_url | URL to access n8n once DNS propagates |
| namespace | Kubernetes namespace n8n is deployed into |
| rds_endpoint | Database endpoint — module-managed RDS when create_database = true, or the value of var.db_host when using an external database (e.g. Aurora). |
| redis_endpoint | ElastiCache Redis endpoint |
| s3_bucket_name | S3 bucket used for n8n binary storage |
