BLiNQ NetworksAWS Deployment All guides/Before you begin

AWS Deployment Guide

Deploy NetLiNQ on AWS with Terragrunt.

Terraform modules from BLiNQ build the VPC, the EKS cluster, the managed databases and the NetLiNQ platform inside your own AWS account. Three applies in order — secrets, base and SFTP, then NetLiNQ — with one inputs file describing the whole environment.

Terragrunt v0.76.3 EKS 1.35 Secrets Manager Atlas + RDS

Before you begin

What you need in place

This deployment builds a NetLiNQ EMS environment in your own AWS account with Terraform and Terragrunt: a VPC, an EKS cluster, managed databases and the NetLiNQ platform on top. BLiNQ provides the modules; you provide the account and the external services.

BLiNQ provides

The Terraform modules

A private module bundle (blinq-ems-tf.zip) and, for ECR-based deployments, the Jenkins pipeline that pushes application images into your registry. Ask BLiNQ support for the release that matches your order.

You provide

The AWS account and services

An AWS account with permissions to create and manage every resource in the modules, a MongoDB Atlas project, a Route 53 hosted zone, and an SMTP server for account email.

Plan these before you run anything

MongoDB AtlasAn account, an organization ID and a project. The project must already exist — the modules create the cluster inside it, not the project itself.
PostgreSQL (RDS)Created by the modules. Decide the database name Keycloak will use; keycloak is the usual answer.
SMTP serverHost, credentials and client ID/secret from your mail provider. NetLiNQ sends account setup and maintenance mail through it.
Region, AZs, sizingRegion and availability zones, EKS node instance types and counts, and the VPC CIDR plan. Changing these later means rebuilding infrastructure.
Route 53 hosted zoneCreate the hosted zone for your domain yourself and keep its zone ID — the modules write DNS records into it.

Decide first

Pulling images from your own registry

NetLiNQ's container images can come from BLiNQ's registry or from an ECR registry in your own AWS account. Decide this before you deploy — it changes what you set up and what you run.

private-public

Your choice

The cluster can reach public registries, so BLiNQ's registry works out of the box. Many customers still prefer their own ECR, for control over what runs in their account.

private-private

The only option

There is no NAT gateway and no route to the internet, so images must come from your own registry. See Private deployments.

Choosing your own registry means three things, in this order:

1 · Create the IAM user
jenkins-ecr, so BLiNQ's pipeline can push the images into your registry. Covered below.
2 · Rewrite the values files
Run generate_ecr_values.sh so every chart pulls from your registry rather than BLiNQ's.
3 · Set the registry in inputs
Put the same URI in privateRegistryUri, so the Terraform modules agree with the values files.

Rewriting the values files

generate_ecr_values.sh lives in the root of the configuration repository. Run it before the NetLiNQ apply:

generate_ecr_values.sh
# usage: ./generate_ecr_values.sh ecrRegistryId [awsRegion] [repoPrefix] [publicAwsEcrUri]
$ ./generate_ecr_values.sh 123456789012 "us-east-1"
$ ./generate_ecr_values.sh 123456789012 "us-east-1" "acmecorp/blinq"
ecrRegistryIdRequired. Your AWS account ID — the registry ID in front of .dkr.ecr.
awsRegionDefaults to us-east-1.
repoPrefixPath prefix inside ECR, default blinq/netlinq. It must match the ECR_REPO_PREFIX used when the images were imported. Pass "" for images sitting at the registry root.
publicAwsEcrUriOptional. Left out, it is assumed to be the same as the private registry URI.

It needs envsubst on the PATH, reads its templates from ecr_values_templates/, and writes the generated files back over the real values files — aws_values_files/, keycloak/oci/, monitoring/, secrets/ and the repository root. extract-aws-images.sh, in the same directory, lists the images that need mirroring in the first place.

Getting the images into your registry

Rewriting the values files only changes where the cluster looks. The images still have to arrive there. BLiNQ sends an import package for this — the ecr-to-external-ecr-transfer tooling — which pulls the release images from BLiNQ's registry and pushes them into yours.

