Thorn Tech Marketing Ad
Skip to main content
Version: 1.2.0

HA Terraform Template for AWS

TLDR

Purpose: Deploy StorageLink in a highly available configuration on AWS using Terraform.

Architecture: Network Load Balancer + Auto Scaling Group (2+ EC2 instances) + RDS PostgreSQL 16 + Secrets Manager

Deployment:

  1. Create storagelink-ha-terraform.tf and terraform.tfvars
  2. Configure variables (region, key pair, admin credentials, IP restrictions)
  3. Run: terraform init && terraform apply

Key Variables:

  • ssh_admin_cidr: Your IP + /32 for SSH access
  • web_admin_cidr: Your IP + /32 for web admin access (ports 80/443)
  • web_admin_username/web_admin_password: Web admin credentials

Prerequisites: Subscribe to StorageLink on AWS Marketplace before deploying

Overview

You can deploy StorageLink in an HA configuration using Terraform. This template provisions a Network Load Balancer, an Auto Scaling Group spanning two availability zones, and an RDS PostgreSQL database. The application tier is HA across AZs; the RDS instance is single-AZ by default to keep recurring spend low. Enable multi_az = true on aws_db_instance.main if you need database-layer failover.

This article covers deploying an HA deployment of StorageLink on AWS. The Terraform template is provided as an example, so feel free to further customize it for your business case.

Note: Make sure you are subscribed to StorageLink on AWS Marketplace before deploying the Terraform template or you will run into errors.

Resources Created

ResourceDescription
VPCNew VPC with public and private subnets across two availability zones
Internet GatewayProvides outbound internet access from public subnets
NAT GatewayAllows private subnet instances to reach the internet
Network Load BalancerRoutes traffic on ports 22, 80, and 443 across instances
Elastic IPStatic public IP address assigned to the NLB
Auto Scaling GroupMaintains the desired number of StorageLink instances (min 2)
ASG Lifecycle HookHolds terminating instances in Terminating:Wait while the NLB drains in-flight traffic (graceful failover)
Launch TemplateDefines the instance configuration and startup script
Security GroupsRestricts inbound access to SSH and web admin ports
RDS PostgreSQLStores StorageLink user and configuration data (single-AZ by default; flip multi_az = true for failover)
Secrets ManagerStores the auto-generated RDS password securely
IAM Role + Instance ProfileGrants EC2 instances access to S3, Secrets Manager, and CloudWatch
CloudWatch Log GroupCaptures StorageLink application logs

Running the Template

This article contains two files:

  • storagelink-ha-terraform.tf
  • terraform.tfvars

Create these two files using the contents below. Make your adjustments to the terraform.tfvars file. Then run:

terraform init
terraform plan

When you are ready to deploy, run:

terraform apply

Deployment typically takes 15–20 minutes, with the RDS instance taking the longest.

When deployment completes, Terraform outputs the load balancer DNS name (load_balancer_dns) and static IP (load_balancer_ip). Use either to access the StorageLink web admin interface:

https://<load_balancer_dns>

Sign in with the web_admin_username / web_admin_password you set in terraform.tfvars. Run terraform output to see all deployment values.

How the AMI is selected

You do not need to look up an AMI ID. The template resolves the latest StorageLink 1.2.0 AMI automatically via the AWS Marketplace SSM parameter /aws/service/marketplace/prod-6fb6mek7ta6vi/1.2.0, which is region-aware — the same Terraform works in any AWS region where StorageLink is published.

To pin a different version line, edit the data "aws_ssm_parameter" "storagelink_ami" block in the .tf file and change the path suffix (e.g. .../1.3.0).

Deleting the Stack

To remove all resources:

terraform destroy
note

The RDS instance has deletion protection disabled in this template to allow terraform destroy to complete without manual intervention. If you enabled deletion protection manually after deployment, disable it in the AWS Console before running terraform destroy.

Variable Reference

VariableDefaultDescription
regionAWS region to deploy into
stack_namePrefix for all resource names
instance_typet3.mediumEC2 instance type
disk_volume_size32Root EBS volume size in GB
key_pairExisting EC2 Key Pair name
desired_capacity2Number of instances in the ASG
vpc_cidr192.168.1.0/24CIDR block for the new VPC
web_admin_cidrCIDR allowed to access ports 80/443
ssh_admin_cidrCIDR allowed SSH access on port 22
web_admin_usernameInitial web admin username
web_admin_passwordInitial web admin password (min 12 chars)
bucket_accessrestrictedS3 IAM access mode (restricted or open)
db_instance_classdb.t3.microRDS PostgreSQL instance class

Terraform file contents

terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
random = {
source = "hashicorp/random"
version = "~> 3.0"
}
}
}

provider "aws" {
region = var.region
}

# ---------------------------------------------------------------------------
# Variables
# ---------------------------------------------------------------------------

variable "region" {
description = "AWS region to deploy into"
type = string
}

variable "stack_name" {
description = "Prefix applied to all resource names (lowercase, hyphens only)"
type = string
}

variable "instance_type" {
description = "EC2 instance type. Use t3.medium for testing, m5.large for production"
type = string
default = "t3.medium"
}

variable "disk_volume_size" {
description = "Root EBS volume size in GB (minimum 32)"
type = number
default = 32
}

variable "key_pair" {
description = "Name of an existing EC2 Key Pair for SSH access"
type = string
}

variable "desired_capacity" {
description = "Desired number of StorageLink instances in the Auto Scaling Group"
type = number
default = 2
}

variable "vpc_cidr" {
description = "CIDR block for the new VPC"
type = string
default = "192.168.1.0/24"
}

variable "web_admin_cidr" {
description = "CIDR range allowed to access the web admin interface (ports 80/443). Use x.x.x.x/32 for a single IP."
type = string
}

variable "ssh_admin_cidr" {
description = "CIDR range allowed SSH access (port 22). Do not use 0.0.0.0/0."
type = string
}

variable "web_admin_username" {
description = "Initial web admin username"
type = string
}

variable "web_admin_password" {
description = "Initial web admin password (minimum 12 characters)"
type = string
sensitive = true
}

variable "bucket_access" {
description = "S3 access mode: 'restricted' limits access to storagelink-prefixed buckets; 'open' grants full S3 access"
type = string
default = "restricted"
}

variable "db_instance_class" {
description = "RDS instance class"
type = string
default = "db.t3.micro"
}

# ---------------------------------------------------------------------------
# Data Sources
# ---------------------------------------------------------------------------

data "aws_availability_zones" "available" {}

data "aws_caller_identity" "current" {}

# StorageLink AMI is resolved automatically from the AWS Marketplace
# SSM parameter. The parameter is region-aware, so this template works
# in any AWS region where StorageLink is published — no per-region AMI
# IDs to look up. Pin to a different version line by changing the path
# (e.g. .../1.3.0).
data "aws_ssm_parameter" "storagelink_ami" {
name = "/aws/service/marketplace/prod-6fb6mek7ta6vi/1.2.0"
}

# ---------------------------------------------------------------------------
# Networking
# ---------------------------------------------------------------------------

resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
tags = { Name = "${var.stack_name}-vpc" }
}

resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = { Name = "${var.stack_name}-igw" }
}

# Public subnets (NLB)
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, 0)
availability_zone = data.aws_availability_zones.available.names[0]
map_public_ip_on_launch = true
tags = { Name = "${var.stack_name}-public-a" }
}

resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, 1)
availability_zone = data.aws_availability_zones.available.names[1]
map_public_ip_on_launch = true
tags = { Name = "${var.stack_name}-public-b" }
}

# Private subnets for RDS
resource "aws_subnet" "private_db_a" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, 2)
availability_zone = data.aws_availability_zones.available.names[0]
tags = { Name = "${var.stack_name}-private-db-a" }
}

resource "aws_subnet" "private_db_b" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, 3)
availability_zone = data.aws_availability_zones.available.names[1]
tags = { Name = "${var.stack_name}-private-db-b" }
}

# Private subnets for EC2 instances
resource "aws_subnet" "private_app_a" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, 4)
availability_zone = data.aws_availability_zones.available.names[0]
tags = { Name = "${var.stack_name}-private-app-a" }
}

resource "aws_subnet" "private_app_b" {
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 4, 5)
availability_zone = data.aws_availability_zones.available.names[1]
tags = { Name = "${var.stack_name}-private-app-b" }
}

# Public route table
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = { Name = "${var.stack_name}-public-rt" }
}

resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}

resource "aws_route_table_association" "public_b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}

# NAT Gateway for private subnets
resource "aws_eip" "nat" {
domain = "vpc"
tags = { Name = "${var.stack_name}-nat-eip" }
}

resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public_a.id
tags = { Name = "${var.stack_name}-nat" }
}

resource "aws_route_table" "private_app_a" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = { Name = "${var.stack_name}-private-app-a-rt" }
}

resource "aws_route_table" "private_app_b" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = { Name = "${var.stack_name}-private-app-b-rt" }
}

resource "aws_route_table_association" "private_app_a" {
subnet_id = aws_subnet.private_app_a.id
route_table_id = aws_route_table.private_app_a.id
}

