Terraform for Windows Server on AWS EC2: Complete Guide
Provision Windows Server 2025 on AWS EC2 with Terraform. Includes AMI selection, password retrieval, RDP, IIS, and joining Active Directory.
DevOps
Deploy OpenClaw AI on AWS EC2 with Terraform: Ubuntu 24.04, gp3 EBS for persistent agent data, SSH key pair, security group, and user-data bootstrap.
OpenClaw AI is a personal AI agent that works best when it has persistent storage for sessions, models, and tool state. Running it on a fresh laptop loses that context every reboot. The fix: a small EC2 instance backed by an EBS volume, fully provisioned by Terraform.
This guide shows the complete pattern — VPC defaults, key pair, security group, Ubuntu 24.04 instance, gp3 EBS volume mounted at /opt/openclaw_data, and a user-data script that installs OpenClaw on first boot.
| Component | Resource |
|---|---|
| Compute | aws_instance (t3.medium, Ubuntu 24.04) |
| Persistent storage | aws_ebs_volume (8 GB, gp3) |
| Attachment | aws_volume_attachment |
| Access | aws_key_pair (RSA 4096) + aws_security_group |
| Bootstrap | EC2 user data script |
terraform {
required_version = ">= 1.6"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.70" }
tls = { source = "hashicorp/tls", version = "~> 4.0" }
}
}
provider "aws" {
region = "eu-central-1"
}resource "tls_private_key" "openclaw" {
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "openclaw" {
key_name = "openclaw-key"
public_key = tls_private_key.openclaw.public_key_openssh
}
resource "local_sensitive_file" "private_key" {
content = tls_private_key.openclaw.private_key_pem
filename = "${path.module}/openclaw-key.pem"
file_permission = "0400"
}resource "aws_security_group" "openclaw" {
name = "openclaw-sg"
description = "OpenClaw EC2 access"
ingress {
description = "SSH from admin IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.admin_cidr] # e.g. "203.0.113.10/32"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "openclaw-sg" }
}Lock the ingress to your public IP — never 0.0.0.0/0.
data "aws_ami" "ubuntu_2404" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd-gp3/ubuntu-noble-24.04-amd64-server-*"]
}
}resource "aws_instance" "openclaw" {
ami = data.aws_ami.ubuntu_2404.id
instance_type = "t3.medium"
key_name = aws_key_pair.openclaw.key_name
vpc_security_group_ids = [aws_security_group.openclaw.id]
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
}
user_data = <<-EOT
#!/bin/bash
set -eux
# Wait for the EBS volume to attach
while [ ! -e /dev/nvme1n1 ]; do sleep 1; done
# Format and mount /opt/openclaw_data
if ! blkid /dev/nvme1n1; then
mkfs -t xfs /dev/nvme1n1
fi
mkdir -p /opt/openclaw_data
mount /dev/nvme1n1 /opt/openclaw_data
echo '/dev/nvme1n1 /opt/openclaw_data xfs defaults,nofail 0 2' >> /etc/fstab
chown -R ubuntu:ubuntu /opt/openclaw_data
# Install OpenClaw
apt-get update -y
apt-get install -y curl git xfsprogs
curl -fsSL https://openclaw.ai/install.sh -o /tmp/install.sh
bash /tmp/install.sh
# Persist OPENCLAW_HOME for the ubuntu user
cat <<'PROFILE' >> /home/ubuntu/.bashrc
export OPENCLAW_HOME=/opt/openclaw_data
export PATH="$HOME/.npm-global/bin:$PATH"
PROFILE
chown ubuntu:ubuntu /home/ubuntu/.bashrc
EOT
tags = { Name = "openclaw-ai" }
}resource "aws_ebs_volume" "openclaw_data" {
availability_zone = aws_instance.openclaw.availability_zone
size = 8
type = "gp3"
encrypted = true
tags = { Name = "openclaw-data" }
}
resource "aws_volume_attachment" "openclaw_data" {
device_name = "/dev/sdf" # surfaces as /dev/nvme1n1 on Nitro
volume_id = aws_ebs_volume.openclaw_data.id
instance_id = aws_instance.openclaw.id
}The device_name you set (/dev/sdf) surfaces on Nitro instances as /dev/nvme1n1, which is what the user-data script formats and mounts.
output "ssh_command" {
value = "ssh -i openclaw-key.pem ubuntu@${aws_instance.openclaw.public_ip}"
}ssh -i openclaw-key.pem ubuntu@<EC2_PUBLIC_IP>
openclaw --version
# OpenClaw 2026.3.13 (61d171a)
df -h /opt/openclaw_data
# /dev/nvme1n1 8.0G ... /opt/openclaw_dataencrypted = true) — it costs nothing and keeps data-at-rest controls auditable.most_recent = true data source is fine for dev, surprising for prod.OPENCLAW_HOME into Terraform: pass it via templatefile() user data so the agent never starts pointing at the wrong path.Provision Windows Server 2025 on AWS EC2 with Terraform. Includes AMI selection, password retrieval, RDP, IIS, and joining Active Directory.
Provision multiplayer game server backends with Terraform: AWS GameLift fleets, FlexMatch matchmaking, queues, and player session APIs.
Provision macOS CI build infrastructure with Terraform: EC2 Mac instances (mac1, mac2-m2pro), dedicated hosts, and self-hosted GitHub Actions runners.
Amazon Linux 2 reaches end of life June 30, 2026. Migrate EC2 instances, Lambda runtimes, and ECS containers to Amazon Linux 2023 before the deadline using