You never receive BLiNQ credentials. Your machine briefly assumes a read-only role in BLiNQ's account to pull, then pushes with your own credentials. Access is granted by role trust, which BLiNQ enables for your account once and can revoke.
1 · Fill in the vars file
In vars/customer.vars.sh, set CUSTOMER_ECR_REGION and, for a local machine, AWS_PROFILE. The BLiNQ-side values arrive pre-filled — leave them alone.
2 · Create the IAM role
Run ./setup-iam.sh once. It creates BlinqImageImportRole in your account and writes customer-setup-output.sh. Send that file to your BLiNQ contact and wait for confirmation before going further.
3 · Import
./import-images.sh --dry-run first, then without the flag. It creates any missing ECR repositories, retries a failed image up to three times, and writes the result to images/imported-images.txt.
import package
$ chmod +x setup-iam.sh import-images.sh
$ ./setup-iam.sh                 # once; send customer-setup-output.sh to BLiNQ
$ ./import-images.sh --dry-run   # preview, changes nothing
$ ./import-images.sh

It needs Docker and AWS CLI v2, and a profile with access to your own account only. Prefer to create the role by hand? It needs sts:AssumeRole on the role ARN BLiNQ provides, ecr:GetAuthorizationToken, and push rights on your own registry:

import role policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AssumeBlinqReadRole",
      "Effect": "Allow",
      "Action": "sts:AssumeRole",
      "Resource": "<role ARN provided by BLiNQ>"
    },
    {
      "Sid": "ECRLogin",
      "Effect": "Allow",
      "Action": ["ecr:GetAuthorizationToken"],
      "Resource": "*"
    },
    {
      "Sid": "PushToYourECR",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage",
        "ecr:DescribeRepositories",
        "ecr:CreateRepository"
      ],
      "Resource": "arn:aws:ecr:YOUR_REGION:YOUR_ACCOUNT_ID:repository/*"
    }
  ]
}
Use the same prefix on both sides. ECR_REPO_PREFIX in vars/customer.vars.sh decides the namespace the images land under; it is the value you then pass to generate_ecr_values.sh as repoPrefix. Set one, use it in both places.

When the import fails

not authorized to perform: sts:AssumeRole
BLiNQ has not granted your role access yet, or the role is missing the AssumeBlinqReadRole permission.
not authorized to perform: ecr:InitiateLayerUpload
The push permission is missing — check the account ID and region in the policy's resource.
ExpiredTokenException, or no basic auth credentials on push
A token expired mid-run. Re-run the script; layers already pushed are not uploaded again.
Still failing after three retries
Usually the network. Re-run; it picks up where it left off.

Telling Terraform the same thing

inputs.hcl
privateRegistryUri = "<account-id>.dkr.ecr.us-east-1.amazonaws.com/blinq/netlinq"
The prefix has to match in all three places. The repoPrefix you pass the script, the ECR_REPO_PREFIX the images were imported under, and the path on the end of privateRegistryUri are the same string. A mismatch shows up as image pull failures after a successful apply.

Staying on BLiNQ's registry instead? Skip this section and the IAM user below, and leave privateRegistryUri empty.

Prepare

Install the tooling

Install these on the machine you will deploy from, at the versions this release was tested with:

Terragruntv0.76.3
Terraformv1.14.8
AWS CLIaws-cli/2.17.52
kubectl1.35, matching the EKS version the modules create
verify
$ terragrunt --version
$ terraform --version
$ aws --version
$ kubectl version --client

Prepare the host

Terragrunt deployments expect IPv6 disabled and forwarding on. Check what the host has now:

check
$ sudo sysctl -p

No output means the settings are not there yet. Add them, then run sudo sysctl -p again — it should echo the four lines back:

sysctl
$ sudo tee -a /etc/sysctl.conf > /dev/null <<'EOF'
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
net.ipv4.ip_forward = 1
EOF
$ sudo sysctl -p
packages and credentials
$ sudo apt update && sudo apt upgrade -y
$ aws configure   # sign in to the account you are deploying into
The credentials you configure here must be able to create and manage every resource the modules touch. The modules themselves are the authoritative list of what that means — check them before you assume an existing role is enough.

Prepare · ECR deployments only

Create the image-transfer IAM user

This step applies when you host NetLiNQ images in your own ECR. It lets BLiNQ's Jenkins pipeline push application images into your registry, so containers run from your account rather than being pulled from ours at deploy time.

