Skip to content

n8n-io/terraform-aws-n8n

Repository files navigation

terraform-aws-n8n

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-n8n module is in pre-release. Expect breaking changes at any time before the first stable release.

Architecture

n8n on AWS architecture: users and webhook apps reach an Application Load Balancer that fronts an EKS cluster running separate main, webhook-processor, and worker pods, with RDS PostgreSQL, ElastiCache Redis, and S3 as managed backing services

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).

Usage

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.

Support

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.

Stability & versioning

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.PATCH0.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.MINOR0.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.

Compatibility

This module ships against specific provider majors. Notably:

  • AWS provider: ~> 6.0. Upgrading from a v0.1.x deployment (which pinned aws ~> 5.0) requires a one-time terraform plan -refresh-only followed by terraform apply -refresh-only to settle AWS provider 6.0's per-resource region attribute 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_release drift detection is stricter, so the first terraform plan after 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 via n8n_chart_version.
  • n8n application image: defaults to the chart's floating stable tag; production deployments should pin a specific version via n8n_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.

Out of scope

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 region argument 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_key output 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_enabled exposes 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.

Examples

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.

KMS key after terraform destroy

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.db uses name_prefix (not a fixed name), 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, run aws kms cancel-key-deletion --key-id <key-id> and import it back into state with terraform import aws_kms_key.db[0] <key-id>.

Prometheus metrics

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.

OpenTelemetry tracing

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.

Log streaming (Enterprise)

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.

Reference

Requirements

Name Version
terraform >= 1.9
aws ~> 6.0
helm ~> 3.0
kubernetes ~> 2.0
random ~> 3.0
time ~> 0.12

Providers

Name Version
aws ~> 6.0
helm ~> 3.0
kubernetes ~> 2.0
random ~> 3.0
time ~> 0.12

Modules

No modules.

Resources

Name Type
aws_acm_certificate.n8n resource
aws_acm_certificate_validation.n8n resource
aws_cloudwatch_log_group.rds_postgresql resource
aws_db_instance.n8n resource
aws_db_subnet_group.n8n resource
aws_eks_addon.ebs_csi resource
aws_eks_addon.pod_identity_agent resource
aws_eks_cluster.n8n resource
aws_eks_node_group.n8n resource
aws_eks_pod_identity_association.cluster_autoscaler resource
aws_eks_pod_identity_association.lbc resource
aws_eks_pod_identity_association.s3 resource
aws_elasticache_cluster.n8n resource
aws_elasticache_subnet_group.n8n resource
aws_iam_policy.cluster_autoscaler resource
aws_iam_policy.lbc resource
aws_iam_policy.s3 resource
aws_iam_role.cluster resource
aws_iam_role.cluster_autoscaler resource
aws_iam_role.ebs_csi resource
aws_iam_role.lbc resource
aws_iam_role.nodes resource
aws_iam_role.rds_enhanced_monitoring resource
aws_iam_role.s3 resource
aws_iam_role_policy_attachment.cluster_autoscaler resource
aws_iam_role_policy_attachment.cluster_policy resource
aws_iam_role_policy_attachment.ebs_csi resource
aws_iam_role_policy_attachment.lbc resource
aws_iam_role_policy_attachment.nodes_cni resource
aws_iam_role_policy_attachment.nodes_ecr resource
aws_iam_role_policy_attachment.nodes_worker resource
aws_iam_role_policy_attachment.rds_enhanced_monitoring resource
aws_iam_role_policy_attachment.s3 resource
aws_kms_alias.db resource
aws_kms_key.db resource
aws_route53_record.cert_validation resource
aws_route53_record.n8n_alias resource
aws_s3_bucket.n8n resource
aws_s3_bucket_public_access_block.n8n resource
aws_security_group.rds resource
aws_security_group.redis resource
helm_release.cluster_autoscaler resource
helm_release.keda resource
helm_release.lbc resource
helm_release.metrics_server resource
helm_release.n8n resource
kubernetes_horizontal_pod_autoscaler_v2.n8n_webhook resource
kubernetes_ingress_v1.n8n resource
kubernetes_namespace.n8n resource
kubernetes_secret.n8n resource
kubernetes_secret.n8n_db resource
kubernetes_storage_class_v1.gp3 resource
random_id.n8n_encryption_key resource
random_password.db_password resource
random_password.task_runner_token resource
time_sleep.wait_for_alb_cleanup resource
aws_caller_identity.current data source
aws_iam_policy_document.lbc data source
aws_lb.n8n data source

Inputs

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({
name = string
value = string
}))
[] 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

Outputs

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

Packages

 
 
 

Contributors