The *arr Stack, part 2: First Linode VM with Terraform
The Goal
Stand up one Linode VM using only Terraform, with:
- SSH key access (no password nonsense)
- output the public IP so I can actually log in
- nothing clever yet (cloud-init comes next)
Also: Linodes cost money while they exist. Don’t forget to destroy stuff when you’re done playing.
Prereqs (boring but required)
- Terraform installed (anything modern, I’m not here to fight versions)
- A Linode (Akamai) account
- An SSH keypair on your machine
Provider-wise, I’m using the Linode Terraform Provider v3+. v3 is the active line and is what you want for “future feature support” type reasons. (akamai.com)
Step 1: Make a Linode API Token
Go to Cloud Manager and create a Personal Access Token (PAT) with only the permissions you need (for now: Compute Instances is enough, but I usually just do Compute + Firewalls so I don’t forget later). (techdocs.akamai.com)
Do not commit this token anywhere. I’m not your mum, but I will judge you.
How I wire the token into Terraform
The provider supports reading LINODE_TOKEN from your shell environment. That’s the path of least regret. (registry.terraform.io)
export LINODE_TOKEN="paste-it-here"
(Yes, your shell history exists. Be sensible.)
Step 2: Scaffold the repo (minimal edition)
I’m keeping this part tiny on purpose.
repo/
infra/
envs/
dev/
main.tf
variables.tf
outputs.tf
versions.tf
terraform.tfvars.example
.gitignore
.gitignore
Terraform leaves little footprints everywhere. Ignore them.
# Terraform
.terraform/
*.tfstate
*.tfstate.*
*.tfvars
crash.log
crash.*.log
.terraform.lock.hcl
I ignore .terraform.lock.hcl in my personal stuff sometimes. In “enterprise mode” you probably commit it. Do what fits.
Step 3: Pin provider versions
versions.tf:
terraform {
required_version = ">= 1.0.0"
required_providers {
linode = {
source = "linode/linode"
version = "~> 3.0"
}
}
}
v3 is the line we want. (akamai.com)
Step 4: Variables
variables.tf:
variable "region" {
type = string
description = "Where the VM lives (e.g. eu-west)"
}
variable "type" {
type = string
description = "Linode plan type (e.g. g6-nanode-1, g6-standard-2)"
}
variable "image" {
type = string
description = "Linode image slug (usually starts with linode/)"
}
variable "label" {
type = string
description = "Instance label"
}
variable "authorized_key" {
type = string
description = "Your SSH public key (single-line, starts with ssh-ed25519 or ssh-rsa)"
}
Notes:
- Linode types are things like
g6-nanode-1,g6-standard-2, etc. (registry.terraform.io) - Public images start with
linode/and you can list them via API if you want to be fancy. (techdocs.akamai.com)
Step 5: Create the instance
main.tf:
provider "linode" {
# token is read from LINODE_TOKEN env var
}
resource "linode_instance" "arr" {
label = var.label
region = var.region
type = var.type
image = var.image
authorized_keys = [var.authorized_key]
tags = ["arr", "terraform", "dev"]
}
Root password?
Every Linode has a root password behind the scenes, but root_pass is optional in Terraform. If you omit it, Linode will generate one (and it won’t be stored in state). (linode.com)
I’m using SSH keys only, so I’m omitting it.
Step 6: Outputs (so we can actually use the thing)
outputs.tf:
output "ipv4" {
value = linode_instance.arr.ip_address
description = "Public IPv4 address"
}
output "ipv6" {
value = linode_instance.arr.ipv6
description = "IPv6 address (if available)"
}
Step 7: tfvars (example only)
terraform.tfvars.example:
region = "eu-west"
type = "g6-standard-1"
image = "linode/ubuntu24.04"
label = "arr-dev-1"
authorized_key = "ssh-ed25519 AAAA... you@machine"
Ubuntu 24.04 has a public image slug linode/ubuntu24.04 (handy default). (linode.com)
Copy it to terraform.tfvars locally (remember: .tfvars is ignored).
Step 8: Apply it
From infra/envs/dev:
terraform init
terraform fmt -recursive
terraform validate
terraform plan
terraform apply
When it’s up:
terraform output
ssh root@$(terraform output -raw ipv4)
If SSH doesn’t work: it’s usually one of
- wrong key pasted
- you’re on the wrong IP
- you created it… and immediately forgot you haven’t done firewalls yet (next part 👀)
Cleanup (seriously, do it)
If this is just a test VM:
terraform destroy
Where this goes next
Right now, this VM is basically an empty apartment: four walls, no furniture.
Next part is where we add the “first day essentials” using cloud-init:
- create a non-root user
- patch baseline packages
- install Docker
- create
/srv/appdata,/srv/downloads,/srv/media - leave the machine in a predictable state every time
Linode’s Metadata service + user-data is how we’ll feed cloud-init that bootstrap config. (techdocs.akamai.com)