Create an IAM user named jenkins-ecr with no console access. Generate an access key ID and secret access key, and hand them to BLiNQ for the pipeline. Attach the three inline policies below.

ecr-docker-transfer-netlinq
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRAuthToken",
      "Effect": "Allow",
      "Action": "ecr:GetAuthorizationToken",
      "Resource": "*"
    },
    {
      "Sid": "ECRDockerPullAndPush",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:GetDownloadUrlForLayer",
        "ecr:BatchGetImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload",
        "ecr:PutImage"
      ],
      "Resource": "arn:aws:ecr:*:*:repository/blinq/netlinq/*"
    }
  ]
}

Authentication is account-wide; pull and push are confined to the blinq/netlinq/* namespace.

ecr-create-on-push-netlinq
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRCreateRepositoryOnPush",
      "Effect": "Allow",
      "Action": "ecr:CreateRepository",
      "Resource": "arn:aws:ecr:*:*:repository/blinq/netlinq/*"
    }
  ]
}

Lets the pipeline create a repository the first time it pushes an image that has none.

ecr-public-auth
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRPublicRegistryAuth",
      "Effect": "Allow",
      "Action": [
        "ecr-public:GetAuthorizationToken",
        "sts:GetServiceBearerToken"
      ],
      "Resource": "*"
    }
  ]
}

Lets the pipeline pull base images from AWS public registries.

Prepare

Unpack the modules and fix the paths

BLiNQ sends the Terraform modules as a private ZIP. Extract it, then point every Terragrunt file at where you put it.

The deployment tree ships two templates to copy from. They differ only in their inputs, not in their structure:

deployment/exampleThe public-facing deployment: AWS Transfer Family SFTP (sftp_server_type = "aws-tf"), external-dns on, a Route 53 zone ID filled in.
deployment/example-privateThe private deployment: enable_sftp = false with sftp_server_type = "none", external-dns off, Kafka external access on.
unpack
$ unzip blinq-ems-tf.zip -d /path/to/modules
$ cd /path/to/modules
$ pwd   # this is the path you paste below
The double slash matters. Terragrunt splits the source at //: everything before it is the repository root, everything after is the module inside it. Write …/modules//awstransfer, not …/modules/awstransfer.
example path
/home/blinq/terragrunt/blinq-ems-autom-configs/terragrunt/tf-mods/7.0.2_3/modules//awstransfer

Search the deployment tree for <your-path-here> and replace every occurrence with your pwd output. These are the files and lines that carry it:

example/base/main.tfFour module sources: s3, mongo, rds and acm (lines 107, 236, 287 and 314 in this release)
example/netlinq/main.tfnetlinq_blue, line 12
example/netlinq_green/main.tfnetlinq_green, line 13
example/secrets/main.tfsecrets-manager, line 15
example/sftp/main.tfawstransfer, line 36

Line numbers shift between releases — searching for the placeholder is the reliable way to find them all. example-private carries the same five files at slightly different lines.

One of them ships with a single slash. In example-private/secrets/main.tf the source reads <your-path-here>/modules/secrets-manager. Add the second slash when you edit it, or Terragrunt treats the whole path as the repository root and finds no module.

Step 1

Deploy the secrets

Secrets go in first, because everything deployed afterwards reads them from AWS Secrets Manager.

secrets
$ terragrunt run-all plan  --queue-include-dir=secrets --source-update
$ terragrunt run-all apply --queue-include-dir=secrets --source-update

The apply creates the secrets empty. Fill in every value in Secrets Manager before you go further.

Finding them. Names carry a prefix and a suffix so one AWS account can hold several deployments. external_mongo_admin_secret appears as netlinq_external_mongo_admin_secret_lnt — prefix netlinq_, suffix _lnt. Search on the middle part.

MongoDB Atlas

external_mongo_admin_secretAtlas admin, used for the initial setup. Keys: mongo-username, mongo-password.
external_mongo_emsdb_secretRead/write on the netlinq database.
external_mongo_spcdb_secretRead/write on the spc database.
external_mongo_spckpidb_secretRead/write on the SPC KPI database.

Keycloak and RDS

external_keycloak_admin_clientKeycloak admin console account, used to create the TOC and Diesel clients. Keys: keycloak-username, keycloak-password.
external_rds_keycloak_clientThe PostgreSQL account Keycloak uses for its own configuration. Keys: pg-username, pg-password.

SMTP

external_smtp_default_user_clientAccount setup and maintenance email. Keys: smtp-username, smtp-password, smtp-client-id, smtp-client-secret — the last two come from your mail provider.

EMS administrator

external_ems_aoa_clientThe admin-of-admins account you log in with first. Keys: ems-username, ems-password.

Rules for these values

Usernames
Letters only — A–Z and a–z.
Passwords
Must start with a letter. After that: A-Z a-z 0-9 !@#$%^&*()_+-={}[]|;:'",.<>/?~. Sixteen characters or more is recommended.
RDS master password
Printable ASCII only, and never /, @, " or a space.
SFTP admin
The username comes from inputs.hcl; the password is generated. Its secret is named <prefix>-SFTP/username-<suffix>.
Changing a secret later is a two-step job. Change it at the source first (Atlas, RDS), then in Secrets Manager. The running platform picks the new value up on a refresh interval measured in hours, not immediately. A shorter interval is planned for a future release.

Step 2

Fill in inputs.hcl

One file describes the whole deployment. Copy deployment/example/inputs.hcl (or example-private) and edit it — that file, not this page, is the complete template.

The sections below are the values you must decide on, not the whole file. The shipped inputs.hcl runs to roughly 350 lines and also carries the RDS instance settings, the Keycloak node group and autoscaling block, MongoDB Atlas sizing and PrivateLink options, a long list of enable_* feature flags, the blue/green upgrade inputs, and the networking block. Work from the file and change what applies to you; leave the rest at its shipped default.

General

inputs.hcl
inputs = {
  state_bucket_name    = "prod-terragrunt-state-ems-1e"   # S3 bucket holding Terraform state
  state_dynamodb_table = "terragrunt-lock-table-1e"       # DynamoDB table for state locking

  enable_sftp          = true
  enable_netlinq_green = false                            # greenfield upgrades only
  resource_prefix      = "netlinq"                        # prefix on every AWS resource name
  aws_region           = "us-east-1"
  netlinq_name         = "blue-t3"                        # label used in names and tags
  endpoint             = "example.blinqnetworks.org"
  emsui_subdomain      = "emsui"                          # emsui.example.blinqnetworks.org
  sftp_subdomain       = "sftp"
  mongo_db_name        = "tocdb"
  rds_db_name          = "keycloak"
  privateRegistryUri   = ""                               # your ECR URI, for ECR deployments
}
state_bucket_nameS3 bucket holding Terraform state, so it survives and can be shared across environments.
state_dynamodb_tableDynamoDB table for state locking, which stops two applies from running at once.
enable_sftp / sftp_server_typeWhich SFTP service gets deployed. The repo uses three combinations: true + "aws-tf" for AWS Transfer Family, true + "oem" for a local OEM server, and false + "none" for no SFTP at all.
enable_netlinq_greenLeave false. Turn it on only for greenfield deployments during an upgrade.
resource_prefixPrefix applied to AWS resource names, so one account can carry several deployments.
endpointThe EMS endpoint URL, for example example.blinqnetworks.org.
emsui_subdomainSubdomain for the EMS UI. emsui gives emsui.example.blinqnetworks.org.
sftp_subdomainSubdomain for SFTP access.
mongo_db_nameMongoDB database NetLiNQ EMS uses, for example tocdb.
rds_db_namePostgreSQL database Keycloak uses, for example keycloak.
privateRegistryUriPrivate ECR URI for image pulls, for example <account-id>.dkr.ecr.us-east-1.amazonaws.com/blinq/netlinq.

Cluster

NetLiNQ blue cluster
EMS_SERIALNUMBER  = "EM-01234564879"
SPC_SERIALNUMBER  = "SP0-0000000000"
aws_region        = "us-east-1"
access_key        = "<loki-s3-access-key-id>"   # Loki's S3 user
secret_access_key = "<secret>"
netlinq_name      = "blue-t6"
endpoint          = "ems.example.org"
emsui_subdomain   = "emsui"
sftp_subdomain    = "sftp"
mongo_db_name     = "tocdb"
rds_db_name       = "keycloak"

k8s_version                  = "1.35"            # EKS version
eks_nodegroup_instance_types = ["m6a.xlarge"]
eks_nodegroup_capacity_type  = "ON_DEMAND"       # or SPOT
eks_nodegroup_max_size       = 9
eks_nodegroup_desired_size   = 7
eks_nodegroup_min_size       = 5
eks_nodegroup_max_unavailable_percentage = 30
eks_nodegroup_volume_size    = 30

spc_chart_version           = "2.6.1"
netlinq_chart_version       = "7.0.1"
ekf_stack_version           = "0.17.7"
netlinq_stack_version       = "2.25.29"
keycloak_stack_version      = "1.8.10"
upgrade_stack_chart_version = "0.3.5"

zone_id      = "<route53-hosted-zone-id>"
spc_tls_dir  = "PATH_TO_TLS_DIR"             # unzipped SPC TLS certs
Chart versions change every release. The ones above are what deployment/example/inputs.hcl carries today — always use the versions BLiNQ ships with your package rather than copying them from any document. zone_id is the Route 53 hosted zone you created yourself; the modules write DNS records into it.

The access_key and secret_access_key belong to a user created only so Loki and Promtail can write logs to S3. Give it nothing more than this:

loki S3 policy
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::prefix*",
        "arn:aws:s3:::prefix*/*"
      ]
    }
  ]
}

