TerraformPilot

DevOps

Deploy OpenClaw AI on AWS EC2 with Terraform and EBS Storage

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.

LLuca Berton1 min read

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.

Architecture

#
ComponentResource
Computeaws_instance (t3.medium, Ubuntu 24.04)
Persistent storageaws_ebs_volume (8 GB, gp3)
Attachmentaws_volume_attachment
Accessaws_key_pair (RSA 4096) + aws_security_group
BootstrapEC2 user data script

Provider

#
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"
}

SSH Key Pair (Generated Locally)

#
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"
}

Security Group (SSH Only)

#
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.

Ubuntu 24.04 LTS AMI

#
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-*"]
  }
}

EC2 Instance with User Data

#
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" }
}

EBS Volume for Persistent Agent Data

#
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.

Outputs

#
output "ssh_command" {
  value = "ssh -i openclaw-key.pem ubuntu@${aws_instance.openclaw.public_ip}"
}

First Login

#
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_data

Best Practices

#
  • Encrypt both volumes (encrypted = true) — it costs nothing and keeps data-at-rest controls auditable.
  • Snapshot the data volume with AWS Backup so an instance terminate doesn't delete agent state.
  • Use SSM Session Manager instead of SSH on port 22 once the instance is healthy — drop the ingress rule afterwards.
  • Pin the AMI ID in production. The most_recent = true data source is fine for dev, surprising for prod.
  • Move OPENCLAW_HOME into Terraform: pass it via templatefile() user data so the agent never starts pointing at the wrong path.
#
#Terraform#OpenClaw#AWS#EC2#EBS

Share this article