resource "aws_route_table_association" "private_app_b" {
subnet_id = aws_subnet.private_app_b.id
route_table_id = aws_route_table.private_app_b.id
}

# ---------------------------------------------------------------------------
# Security Groups
# ---------------------------------------------------------------------------

resource "aws_security_group" "storagelink" {
name = "${var.stack_name}-sg"
description = "StorageLink instance security group"
vpc_id = aws_vpc.main.id

ingress {
description = "SSH from admin"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.ssh_admin_cidr]
}

ingress {
description = "HTTP from admin"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.web_admin_cidr]
}

ingress {
description = "HTTPS from admin"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.web_admin_cidr]
}

ingress {
description = "SSH from VPC"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}

ingress {
description = "HTTP from VPC"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}

ingress {
description = "HTTPS from VPC"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [var.vpc_cidr]
}

egress {
from_port = 0
to_port = 65535
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}

egress {
from_port = 0
to_port = 65535
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}

tags = { Name = "${var.stack_name}-sg" }
}

resource "aws_security_group" "rds" {
name = "${var.stack_name}-rds-sg"
description = "RDS PostgreSQL security group"
vpc_id = aws_vpc.main.id

ingress {
description = "PostgreSQL from StorageLink instances"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = [aws_security_group.storagelink.id]
}

tags = { Name = "${var.stack_name}-rds-sg" }
}

# ---------------------------------------------------------------------------
# Secrets Manager (RDS password)
# ---------------------------------------------------------------------------

resource "random_password" "db" {
length = 30
special = false
}

resource "aws_secretsmanager_secret" "db" {
name = "${var.stack_name}-db-secret"
description = "Auto-generated password for StorageLink RDS database"
tags = { Name = "StorageLink DB Secret" }
}

resource "aws_secretsmanager_secret_version" "db" {
secret_id = aws_secretsmanager_secret.db.id
secret_string = jsonencode({
username = "swiftgw"
password = random_password.db.result
})
}

# ---------------------------------------------------------------------------
# RDS PostgreSQL
# ---------------------------------------------------------------------------

resource "aws_db_subnet_group" "main" {
name = "${var.stack_name}-db-subnet-group"
subnet_ids = [aws_subnet.private_db_a.id, aws_subnet.private_db_b.id]
tags = { Name = "StorageLink DB Subnet Group" }
}

resource "aws_db_instance" "main" {
identifier = "storagelink-${var.stack_name}"
db_name = "swiftgw"
instance_class = var.db_instance_class
db_subnet_group_name = aws_db_subnet_group.main.name
allocated_storage = 50
engine = "postgres"
engine_version = "16"

iam_database_authentication_enabled = true
username = jsondecode(aws_secretsmanager_secret_version.db.secret_string)["username"]
password = jsondecode(aws_secretsmanager_secret_version.db.secret_string)["password"]

auto_minor_version_upgrade = true
storage_encrypted = true
publicly_accessible = false
storage_type = "gp2"
vpc_security_group_ids = [aws_security_group.rds.id]
skip_final_snapshot = true

tags = { Name = "StorageLink Database" }
}

# ---------------------------------------------------------------------------
# CloudWatch Log Group
# ---------------------------------------------------------------------------

resource "aws_cloudwatch_log_group" "main" {
name = "storagelink-${var.stack_name}"
}

# ---------------------------------------------------------------------------
# IAM Role and Instance Profile
# ---------------------------------------------------------------------------

resource "aws_iam_role" "storagelink" {
name = "${var.stack_name}-role"
path = "/"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "ec2.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}

# Managed-policy attachments use separate resources rather than the
# (deprecated) managed_policy_arns argument, which behaves as an exclusive
# list and will silently detach inline custom policies on subsequent applies.
resource "aws_iam_role_policy_attachment" "ssm" {
role = aws_iam_role.storagelink.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonEC2RoleforSSM"
}

resource "aws_iam_role_policy_attachment" "s3_full" {
count = var.bucket_access == "open" ? 1 : 0
role = aws_iam_role.storagelink.name
policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
}

resource "aws_iam_policy" "storagelink" {
name = "${var.stack_name}-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:CreateBucket",
"s3:PutBucketPublicAccessBlock"
]
Resource = "arn:aws:s3:::storagelink-i-*"
},
{
Effect = "Allow"
Action = [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
"logs:CreateLogGroup",
"logs:GetLogEvents"
]
Resource = "*"
},
{
Effect = "Allow"
Action = ["ec2:DescribeAvailabilityZones", "ec2:DescribeInstances", "ec2:DescribeTags"]
Resource = "*"
},
{
Effect = "Allow"
Action = ["cloudformation:DescribeStacks", "cloudformation:ListStackResources"]
Resource = "*"
},
{
Effect = "Allow"
Action = [
"secretsmanager:GetResourcePolicy",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
]
Resource = aws_secretsmanager_secret.db.arn
},
{
Effect = "Allow"
Action = ["secretsmanager:ListSecrets"]
Resource = "*"
},
{
Effect = "Allow"
Action = ["rds-db:connect"]
Resource = "arn:aws:rds-db:*:${data.aws_caller_identity.current.account_id}:dbuser:${aws_db_instance.main.resource_id}/swiftgw"
}
]
})
}