MongoDB Atlas

atlas
public_key             = ""                 # Atlas API public key
private_key            = ""                 # Atlas API private key
atlas_project_name     = "prod-ems-mongo"   # must already exist
cluster_name           = ""
atlas_region           = "US_EAST_1"
aws_account_id         = ""                 # AWS account linked to Atlas
atlasorgid             = ""
atlas_vpc_cidr         = "10.105.0.0/21"    # Atlas-side network
mongo_db_major_version = "8.0"
cluster_type           = "REPLICASET"
The Atlas project is yours to create. The modules build the cluster inside an existing project; they do not create the project. The API keys are used only for that cluster creation.

VPC and SFTP

network
sftp_bucket_name   = "ems-sftp"
sftp_name          = "ems-root-sftp"
blinqroot_username = "sftp-admin"
subnet_cidr        = "10.12.5.0/28"
aws_mode           = true                  # always true
az                 = "us-east-1a"

s3_name            = "ems-internal"
vpc_name           = "ems-vpc"
vpc_cidr           = "10.12.0.0/20"
aws-zone-count     = 3
private_subnets    = ["10.12.1.0/24", "10.12.2.0/24", "10.12.3.0/24"]
public_subnets     = ["10.12.10.0/28", "10.12.11.0/28", "10.12.12.0/28"]

