In this guide we will explore how to launch resources in the AWS Lightsail platform using Terraform.
Lightsail is an easy-to-use virtual private server (VPS) provider that offers you everything needed to build an application or website for a cost-effective, monthly plan. It is provided by AWS as a simpler way to manage cloud: a fixed monthly price per instance, with the IP, firewall and DNS handled inside Lightsail instead of VPCs and security groups.
We will create an SSH key pair, an Ubuntu 24.04 instance, a static IP attached to it, and firewall rules, then print the SSH command as an output. The same code works with OpenTofu, the open-source fork of Terraform: replace terraform with tofu in every command. The configuration below was validated with OpenTofu 1.11 and AWS provider 6.x.
Related content
- How to Use Terraform to Launch an AWS EC2 Instance
- Launch a DigitalOcean Droplet (VPS) Using Terraform
- SSH – Generating and Working with ssh keys
Prerequisites
- Terraform 1.6+ or OpenTofu 1.6+ installed (see the OpenTofu install docs or Terraform install docs).
- AWS credentials configured, for example with
aws configure. The provider reads the same~/.aws/credentialsandAWS_PROFILEas the AWS CLI. - An SSH key pair. If you don’t have one:
ssh-keygen -t ed25519 -C "lightsail" -f ~/.ssh/id_ed25519
Pick a blueprint and bundle
A blueprint is the OS image and a bundle is the instance size and price. IDs change as AWS adds releases and new hardware generations, so list the current ones for your region with the AWS CLI:
aws lightsail get-blueprints --region eu-west-1 \
--query 'blueprints[?type==`os` && isActive].[blueprintId,name,version]' --output table
aws lightsail get-bundles --region eu-west-1 \
--query 'bundles[?isActive].[bundleId,price,cpuCount,ramSizeInGb,diskSizeInGb]' --output table
aws lightsail get-regions --include-availability-zones \
--query 'regions[?name==`eu-west-1`].availabilityZones[].zoneName'
This guide uses ubuntu_24_04 and small_3_0. The original version of this post used centos_8, which reached end of life in 2021 and is no longer offered.
The configuration
Create a directory with the following files.
versions.tf pins the provider and sets the region:
terraform {
required_version = ">= 1.6"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}
provider "aws" {
region = var.region
}
variables.tf. Set admin_cidr to your own public IP so SSH is not open to the whole internet:
variable "region" {
type = string
default = "eu-west-1"
}
variable "availability_zone" {
type = string
default = "eu-west-1a"
}
variable "ssh_public_key_path" {
type = string
default = "~/.ssh/id_ed25519.pub"
}
variable "admin_cidr" {
description = "CIDR allowed to SSH in, e.g. your office IP with /32"
type = string
default = "203.0.113.10/32"
}
main.tf holds the key pair, instance, static IP and firewall:
resource "aws_lightsail_key_pair" "main" {
name = "citizix-key"
public_key = file(pathexpand(var.ssh_public_key_path))
}
resource "aws_lightsail_instance" "server" {
name = "server0"
availability_zone = var.availability_zone
blueprint_id = "ubuntu_24_04"
bundle_id = "small_3_0"
key_pair_name = aws_lightsail_key_pair.main.name
# IPv4 only: the port rules below restrict IPv4 CIDRs, and a dual-stack
# instance would also need ipv6_cidrs set on every port.
ip_address_type = "ipv4"
user_data = <<-EOT
#!/bin/bash
apt-get update
apt-get install -y nginx
systemctl enable --now nginx
EOT
tags = {
Env = "dev"
ManagedBy = "opentofu"
}
}
resource "aws_lightsail_static_ip" "server" {
name = "server0-ip"
}
resource "aws_lightsail_static_ip_attachment" "server" {
static_ip_name = aws_lightsail_static_ip.server.name
instance_name = aws_lightsail_instance.server.name
}
resource "aws_lightsail_instance_public_ports" "server" {
instance_name = aws_lightsail_instance.server.name
port_info {
protocol = "tcp"
from_port = 22
to_port = 22
cidrs = [var.admin_cidr]
}
port_info {
protocol = "tcp"
from_port = 80
to_port = 80
}
port_info {
protocol = "tcp"
from_port = 443
to_port = 443
}
}
A few notes:
aws_lightsail_key_pairwithpublic_keyimports your existing key. If you leavepublic_keyout, Lightsail generates a key pair and the private key ends up in the state file, so avoid that.user_datais the Lightsail launch script. It runs once on first boot; here it installs nginx. Changing it later replaces the instance.- Without a static IP, the public IP changes every time the instance is stopped and started. A static IP is free while it is attached to an instance.
aws_lightsail_instance_public_portsmanages the whole firewall for the instance: any port not listed is closed, including the default SSH and HTTP rules Lightsail adds. Each port appears once;cidrsrestricts the source and defaults to everywhere.
outputs.tf prints what you need to connect:
output "public_ip" {
value = aws_lightsail_static_ip.server.ip_address
}
output "username" {
value = aws_lightsail_instance.server.username
}
output "ssh_command" {
value = "ssh ${aws_lightsail_instance.server.username}@${aws_lightsail_static_ip.server.ip_address}"
}
Initialize and validate
tofu init
tofu fmt
tofu validate
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 6.0"...
- Installing hashicorp/aws v6.72.0...
- Installed hashicorp/aws v6.72.0 (signed, key ID 0C0AF313E5FD9F80)
...
OpenTofu has been successfully initialized!
Success! The configuration is valid.
init writes a .terraform.lock.hcl file that pins the exact provider version; commit it with your code.
Plan and apply
Save the plan to a file and apply exactly that file, so what gets created is what you reviewed:
tofu plan -out=lightsail.tfplan
tofu apply lightsail.tfplan
The plan should show five resources to add: the key pair, the instance, the static IP, the attachment and the public ports. The instance takes under a minute to come up. When apply finishes it prints the outputs:
tofu output ssh_command
Connect with the command it prints (ubuntu@<static-ip> for Ubuntu blueprints) and check nginx answers on http://<static-ip>/. Delete lightsail.tfplan afterwards; it is a snapshot that can contain sensitive values, so keep it out of Git.
Destroy
Lightsail bills by the hour until the instance is deleted, and a static IP that is no longer attached is billed too. Tear everything down with:
tofu destroy
Troubleshooting
InvalidInputException: ... blueprint ... not foundor bundle not found – the ID is not offered in that region or was retired. Re-run theget-blueprints/get-bundlescommands above.availability_zoneerrors – the zone must be in the provider’s region (eu-west-1aforeu-west-1).- SSH times out – your IP is not in
admin_cidr. Check your public IP and update the variable, then apply again. Permission denied (publickey)– you are using the wrong user or key. Ubuntu blueprints useubuntu, Debian usesadmin, Amazon Linux usesec2-user; pass-i ~/.ssh/id_ed25519if the key is not the default.No valid credential sources found– the provider cannot find AWS credentials. Runaws sts get-caller-identityto check, or exportAWS_PROFILE.