Create an AWS Lightsail Instance With Terraform or OpenTofu

Create an AWS Lightsail instance with Terraform or OpenTofu: SSH key pair, Ubuntu 24.04 VPS, static IP, firewall ports and outputs, then apply and destroy.

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.

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/credentials and AWS_PROFILE as 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_pair with public_key imports your existing key. If you leave public_key out, Lightsail generates a key pair and the private key ends up in the state file, so avoid that.
  • user_data is 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_ports manages 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; cidrs restricts 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 found or bundle not found – the ID is not offered in that region or was retired. Re-run the get-blueprints / get-bundles commands above.
  • availability_zone errors – the zone must be in the provider’s region (eu-west-1a for eu-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 use ubuntu, Debian uses admin, Amazon Linux uses ec2-user; pass -i ~/.ssh/id_ed25519 if the key is not the default.
  • No valid credential sources found – the provider cannot find AWS credentials. Run aws sts get-caller-identity to check, or export AWS_PROFILE.
comments powered by Disqus
Citizix Ltd
Built with Hugo
Theme Stack designed by Jimmy