Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,63 @@ module "plane_infra" {
}
```

### Email Service NLB (optional)

Plane includes a built-in email service for receiving inbound email. To expose it externally via a dedicated AWS Network Load Balancer, set `enable_email_nlb = true`.

> **Important:** Deploy in two stages. The NLB targets EKS worker nodes via fixed NodePorts, so the EKS cluster and the email service must exist before the NLB is created.

**Stage 1 — Deploy infrastructure and the Plane application:**

```hcl
module "plane_infra" {
source = "git::https://github.com/makeplane/commercial-deployments.git//terraform?ref=main"

cluster_name = "plane-eks-cluster"
region = "us-west-2"
enable_aws_lb_controller = true
# enable_email_nlb = false # default — omit until Stage 2
}
```

```bash
terraform apply
aws eks update-kubeconfig --region us-west-2 --name plane-eks-cluster
kubectl apply -k kustomize/overlays/example # deploys email-service with fixed NodePorts
```

**Stage 2 — Create the NLB once the email service is running:**

```hcl
module "plane_infra" {
source = "git::https://github.com/makeplane/commercial-deployments.git//terraform?ref=main"

cluster_name = "plane-eks-cluster"
region = "us-west-2"
enable_aws_lb_controller = true
enable_email_nlb = true # creates NLB + registers EKS nodes in target groups
}
```

```bash
terraform apply
terraform output email_nlb_dns_name # use this value for your MX record
```

**Route53 MX record:**

After apply, create the following records in Route53 for your domain:

| Type | Name | TTL | Value |
|------|------|-----|-------|
| MX | `yourdomain.com` | 300 | `10 <email_nlb_dns_name>.` |
| TXT | `yourdomain.com` | 300 | `"v=spf1 include:<email_nlb_dns_name> ~all"` |

The NLB listens on:
- Port **25** — SMTP (inbound email from other mail servers)
- Port **465** — SMTPS
- Port **587** — Submission

Override defaults by passing `eks`, `cache`, `mq`, `opensearch`, `object_store`, or `db` objects. See [terraform/README.md](terraform/README.md) for all options.

### Outputs
Expand Down
6 changes: 4 additions & 2 deletions kustomize/components/email-service/service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ metadata:
app.kubernetes.io/name: plane-enterprise
app.kubernetes.io/component: email-service
spec:
type: LoadBalancer
externalTrafficPolicy: Local # Important for email servers
type: NodePort
selector:
app.kubernetes.io/name: plane-enterprise
app.kubernetes.io/component: email-service
ports:
- name: smtp
port: 25
targetPort: 10025
nodePort: 30025
protocol: TCP
- name: smtps
port: 465
targetPort: 10465
nodePort: 30465
protocol: TCP
- name: submission
port: 587
targetPort: 10587
nodePort: 30587
protocol: TCP
13 changes: 13 additions & 0 deletions terraform/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,19 @@ module "aws_lb_controller" {
depends_on = [module.eks]
}

module "email_nlb" {
count = var.enable_email_nlb ? 1 : 0
source = "./modules/email-nlb"

cluster_name = var.cluster_name
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnet_ids
node_security_group_id = module.eks.node_security_group_id
tags = var.tags

depends_on = [module.vpc, module.eks]
}

resource "random_password" "opensearch" {
length = 32
special = true
Expand Down
160 changes: 160 additions & 0 deletions terraform/modules/email-nlb/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# Discover the EKS node group ASG so we can attach target groups to it.
# EKS managed node groups tag their ASGs with eks:cluster-name automatically.
data "aws_autoscaling_groups" "eks_nodes" {
filter {
name = "tag:eks:cluster-name"
values = [var.cluster_name]
}
}

# Allow inbound traffic from NLB to EKS node NodePorts
resource "aws_security_group_rule" "smtp_nodeport" {
type = "ingress"
from_port = var.smtp_node_port
to_port = var.smtp_node_port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = var.node_security_group_id
description = "Email NLB -> SMTP NodePort"
}

resource "aws_security_group_rule" "smtps_nodeport" {
type = "ingress"
from_port = var.smtps_node_port
to_port = var.smtps_node_port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = var.node_security_group_id
description = "Email NLB -> SMTPS NodePort"
}

resource "aws_security_group_rule" "submission_nodeport" {
type = "ingress"
from_port = var.submission_node_port
to_port = var.submission_node_port
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = var.node_security_group_id
description = "Email NLB -> Submission NodePort"
}

# Target groups (instance target type — routes to NodePorts on worker nodes)
resource "aws_lb_target_group" "smtp" {
name = "${var.cluster_name}-email-smtp"
port = var.smtp_node_port
protocol = "TCP"
vpc_id = var.vpc_id
target_type = "instance"

health_check {
protocol = "TCP"
port = tostring(var.smtp_node_port)
healthy_threshold = 2
unhealthy_threshold = 2
interval = 10
}

tags = merge(var.tags, { Name = "${var.cluster_name}-email-smtp" })
}

resource "aws_lb_target_group" "smtps" {
name = "${var.cluster_name}-email-smtps"
port = var.smtps_node_port
protocol = "TCP"
vpc_id = var.vpc_id
target_type = "instance"

health_check {
protocol = "TCP"
port = tostring(var.smtps_node_port)
healthy_threshold = 2
unhealthy_threshold = 2
interval = 10
}

tags = merge(var.tags, { Name = "${var.cluster_name}-email-smtps" })
}

resource "aws_lb_target_group" "submission" {
name = "${var.cluster_name}-email-sub"
port = var.submission_node_port
protocol = "TCP"
vpc_id = var.vpc_id
target_type = "instance"

health_check {
protocol = "TCP"
port = tostring(var.submission_node_port)
healthy_threshold = 2
unhealthy_threshold = 2
interval = 10
}

tags = merge(var.tags, { Name = "${var.cluster_name}-email-sub" })
}

# Internet-facing Network Load Balancer
resource "aws_lb" "email" {
name = "${var.cluster_name}-email"
internal = false
load_balancer_type = "network"
subnets = var.subnet_ids

enable_deletion_protection = false

tags = merge(var.tags, { Name = "${var.cluster_name}-email-nlb" })
}

# Listeners
resource "aws_lb_listener" "smtp" {
load_balancer_arn = aws_lb.email.arn
port = 25
protocol = "TCP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.smtp.arn
}
}

resource "aws_lb_listener" "smtps" {
load_balancer_arn = aws_lb.email.arn
port = 465
protocol = "TCP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.smtps.arn
}
}

resource "aws_lb_listener" "submission" {
load_balancer_arn = aws_lb.email.arn
port = 587
protocol = "TCP"

default_action {
type = "forward"
target_group_arn = aws_lb_target_group.submission.arn
}
}

# Attach each target group to every EKS node ASG so nodes are registered
# automatically as the node group scales up/down.
resource "aws_autoscaling_attachment" "smtp" {
for_each = toset(data.aws_autoscaling_groups.eks_nodes.names)
autoscaling_group_name = each.value
lb_target_group_arn = aws_lb_target_group.smtp.arn
}

resource "aws_autoscaling_attachment" "smtps" {
for_each = toset(data.aws_autoscaling_groups.eks_nodes.names)
autoscaling_group_name = each.value
lb_target_group_arn = aws_lb_target_group.smtps.arn
}

resource "aws_autoscaling_attachment" "submission" {
for_each = toset(data.aws_autoscaling_groups.eks_nodes.names)
autoscaling_group_name = each.value
lb_target_group_arn = aws_lb_target_group.submission.arn
}
23 changes: 23 additions & 0 deletions terraform/modules/email-nlb/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
output "nlb_arn" {
description = "ARN of the email NLB"
value = aws_lb.email.arn
}

output "nlb_dns_name" {
description = "DNS name of the email NLB — use as MX record target"
value = aws_lb.email.dns_name
}

output "nlb_zone_id" {
description = "Route53 hosted zone ID of the NLB — use for alias records"
value = aws_lb.email.zone_id
}

output "target_group_arns" {
description = "Target group ARNs keyed by port name (smtp, smtps, submission)"
value = {
smtp = aws_lb_target_group.smtp.arn
smtps = aws_lb_target_group.smtps.arn
submission = aws_lb_target_group.submission.arn
}
}
43 changes: 43 additions & 0 deletions terraform/modules/email-nlb/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
variable "cluster_name" {
description = "EKS cluster name (used for naming and tagging)"
type = string
}

variable "vpc_id" {
description = "VPC ID where the NLB will be created"
type = string
}

variable "subnet_ids" {
description = "Public subnet IDs for the internet-facing NLB (one per AZ)"
type = list(string)
}

variable "node_security_group_id" {
description = "Security group ID of EKS worker nodes — NodePort ingress rules are added here"
type = string
}

variable "smtp_node_port" {
description = "Fixed NodePort for SMTP (port 25)"
type = number
default = 30025
}

variable "smtps_node_port" {
description = "Fixed NodePort for SMTPS (port 465)"
type = number
default = 30465
}

variable "submission_node_port" {
description = "Fixed NodePort for Submission (port 587)"
type = number
default = 30587
}

variable "tags" {
description = "Additional tags to apply to all resources"
type = map(string)
default = {}
}
5 changes: 5 additions & 0 deletions terraform/outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,9 @@ output "rds_password_secret_arn" {
description = "ARN of the RDS master user password secret in Secrets Manager"
value = module.rds.master_user_secret[0].secret_arn
sensitive = true
}

output "email_nlb_dns_name" {
description = "DNS name of the email NLB — add as MX record in Route53 (e.g. 10 <value>.). Null when enable_email_nlb = false."
value = var.enable_email_nlb ? module.email_nlb[0].nlb_dns_name : null
}
6 changes: 6 additions & 0 deletions terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ variable "enable_aws_lb_controller" {
default = false
}

variable "enable_email_nlb" {
description = "Create an internet-facing NLB for the email service (ports 25/465/587) and add NodePort ingress rules to EKS nodes"
type = bool
default = false
}

variable "cache" {
description = "ElastiCache Redis configuration"
type = object({
Expand Down