resource "aws_iam_role_policy_attachment" "storagelink" {
role = aws_iam_role.storagelink.name
policy_arn = aws_iam_policy.storagelink.arn
}

resource "aws_iam_instance_profile" "storagelink" {
name = "${var.stack_name}-instance-profile"
role = aws_iam_role.storagelink.name
}

# ---------------------------------------------------------------------------
# Elastic IP + Network Load Balancer
# ---------------------------------------------------------------------------

resource "aws_eip" "nlb" {
domain = "vpc"
tags = { Name = "${var.stack_name}-nlb-eip" }
}

resource "aws_lb" "main" {
name = "${var.stack_name}-nlb"
load_balancer_type = "network"
internal = false

subnet_mapping {
subnet_id = aws_subnet.public_a.id
allocation_id = aws_eip.nlb.id
}

subnet_mapping {
subnet_id = aws_subnet.public_b.id
}

enable_cross_zone_load_balancing = true
tags = { Name = "${var.stack_name}-nlb" }
}

resource "aws_lb_target_group" "port22" {
name = "${var.stack_name}-tg-22"
port = 22
protocol = "TCP"
vpc_id = aws_vpc.main.id
preserve_client_ip = true
deregistration_delay = 60

health_check {
enabled = true
port = 22
protocol = "TCP"
interval = 30
healthy_threshold = 2
unhealthy_threshold = 2
}
}

resource "aws_lb_target_group" "port80" {
name = "${var.stack_name}-tg-80"
port = 80
protocol = "TCP"
vpc_id = aws_vpc.main.id
deregistration_delay = 60

health_check {
enabled = true
port = 80
protocol = "TCP"
interval = 30
healthy_threshold = 2
unhealthy_threshold = 2
}
}

# Application-aware HTTPS health check: validates that swiftgateway is
# reachable through nginx, not just that nginx is listening. Without this,
# a cold-starting or shutting-down instance will return 502s for ~30-60s
# while it is still marked healthy by a TCP-only probe.
resource "aws_lb_target_group" "port443" {
name = "${var.stack_name}-tg-443"
port = 443
protocol = "TCP"
vpc_id = aws_vpc.main.id
deregistration_delay = 60

health_check {
enabled = true
port = 443
protocol = "HTTPS"
path = "/backend/v3/api-docs"
matcher = "200"
interval = 10
healthy_threshold = 2
unhealthy_threshold = 2
}
}

resource "aws_lb_listener" "port22" {
load_balancer_arn = aws_lb.main.arn
port = 22
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.port22.arn
}
}

resource "aws_lb_listener" "port80" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.port80.arn
}
}

resource "aws_lb_listener" "port443" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "TCP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.port443.arn
}
}

# ---------------------------------------------------------------------------
# Launch Template + Auto Scaling Group
# ---------------------------------------------------------------------------

resource "aws_launch_template" "storagelink" {
name = "${var.stack_name}-lt"
image_id = nonsensitive(data.aws_ssm_parameter.storagelink_ami.value)
instance_type = var.instance_type
key_name = var.key_pair

network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.storagelink.id]
}

iam_instance_profile {
name = aws_iam_instance_profile.storagelink.name
}

block_device_mappings {
device_name = "/dev/xvda"
ebs {
volume_size = var.disk_volume_size
volume_type = "gp2"
encrypted = true
}
}

