Terraform Template
Purpose: Deploy a single StorageLink BYOL instance using Terraform (AWS, Azure, or Google Cloud)
Deployment:
- Create
slink-single-instance.tfandterraform.tfvars - Configure variables (region, admin IP, credentials)
- Run:
terraform init && terraform apply
Resources Created:
- AWS: EC2 instance, Security Group, Elastic IP, IAM role
- Azure: VM, NSG, Public IP, Storage Account, VNet
- Google: VM, Firewall, Static IP, VPC Network
Key Variables: admin_ip (your IP/32), SSH key or password, region
Prerequisites: AWS and Google Cloud: subscribe to the StorageLink BYOL listing. Azure: accept the StorageLink BYOL image terms once per subscription.
After Deployment: Create the admin account, then start the free 30-day trial in the admin UI — a BYOL instance starts unlicensed
Product: StorageLink by Thorn Technologies — cloud storage gateway for secure file sharing
Overview
You can now deploy StorageLink using Terraform.
This article covers deploying a single instance of StorageLink version 1.3.0. The Terraform template is provided as an example, so feel free to further customize it for your business case.
Each template deploys the StorageLink BYOL listing, which has no marketplace software fee. Before you deploy, subscribe to the listing (AWS, Google Cloud) or accept its image terms (Azure), or else you will run into an error while creating the virtual machine. Each cloud's tab below has the exact steps.
- AWS: StorageLink BYOL on AWS Marketplace
- Azure: StorageLink BYOL on Azure Marketplace
- Google Cloud: StorageLink BYOL on Google Cloud Marketplace
A BYOL instance starts unlicensed. After you create the admin account, start the free 30-day trial in the admin UI, or activate a purchased license in Settings → License (see First Launch).
Running the template
This article contains two files:
slink-single-instance.tfterraform.tfvars
Create these two files on your workstation, using the file contents at the bottom of this page. Make adjustments to the terraform.tfvars file. Then, run the following commands:
terraform init
terraform plan
When you are ready to deploy the template, run:
terraform apply
How does it work
This article contains a main Terraform template named:
slink-single-instance.tf
- AWS
- Azure
This template provisions the following resources:
EC2 instance: This server is based on the StorageLink BYOL marketplace AMI, launched into your default VPCEC2 Security Group: Allows TCP80and443from anywhere, but locks down admin port22to a single IPElastic IP: Static IP that retains the IP after a rebootIAM role: Grants the EC2 instance access to S3
Before you deploy, subscribe to StorageLink BYOL on AWS Marketplace: click View purchase options, select the Unit dimension (License units, $0.00), and click Subscribe. Subscribing is free — there is no software charge on the BYOL listing. Without the subscription, terraform apply fails with OptInRequired when it launches the instance.
There's also another file that contains variables:
terraform.tfvars
Since this file is named terraform.tfvars, it will be automatically used without having to run:
terraform -var-file terraform.tfvars
You can configure the following variables:
key_name: Specify the name of your EC2 key pairregion: Specify your current regionadmin_ip: Get your workstation's public IP fromcheckip.dyndns.org. Append/32to specify a single IP rangename_prefix: Optional. Defaults tostoragelink. Prefix for the IAM role, instance profile, IAM policy, and security group names. IAM names are global to your AWS account, so use a different prefix for each deploymentopen_s3_permissions: Optional. Defaults tofalse, which restricts permissions to buckets namedstoragelink-i-*(StorageLink createsstoragelink-i-<instance-id>as its default bucket). Set this totruefor full S3 permissions, for example to connect buckets with other namesimage_id: Optional. Defaults to the StorageLink BYOL 1.3.0.1 AMI alias/aws/service/marketplace/prod-7y2el53dcmt24/1.3.0.1. To deploy the pay-as-you-go Standard listing instead, subscribe to StorageLink on AWS Marketplace and set/aws/service/marketplace/prod-6fb6mek7ta6vi/1.3.0; a Standard instance is limited to 10 StorageLink usersaws_profile: Optional. Specify an AWS CLI profile if not using the default profile.ec2_instance_size: Optional. Defaults tot3.medium. Use this to override the EC2 instance size.disk_volume_size: Optional. Defaults to32. Use this to override the EC2 disk size, in GB.
When the deployment finishes, Terraform prints the instance's public_ip. Browse to https://<public_ip> and complete the First Launch Experience: the template does not create an admin account, so create it on the Welcome page (Create your account), then sign in with it. StorageLink creates its S3 bucket, storagelink-i-<instance-id>, when you create the admin account and uses it for the root folder. A BYOL instance starts unlicensed — start the free 30-day trial from the admin UI, then book a 15-minute call when you are ready to purchase a license.
terraform destroy removes the instance, Elastic IP, IAM role, and security group. It does not remove the storagelink-i-<instance-id> bucket, which holds your files; empty and delete it if you no longer need them.
Terraform file contents
slink-single-instance.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 0.14.9"
}
provider "aws" {
region = var.region
profile = var.aws_profile != "" ? var.aws_profile : null
}
data "aws_partition" "current" {}
variable "open_s3_permissions" {
type = bool
description = "Set this to true to allow full S3 access to support multiple buckets. Otherwise, S3 permissions are limited to our default bucket naming convention: storagelink-i-<instance-id>."
default = false
}
variable "region" {
type = string
description = "This is the AWS region"
}
variable "image_id" {
type = string
default = "/aws/service/marketplace/prod-7y2el53dcmt24/1.3.0.1"
description = "SSM alias of the StorageLink Marketplace AMI. Default: StorageLink BYOL 1.3.0.1 (/aws/service/marketplace/prod-7y2el53dcmt24/1.3.0.1). For the pay-as-you-go Standard listing use /aws/service/marketplace/prod-6fb6mek7ta6vi/1.3.0."
validation {
condition = contains(["/aws/service/marketplace/prod-7y2el53dcmt24/1.3.0.1", "/aws/service/marketplace/prod-6fb6mek7ta6vi/1.3.0"], var.image_id)
error_message = "Use /aws/service/marketplace/prod-7y2el53dcmt24/1.3.0.1 (StorageLink BYOL) or /aws/service/marketplace/prod-6fb6mek7ta6vi/1.3.0 (Standard, pay-as-you-go)."
}
}
data "aws_ssm_parameter" "marketplace_ami" {
name = var.image_id
}
variable "name_prefix" {
type = string
default = "storagelink"
description = "Prefix for the IAM role, instance profile, policy and security group names. Use a different prefix for each deployment in the same account."
}
variable "aws_profile" {
type = string
default = "default"
description = "Optional: specify an AWS profile"
}
variable "key_name" {
type = string
description = "Make sure you have access to this EC2 key pair. Otherwise, create a new key pair before proceeding."
}
variable "ec2_instance_size" {
type = string
description = "EC2 instance size. Recommended: t3.medium for testing, m5.large for production."
default = "t3.medium"
}
variable "disk_volume_size" {
type = number
description = "Disk volume size in GB. Must be at least 8."
default = 32
}
variable "admin_ip" {
type = string
description = "Public IP address range for SSH and web access. Use a CIDR range to restrict access. To get your local machine's IP, see http://checkip.dyndns.org/. (Remember to append /32 for a single IP e.g. 12.34.56.78/32) For security reasons, do not use 0.0.0.0/0."
validation {
condition = can(regex("([1-9]\\d{0,2})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/([1-9]\\d{0,1})",var.admin_ip))
error_message = "Must be a valid IP CIDR range in the form of x.x.x.x/x. Do not use 0.0.0.0/0."
}
}
resource "aws_eip" "eip" {
instance = aws_instance.storagelink.id
domain = "vpc"
}
resource "aws_instance" "storagelink" {
ami = nonsensitive(data.aws_ssm_parameter.marketplace_ami.value)
instance_type = var.ec2_instance_size
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.sg.id]
iam_instance_profile = aws_iam_instance_profile.ec2_profile.name
root_block_device {
volume_size = var.disk_volume_size
volume_type = "gp2"
encrypted = true
}
tags = {
Name = "storagelink"
}
}
resource "aws_iam_role" "iam_role" {
name = "${var.name_prefix}-role"
assume_role_policy = jsonencode({
Version: "2012-10-17",
Statement: [
{
Action: "sts:AssumeRole",
Principal: {
Service: "ec2.amazonaws.com"
},
Effect: "Allow"
}
]
})
}
resource "aws_iam_role_policy_attachment" "s3_full" {
count = var.open_s3_permissions ? 1 : 0
role = aws_iam_role.iam_role.name
policy_arn = "arn:${data.aws_partition.current.partition}:iam::aws:policy/AmazonS3FullAccess"
}
resource "aws_iam_instance_profile" "ec2_profile" {
name = "${var.name_prefix}-instance-profile"
role = aws_iam_role.iam_role.name
}
resource "aws_iam_role_policy" "ec2_policy" {
name = "${var.name_prefix}-policy"
role = aws_iam_role.iam_role.id
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [{
Action: "s3:*",
Effect: "Allow",
Resource: "arn:${data.aws_partition.current.partition}:s3:::storagelink-i-*"
}, {
Action: [
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
"logs:CreateLogGroup",
"ec2:DescribeAvailabilityZones",
"ec2:DescribeInstances",
"ec2:DescribeTags",
"s3:ListAllMyBuckets",
"cloudformation:DescribeStacks",
"cloudformation:ListStackResources"
],
Effect: "Allow",
Resource: "*"
}]
})
}
resource "aws_security_group" "sg" {
name = "${var.name_prefix}-sg"
description = "EC2 Security Group"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_ip]
}
egress {
from_port = 0
protocol = "tcp"
to_port = 65535
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
protocol = "udp"
to_port = 65535
cidr_blocks = ["0.0.0.0/0"]
}
}
output "public_ip" {
value = aws_eip.eip.public_ip
description = "Static public IP of the StorageLink instance — browse to https://<public_ip>"
}
terraform.tfvars
key_name = "my-keypair"
region = "us-east-1"
admin_ip = "1.2.3.4/32"
name_prefix = "storagelink"
This template provisions the following resources:
Virtual Machine: This server is based on the StorageLink BYOL marketplace imageNetwork Security Group: Allows TCP80and443from anywhere, but locks down admin port22to a single IPPublic IP: Static IP that retains the IP after a rebootStorage Account: This contains the Boot Diagnostics of the VM. You can also use it for StorageLink's default storage (see below).
Before the first deployment in a subscription, accept the terms of the StorageLink BYOL image. Otherwise Terraform fails while creating the VM with ResourcePurchaseValidationFailed: ... You have not accepted the legal terms on this subscription:
az vm image terms accept --publisher thorntechnologiesllc --offer storagelink-byol --plan storagelink-byol
You only need to do this once per subscription. Deploying StorageLink BYOL from the Azure Portal accepts the terms too.
After terraform apply completes, open https://<public_ip_address> and create your admin account (First Launch). Then:
- License the instance. StorageLink BYOL starts unlicensed. Start the 30-day free trial from the Start a 30-day free trial card, or activate a purchased license in Settings → License.
- Configure default storage. This template doesn't give the VM a managed identity, so StorageLink has no default storage and shows a Configure Default Storage prompt. Create a container in the template's Storage Account (Azure Portal → the Storage Account → Containers → + Container), then enter the Storage Account name, the container name and the Storage Account's connection string. Test Connection should show three green checkmarks.
There's also another file that contains variables:
terraform.tfvars
Since this file is named terraform.tfvars, it will be automatically used without having to run:
terraform -var-file terraform.tfvars
You can configure the following variables:
linux_username: Optional. The username of your Linux admin user. Defaults toubuntu.admin_public_key: Set this to your SSH public key. Alternatively, you can useadmin_password.admin_password: Set a password for your admin user. If you do not set this, you must useadmin_public_key.region: Specify your current region.admin_ip: Get your workstation's public IP fromcheckip.dyndns.org. Append/32to specify a single IP range.resource_group_name: Specify the name of the resource group that will be created to house your resources.vm_size: Optional. Specify the size of your VM. Defaults toStandard_B1ms.disk_volume_size: Optional. Specify the size of your disk volume. Defaults to30GB.
Terraform file contents
slink-single-instance.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
}
}
}
provider "azurerm" {
features {}
}
variable "linux_username" {
type = string
description = "Username of the Linux admin user"
default = "ubuntu"
}
variable "admin_password" {
type = string
description = "Password of the Linux admin user"
default = null
}
variable "admin_public_key" {
type = string
description = "Public SSH key for the Linux admin user"
default = null
}
variable "disk_volume_size" {
type = number
description = "Disk volume size in GB. Must be at least 30."
default = 30
}
variable "resource_group_name" {
type = string
description = "Name of the Resource Group"
}
variable "region" {
type = string
description = "Region in which you deploy resources"
}
variable "admin_ip" {
type = string
description = "Public IP address range for SSH and web access. Use a CIDR range to restrict access. To get your local machine's IP, see http://checkip.dyndns.org/. (Remember to append /32 for a single IP e.g. 12.34.56.78/32) For security reasons, do not use 0.0.0.0/0."
validation {
condition = can(regex("([1-9]\\d{0,2})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})/([1-9]\\d{0,1})",var.admin_ip))
error_message = "Must be a valid IP CIDR range in the form of x.x.x.x/x. Do not use 0.0.0.0/0."
}
}
variable "vm_size" {
type = string
description = "VM size. Recommended: Standard_B1ms for testing, Standard_D2_v3 for production."
default = "Standard_B1ms"
}
resource "azurerm_resource_group" "slink-resource-group" {
name = var.resource_group_name
location = var.region
}
resource "azurerm_virtual_network" "slink-vnet" {
name = "slink-vnet"
address_space = ["10.0.0.0/16"]
location = var.region
resource_group_name = azurerm_resource_group.slink-resource-group.name
}
resource "azurerm_subnet" "slink-subnet" {
name = "slink-subnet"
resource_group_name = azurerm_resource_group.slink-resource-group.name
virtual_network_name = azurerm_virtual_network.slink-vnet.name
address_prefixes = ["10.0.2.0/24"]
}
resource "azurerm_public_ip" "slink-ip" {
name = "slink-ip"
location = var.region
resource_group_name = azurerm_resource_group.slink-resource-group.name
allocation_method = "Static"
}
resource "azurerm_network_security_group" "slink-nsg" {
name = "slink-nsg"
location = var.region
resource_group_name = azurerm_resource_group.slink-resource-group.name
}
resource "azurerm_network_security_rule" "slink-ingress-rule-80" {
name = "slink-ingress-rule-80"
priority = 1001
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.slink-resource-group.name
network_security_group_name = azurerm_network_security_group.slink-nsg.name
}
resource "azurerm_network_security_rule" "slink-ingress-rule-443" {
name = "slink-ingress-rule-443"
priority = 1002
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.slink-resource-group.name
network_security_group_name = azurerm_network_security_group.slink-nsg.name
}
resource "azurerm_network_security_rule" "slink-ingress-rule-22" {
name = "slink-ingress-rule-22"
priority = 1003
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefix = var.admin_ip
destination_address_prefix = "*"
resource_group_name = azurerm_resource_group.slink-resource-group.name
network_security_group_name = azurerm_network_security_group.slink-nsg.name
}
resource "azurerm_network_interface" "slink-nic" {
name = "slink-nic"
location = var.region
resource_group_name = azurerm_resource_group.slink-resource-group.name
ip_configuration {
name = "NIC-config"
subnet_id = azurerm_subnet.slink-subnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = azurerm_public_ip.slink-ip.id
}
}
resource "azurerm_network_interface_security_group_association" "slink-nic-association" {
network_interface_id = azurerm_network_interface.slink-nic.id
network_security_group_id = azurerm_network_security_group.slink-nsg.id
}
resource "random_id" "random_id" {
keepers = {
resource_group = azurerm_resource_group.slink-resource-group.name
}
byte_length = 6
}
resource "azurerm_storage_account" "slink-storage-account" {
name = "slink${random_id.random_id.hex}"
resource_group_name = azurerm_resource_group.slink-resource-group.name
location = var.region
account_replication_type = "LRS"
account_tier = "Standard"
}
resource "azurerm_linux_virtual_machine" "slink-vm" {
name = "slink-vm"
location = var.region
resource_group_name = azurerm_resource_group.slink-resource-group.name
network_interface_ids = [azurerm_network_interface.slink-nic.id]
size = var.vm_size
disable_password_authentication = var.admin_password == null ? true : false
os_disk {
name = "slink-osdisk"
disk_size_gb = var.disk_volume_size
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}
source_image_reference {
publisher = "thorntechnologiesllc"
offer = "storagelink-byol"
sku = "storagelink-byol"
version = "latest"
}
plan {
name = "storagelink-byol"
publisher = "thorntechnologiesllc"
product = "storagelink-byol"
}
computer_name = "slink-vm"
admin_username = var.linux_username
admin_password = var.admin_password
admin_ssh_key {
username = var.linux_username
public_key = var.admin_public_key
}
boot_diagnostics {
storage_account_uri = azurerm_storage_account.slink-storage-account.primary_blob_endpoint
}
}
output "public_ip_address" {
value = "${azurerm_public_ip.slink-ip.*.ip_address}"
}
terraform.tfvars
admin_public_key = "ssh-rsa AAAAB3NzanNKEbh"
resource_group_name = "terraform-resource-group"
region = "eastus"
admin_ip = "203.0.113.100/32"
This template provisions the following resources:
Virtual Machine: This server is based on the StorageLink BYOL marketplace imageVPC network: A new auto-mode network for the VMFirewall: Allows TCP80and443from anywhere, but locks down admin port22to a single IPPublic IP: Static IP that retains the IP after a rebootService account: Attached to the VM with the Storage Admin role, so StorageLink can reach Cloud Storage with Attached Service Account credentials instead of a JSON key
Before you deploy, subscribe to StorageLink BYOL on the Google Cloud Marketplace (click Get Started and accept the terms in your project). There is no software charge on the BYOL listing.
There's also another file that contains variables:
terraform.tfvars
Since this file is named terraform.tfvars, it will be automatically used without having to run:
terraform -var-file terraform.tfvars
You can configure the following variables:
project: Specify the project in which to deploy the VMregion: Specify your current regionzone: Specify your current zonesource_ranges: Get your workstation's public IP fromcheckip.dyndns.org. Append/32to specify a single IP range (for example1.2.3.4/32)ssh-keys: Set an SSH key to be used to SSH into your VM. Note: the format isubuntu:ssh-rsa AAAAB3NzaC1, where you specify the username followed by the public key.credentials: Optional. Specify a json key credentials file to deploy this Terraform template.machine_type: Optional. Specify the size of your VM. Defaults toe2-medium.
Terraform file contents
slink-single-instance.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.0"
}
}
}
provider "google" {
credentials = var.credentials == null ? null : file(var.credentials)
project = var.project
region = var.region
zone = var.zone
}
variable "credentials" {
type = string
description = "Name of your Service Key"
default = null
}
variable "project" {
type = string
description = "Name of your Google Cloud project"
}
variable "region" {
type = string
description = "Name of your Google Cloud region"
default = "us-central1"
}
variable "zone" {
type = string
description = "Name of your region zone"
default = "us-central1-c"
}
variable "machine_type" {
type = string
description = "Machine type of your VM"
default = "e2-medium"
}
variable "ssh-keys" {
type = string
description = "Key used to SSH into your VM"
default = null
}
variable "source_ranges" {
type = string
description = "Source IP range"
}
resource "google_compute_network" "vpc_network" {
name = "terraform-network-${random_id.new.hex}"
}
resource "google_compute_address" "static" {
name = "ipv4-address-${random_id.new.hex}"
}
resource "random_id" "new" {
byte_length = "8"
}
# Service account for the VM, so StorageLink can reach Cloud Storage with its
# attached identity ("Attached Service Account" in the admin UI) instead of a
# JSON key.
resource "google_service_account" "storagelink" {
account_id = "storagelink-${substr(random_id.new.hex, 0, 8)}"
display_name = "StorageLink VM service account"
}
resource "google_project_iam_member" "storagelink_storage" {
project = var.project
role = "roles/storage.admin"
member = "serviceAccount:${google_service_account.storagelink.email}"
}
resource "google_compute_instance" "default" {
name = "terraform-instance-${random_id.new.hex}"
machine_type = var.machine_type
zone = var.zone
tags = ["terraform-instance"]
boot_disk {
initialize_params {
image = "thorn-technologies-public/storagelink-byol-1-3-0-1787784674"
}
}
network_interface {
network = google_compute_network.vpc_network.name
access_config {
nat_ip = google_compute_address.static.address
}
}
metadata = {
ssh-keys = var.ssh-keys
}
service_account {
email = google_service_account.storagelink.email
scopes = ["cloud-platform"]
}
}
resource "google_compute_firewall" "default" {
name = "terraform-firewall-${random_id.new.hex}"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["22"]
}
target_tags = ["terraform-instance"]
source_ranges = [var.source_ranges]
}
resource "google_compute_firewall" "web-ports" {
name = "terraform-firewall--${random_id.new.hex}"
network = google_compute_network.vpc_network.name
allow {
protocol = "tcp"
ports = ["80", "443"]
}
target_tags = ["terraform-instance"]
source_ranges = ["0.0.0.0/0"]
}
output "public_ip_address" {
value = "${google_compute_address.static.address}"
}
terraform.tfvars
project = "your-google-cloud-project"
region = "us-central1"
zone = "us-central1-c"
ssh-keys = "ubuntu:ssh-rsa AAAAB3NzaC1"
source_ranges = "1.2.3.4/32"
The template requires the hashicorp/google provider 6.x, which runs on every platform Terraform supports, including Apple silicon Macs. You also need permission to create service accounts and grant IAM roles in the project.
After the deployment
Terraform outputs public_ip_address. Open https://<public_ip_address> in your browser, accept the self-signed certificate warning, and create the admin account on the Welcome page (the password must be 12 or more characters). Sign in, and StorageLink opens the root folder settings to Configure Default Storage: enter the name of a Cloud Storage bucket in your project, keep Use Attached Service Account (the VM's service account created by the template), click Test Connection (Connection, Read Permission and Write Permission should all pass), then click Save.
A StorageLink BYOL instance starts unlicensed, so start the free 30-day trial from the Start a 30-day free trial card at the top of the admin UI, as described in First Launch Experience.
Subscribe to StorageLink on the Google Cloud Marketplace and change image to thorn-technologies-public/storagelink-1-3-0-1787784680. A Standard instance licenses itself automatically — its license check needs the attached service account this template creates — but is limited to 10 StorageLink users (administrators are not counted).