# only if spare space remains in the VPC CIDR
enabled_additional_private_subnet = false
additional_private_subnet_cidr    = []

The shipped example uses a different plan from the one below: vpc_cidr = "10.121.0.0/20", private subnets as /23 and public as /27, with a second set of green subnets reserved inside the same CIDR for blue/green upgrades. Either plan works; what matters is that nothing overlaps. The sizing guidance from the runbook:

VPC
10.12.0.0/20
Private subnets
EKS worker nodes: 10.12.1.0/24 (us-east-1a), 10.12.2.0/24 (1b), 10.12.3.0/24 (1c)
Public subnets
Load balancers: /24 blocks if you want room to grow, /28 if limiting public IPs matters more
SFTP subnet
A fresh block that overlaps nothing else, for example 10.12.13.0/28

blinqroot_username is the SFTP root account; its password is generated for you. aws_mode stays true — it is what makes NetLiNQ configure itself for AWS. Set enabled_additional_private_subnet only when you need more subnet capacity and the VPC CIDR still has unused space; the extra CIDRs must not overlap anything existing.

Steps 3 and 4

Deploy

Two applies, in this order. Plan first each time and read what it intends to do.

Base and SFTP

base + sftp
$ terragrunt run-all plan  --queue-include-dir=base --queue-include-dir=sftp \
    --source-update
$ terragrunt run-all apply --queue-include-dir=base --queue-include-dir=sftp \
    --source-update

NetLiNQ