user_data = base64encode(<<-EOT
#cloud-config
repo_update: true
repo_upgrade: all
packages:
- jq
write_files:
- content: |
#!/bin/bash
export CLOUD_PROVIDER=aws
export ARCHITECTURE=HA
export LOG_GROUP_NAME=${aws_cloudwatch_log_group.main.name}
export SECRET_ID=${aws_secretsmanager_secret.db.name}
export DB_HOST=${aws_db_instance.main.address}
path: /opt/swiftgw/launch_config.env
- content: |
${var.web_admin_password}
path: /tmp/admin_pwd.txt
permissions: '0600'
- content: |
${var.web_admin_username}
path: /tmp/admin_user.txt
permissions: '0600'
runcmd:
- |
# Wait for swiftgateway to be responsive on 8080 (up to 2 min).
for i in $(seq 1 24); do
if curl -sf http://localhost:8080/v3/api-docs > /dev/null 2>&1; then break; fi
sleep 5
done
# The AMI's first-boot script can leave nginx in inactive state
# (cleanly exited after cert/config regeneration). Start it
# explicitly so the NLB target ever reaches healthy.
systemctl start nginx
# Bootstrap initial admin. 201 = created, 404 = admin already
# exists in the persistent DB (intentional API behaviour; expected
# on every replacement instance after the first deploy).
if [ -s /tmp/admin_pwd.txt ] && [ -s /tmp/admin_user.txt ]; then
PASSWORD=$(cat /tmp/admin_pwd.txt | tr -d '\n')
USERNAME=$(cat /tmp/admin_user.txt | tr -d '\n')
PAYLOAD=$(jq -n \
--arg pwd "$PASSWORD" \
--arg user "$USERNAME" \
'{"password": $pwd, "username": $user}')
HTTP_CODE=$(curl -s -o /dev/null -w "%%{http_code}" -X POST "http://localhost:8080/1.0.0/admin/config" \
-H "accept: */*" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
echo "Admin config response: $HTTP_CODE" | tee -a /var/log/storagelink-init.log
rm -f /tmp/admin_pwd.txt /tmp/admin_user.txt
fi
EOT
)
}

resource "aws_autoscaling_group" "storagelink" {
name = "${var.stack_name}-asg"
desired_capacity = var.desired_capacity
min_size = 2
max_size = 10

launch_template {
id = aws_launch_template.storagelink.id
version = "$Latest"
}

vpc_zone_identifier = [
aws_subnet.private_app_a.id,
aws_subnet.private_app_b.id
]

target_group_arns = [
aws_lb_target_group.port22.arn,
aws_lb_target_group.port80.arn,
aws_lb_target_group.port443.arn
]

tag {
key = "Name"
value = "StorageLink"
propagate_at_launch = true
}
}

# Lifecycle hook: hold a terminating instance in Terminating:Wait long enough
# for the NLB to fully drain in-flight traffic (deregistration_delay = 60s on
# the target groups) before services shut down. ASG auto-deregisters from
# target groups on transition into Terminating:Wait, so no hook script is
# required; default_result = CONTINUE lets the hook time out naturally.
resource "aws_autoscaling_lifecycle_hook" "drain" {
name = "${var.stack_name}-drain"
autoscaling_group_name = aws_autoscaling_group.storagelink.name
lifecycle_transition = "autoscaling:EC2_INSTANCE_TERMINATING"
default_result = "CONTINUE"
heartbeat_timeout = 120
}

# ---------------------------------------------------------------------------
# Outputs
# ---------------------------------------------------------------------------

output "load_balancer_dns" {
value = aws_lb.main.dns_name
description = "Network Load Balancer DNS name — use this as your StorageLink hostname"
}

output "load_balancer_ip" {
value = aws_eip.nlb.public_ip
description = "Static public IP address of the Network Load Balancer"
}

output "cloudwatch_log_group" {
value = aws_cloudwatch_log_group.main.name
description = "CloudWatch Log Group name for StorageLink logs"
}

terraform.tfvars

# AWS region to deploy into
region = "us-east-1"

# Prefix for all resource names (lowercase letters, numbers, and hyphens only)
stack_name = "my-storagelink"

# EC2 instance type. t3.medium is suitable for testing; use m5.large for production.
instance_type = "t3.medium"

# Root EBS volume size in GB (minimum 32)
disk_volume_size = 32

# Name of an existing EC2 Key Pair in the target region
key_pair = "my-keypair"

# Number of StorageLink instances to run (minimum 2 for HA)
desired_capacity = 2

# CIDR block for the new VPC
vpc_cidr = "192.168.1.0/24"

# Your workstation's public IP address with /32 suffix
# Used to restrict access to the web admin interface (ports 80/443)
web_admin_cidr = "1.2.3.4/32"

# Your workstation's public IP address with /32 suffix
# Used to restrict SSH access (port 22). Do not use 0.0.0.0/0.
ssh_admin_cidr = "1.2.3.4/32"

# Initial web admin credentials
web_admin_username = "admin"
web_admin_password = "YourSecurePassword123!"

# S3 access mode:
# "restricted" — limits access to buckets named storagelink-<instance-id> (recommended)
# "open" — grants full S3 access to all buckets in the account
bucket_access = "restricted"

# RDS instance class. db.t3.micro is suitable for testing; use db.m5.large for production.
db_instance_class = "db.t3.micro"