This builds the EKS infrastructure and deploys NetLiNQ EMS into the cluster, leaving the modules you already applied alone.

netlinq
$ terragrunt run-all plan  --queue-exclude-dir=sftp --queue-exclude-dir=base \
    --queue-exclude-dir=secrets --source-update
$ terragrunt run-all apply --queue-exclude-dir=sftp --queue-exclude-dir=base \
    --queue-exclude-dir=secrets --source-update
When this apply finishes, NetLiNQ is deployed in your AWS environment.

After the deploy

Check that it worked

Cluster health
Run your usual EKS health checks against the new cluster.
Pods running
kubectl get pods -n netlinq — everything should settle into Running.
Application reachable
Log in to the EMS UI at <emsui_subdomain>.<endpoint> with the external_ems_aoa_client account.

If something is wrong, pod logs are the first place to look. Contact EMS support if they do not explain it.

Variant

Private deployments

The deployment tree ships a second template, deployment/example-private, for running NetLiNQ on EKS inside a private VPC. One input decides the shape of the whole environment:

inputs.hcl
deployment_mode = "private-private"   # or "private-public"
private-publicThe default. Private subnets for the cluster, public subnets for the load balancers, so management interfaces are reachable from outside.
private-privateNothing is publicly reachable. Public subnets are ignored, and the deployment has no route to the internet at all.
private-private builds no NAT gateway and uses no internet gateway. In base/main.tf both enable_nat_gateway and single_nat_gateway are set from deployment_mode == "private-public", and the IGW is passed as empty otherwise. Everything the cluster needs — images, AWS APIs, Atlas — must therefore arrive over VPC endpoints or PrivateLink. Plan that before you apply, not after.

Images must come from your own registry

A private-private cluster has no route to a public registry, so the registry step is not optional here. Follow Pulling images from your own registry above, then come back: generate_ecr_values.sh must have been run and privateRegistryUri set before the NetLiNQ apply.

VPC endpoints

enable_vpc_endpoints = trueRecommended. The base module creates the endpoints the cluster needs, with their own security group accepting HTTPS from the VPC.
enable_vpc_endpoints = falseAssumes your VPC already has them. You are responsible for every service in the list below.

What true creates, from base/main.tf: one Gateway endpoint for s3, attached to the private route tables, and Interface endpoints with private DNS for:

interface endpoints
autoscaling   ecr.api   ecr.dkr          ec2   ec2messages   elasticloadbalancing
sts           kms       secretsmanager   sqs   ssm           eks
Interface endpoints allow one subnet per availability zone, so they are placed in the blue private subnets only — not blue and green together. Keep that in mind when adding a green environment to an existing private VPC.
That list is exactly what gets created — nothing else. CloudWatch Logs and EFS are not among them. If a workload in a private deployment needs an AWS service that is not on the list, add that endpoint yourself; with no NAT there is no other route to it.

DNS records are manual here

A private deployment runs with enable_external_dns = false, so nothing writes DNS records for you. Create the records for the EMS UI and the other endpoints by hand after the apply, pointing at the load balancers the deployment creates.

Certificates

Private deployments still use ACM certificates — this does not change with the deployment mode.

enable_acm = trueCreates a new ACM certificate.
enable_acm = falseUses one you already have — pass its ARN in existing_acm_certificate_arn. It must be in the deployment region and valid for the domains in use.

MongoDB Atlas

connectivity_type"privateLink" keeps the connection on the AWS network and is the preferred option. "peering" sets up VPC peering, which traverses the internet.
enable_mongo_vpc_endpointtrue creates the endpoint for PrivateLink. Set it false only when supplying an existing one in mongo_vpc_endpoint_id.
create_network_containerAtlas needs a network container for both PrivateLink and peering. true for a new Atlas project; false only if you have verified one already exists.
create_privatelinkStarts the PrivateLink connection.
enable_endpoint_acceptAccepts that connection on the Atlas side. Set false when using peering.

Like the interface endpoints, the Atlas PrivateLink endpoint takes one subnet per AZ from the blue private subnets.

SFTP

private-public
enable_sftp = true with sftp_server_type = "aws-tf" — AWS Transfer Family.
private-private
Not supported in this release. Use enable_sftp = false and sftp_server_type = "none".

Tags your existing subnets must already carry

When the modules create the VPC they tag the subnets themselves. With create_vpc = false nothing tags them for you, and both the load balancer controller and Karpenter find subnets by tag. Check these are present before you apply:

kubernetes.io/role/internal-elb = 1On every private subnet. This is what the modules set when they own the VPC.
kubernetes.io/role/elb = 1On every public subnet. Only applies in private-public mode; public subnets are unused in private-private.
karpenter_subnet_selector_tagsWhatever tag set you put in inputs.hcl must exist on the subnets you want Karpenter to provision nodes into. The example ships it commented out, so decide the tag deliberately and apply it to those subnets.
inputs.hcl
karpenter_subnet_selector_tags = {
  "karpenter.sh/discovery" = "netlinq-ems-blue"
}

Verify what is actually on the subnets before applying:

check the tags
$ aws ec2 describe-subnets --subnet-ids subnet-aaa subnet-bbb subnet-ccc \
    --query "Subnets[].{Id:SubnetId,Tags:Tags}"

Restricting who reaches the load balancers

The ingress load balancers filter on source IP, and the shipped default allows everything. In a private deployment, narrow it:

inputs.hcl
allowed_ingress_source_cidrs = ["10.0.0.0/8"]   # default is ["0.0.0.0/0"]

Variant

Deploying into an existing VPC

To reuse a VPC you already have, tell the modules about it before Step 4 and let them skip VPC creation.

In example/base/output.tf, replace the computed outputs with your real IDs:

example/base/output.tf
output "public_subnets" {
  value = ["subnet-23421312", "subnet-1232132312", "subnet-123131321"]
}

output "igw_id" {
  description = "The ID of the Internet Gateway"
  value       = "igw-12313321321321232"
}

Then in example/inputs.hcl, point a new state bucket at this configuration so it is tracked separately, and switch VPC creation off:

example/inputs.hcl
create_vpc = false

# read only when create_vpc is false
existing_vpc_id                  = "vpc-asdsadsdsad"
existing_main_route_table_id     = "rtb-asdsadadsadad"
existing_private_route_table_ids = ["rtb-asdsadsa"]
existing_vpc_cidr                = "10.121.0.0/16"
existing_vpc_subnets             = ["subnet-asdadasdsad", "subnet-asdsadasd", "subnet-asdsaddsad"]
existing_vpc_name                = "netlinq-ems-vpc"

# second path: add a CIDR block when the existing one is full
enabled_additional_vpc_cidr        = false
additional_vpc_cidr                = ""
additional_vpc_private_subnet_cidr = []
existing_vpc_subnets takes private subnet IDs — the private ones from that VPC, not the public ones.

Check the rest of the configuration still makes sense against the existing network, then start again from Step 4.

Teardown

Destroying the deployment

These commands delete infrastructure and the data on it. Only run them when the environment is genuinely finished with: back up anything critical first and stop active workloads.

Delete the Kafka topics first, before any destroy that removes the cluster:

first
$ kubectl delete kafkatopic --all -n netlinq

Pick the scope

NetLiNQ module only
$ terragrunt run-all destroy --queue-exclude-dir=sftp --queue-exclude-dir=base \
    --queue-exclude-dir=secrets --source-update
SFTP module only
$ terragrunt run-all destroy --queue-exclude-dir=base --queue-exclude-dir=secrets \
    --source-update
everything except secrets
$ terragrunt run-all destroy --queue-exclude-dir=secrets --source-update
everything
$ terragrunt run-all destroy

Keeping the VPC

To tear the base module down piece by piece and leave the VPC standing, target each sub-module:

base sub-modules
$ terragrunt run-all destroy --queue-include-dir=base --source-update -target=module.mongo
$ terragrunt run-all destroy --queue-include-dir=base --source-update -target=module.rds
$ terragrunt run-all destroy --queue-include-dir=base --source-update -target=module.acm
$ terragrunt run-all destroy --queue-include-dir=base --source-update -target=module.s3

Secrets

Secrets are left out of every command above on purpose. Destroy them only when you are sure nothing will be redeployed against them:

secrets only
$ terragrunt run-all destroy --queue-exclude-dir=base --queue-include-dir=secrets \
    --source-update