Overview

Deploy a website on Atlas Cloud using Terraform. Two paths:

PathUse WhenSSL
HTTPNo domain, quick testingNo
HTTPSHave a domain nameYes (Traefik + Let’s Encrypt)

Full example: terraform-examples/vm-website

Prerequisites

  • Terraform >= 1.0
  • Atlas Cloud account
  • SSH key pair
  • (Optional) Domain name for HTTPS

Get API Credentials

  1. Log in to sky.runatlas.is
  2. Click your profile (top-right)
  3. Copy API Key and Secret Key

Need help? See API Credentials for detailed instructions.

Quick Start

1. Clone the Example

git clone https://github.com/RunAtlas-is/terraform-examples.git
cd terraform-examples/vm-website

2. Configure Variables

Create terraform.tfvars:

cloudstack_api_url    = "https://sky.runatlas.is/client/api"
cloudstack_api_key    = "your-api-key"
cloudstack_secret_key = "your-secret-key"
ssh_public_key        = "ssh-rsa AAAA..."
 
# For HTTPS (optional)
domain_name   = "example.com"
email_address = "you@example.com"

3. Deploy

terraform init
terraform apply

4. Configure DNS (HTTPS only)

Point your domain A record to the output IP:

terraform output webserver_public_ip

SSL certificates are auto-provisioned by Traefik.

HTTP Path (No Domain)

Use this for testing or when you don’t have a domain.

📁 main.tf
terraform {
  required_providers {
    cloudstack = {
      source  = "cloudstack/cloudstack"
      version = "0.6.0"
    }
  }
  required_version = ">=1.0.0"
}
 
provider "cloudstack" {
  api_url    = var.cloudstack_api_url
  api_key    = var.cloudstack_api_key
  secret_key = var.cloudstack_secret_key
}
 
resource "cloudstack_network" "webserver_network" {
  name             = "webserver-network"
  cidr             = "10.1.0.0/24"
  network_offering = var.network_offering
  zone             = var.zone
}
 
resource "cloudstack_instance" "webserver" {
  name             = "webserver-vm"
  service_offering = var.instance_service_offering
  template         = var.instance_template
  zone             = var.zone
  network          = cloudstack_network.webserver_network.name
  user_data = templatefile("${path.module}/cloud-init-http.yaml", {
    ssh_public_key = var.ssh_public_key
  })
}
 
resource "cloudstack_ipaddress" "webserver_ip" {
  network = cloudstack_network.webserver_network.name
}
 
resource "cloudstack_port_forward" "webserver_ports" {
  for_each = {
    http = 80
    ssh  = 22
  }
  ip_address_id = cloudstack_ipaddress.webserver_ip.id
  forward {
    protocol       = "tcp"
    public_port     = each.value
    private_port    = each.value
    virtual_machine_id = cloudstack_instance.webserver.id
  }
}
 
resource "cloudstack_firewall" "ingress" {
  ip_address_id = cloudstack_ipaddress.webserver_ip.id
  rule{
    protocol   = "tcp"
    start_port = 80
    end_port   = 80
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 22
    end_port   = 22
    cidr_list  = var.ssh_allowed_ips
  }
}
 
resource "cloudstack_egress_firewall" "egress" {
  network_id = cloudstack_network.webserver_network.id
  rule{
    protocol   = "tcp"
    start_port = 80
    end_port   = 80
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 443
    end_port   = 443
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "udp"
    start_port = 53
    end_port   = 53
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 53
    end_port   = 53
    cidr_list  = ["0.0.0.0/0"]
  }
}
 
output "webserver_public_ip" {
  value = cloudstack_ipaddress.webserver_ip.ip_address
}
 
output "website_url"{
  value = "http://${cloudstack_ipaddress.webserver_ip.ip_address}/"
}
📁 cloud-init-http.yaml
#cloud-config
package_update: true
packages:
  - nginx
 
ssh_authorized_keys:
  - ${ssh_public_key}
 
write_files:
  - path: /var/www/html/index.html
    content: |
      <!DOCTYPE html>
      <html>
      <head><title>Hello Atlas</title></head>
      <body><h1>Hello from Atlas Cloud!</h1></body>
      </html>
    permissions: '0644'
 
runcmd:
  - systemctl enable nginx
  - systemctl start nginx
📁 variables.tf
variable "cloudstack_api_url" {
  type      = string
  sensitive = true
}
 
variable "cloudstack_api_key" {
  type      = string
  sensitive = true
}
 
variable "cloudstack_secret_key" {
  type      = string
  sensitive = true
}
 
variable "ssh_public_key" {
  type = string
}
 
variable "zone"{
  type    = string
  default = "is1"
}
 
variable "instance_service_offering"{
  type    = string
  default = "Atlas.a4"
}
 
variable "instance_template"{
  type    = string
  default = "Ubuntu 24.04 LTS"
}
 
variable "network_offering"{
  type    = string
  default = "DefaultIsolatedNetworkOfferingWithSourceNatService"
}
 
variable "ssh_allowed_ips"{
  type    = list(string)
  default = ["0.0.0.0/0"]
}

HTTPS Path (With Domain)

For production with automatic SSL via Traefik and Let’s Encrypt.

📁 main.tf (HTTPS)
terraform {
  required_providers {
    cloudstack = {
      source  = "cloudstack/cloudstack"
      version = "0.6.0"
    }
  }
  required_version = ">=1.0.0"
}
 
provider "cloudstack"{
  api_url    = var.cloudstack_api_url
  api_key    = var.cloudstack_api_key
  secret_key = var.cloudstack_secret_key
}
 
resource "cloudstack_network" "webserver_network"{
  name             = "webserver-network"
  cidr             = "10.1.0.0/24"
  network_offering = var.network_offering
  zone             = var.zone
}
 
resource "cloudstack_instance" "webserver"{
  name             = "webserver-vm"
  service_offering = var.instance_service_offering
  template         = var.instance_template
  zone             = var.zone
  network          = cloudstack_network.webserver_network.name
  user_data = templatefile("${path.module}/cloud-init-https.yaml", {
    ssh_public_key = var.ssh_public_key
    domain_name    = var.domain_name
    email_address  = var.email_address
  })
}
 
resource "cloudstack_ipaddress" "webserver_ip"{
  network = cloudstack_network.webserver_network.name
}
 
resource "cloudstack_port_forward" "webserver_ports"{
  for_each = {
    http  = 80
    https = 443
    ssh   = 22
  }
  ip_address_id = cloudstack_ipaddress.webserver_ip.id
  forward{
    protocol       = "tcp"
    public_port     = each.value
    private_port    = each.value
    virtual_machine_id = cloudstack_instance.webserver.id
  }
}
 
resource "cloudstack_firewall" "ingress"{
  ip_address_id = cloudstack_ipaddress.webserver_ip.id
  rule{
    protocol   = "tcp"
    start_port = 80
    end_port   = 80
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 443
    end_port   = 443
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 22
    end_port   = 22
    cidr_list  = var.ssh_allowed_ips
  }
}
 
resource "cloudstack_egress_firewall" "egress"{
  network_id = cloudstack_network.webserver_network.id
  rule{
    protocol   = "tcp"
    start_port = 80
    end_port   = 80
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 443
    end_port   = 443
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "udp"
    start_port = 53
    end_port   = 53
    cidr_list  = ["0.0.0.0/0"]
  }
  rule{
    protocol   = "tcp"
    start_port = 53
    end_port   = 53
    cidr_list  = ["0.0.0.0/0"]
  }
}
 
output "webserver_public_ip"{
  value = cloudstack_ipaddress.webserver_ip.ip_address
}
 
output "website_url"{
  value = "https://${var.domain_name}/"
}
📁 cloud-init-https.yaml
#cloud-config
package_update: true
packages:
  - docker.io
  - docker-compose-v2
 
ssh_authorized_keys:
  - ${ssh_public_key}
 
write_files:
  - path: /var/www/html/index.html
    content: |
      <!DOCTYPE html>
      <html>
      <head><title>Hello Atlas</title></head>
      <body><h1>Hello from Atlas Cloud (HTTPS)!</h1></body>
      </html>
    permissions: '0644'
  - path: /opt/traefik/docker-compose.yml
    content: |
      services:
        traefik:
          image: traefik:v3
          command:
            - "--providers.docker=true"
            - "--entrypoints.web.address=:80"
            - "--entrypoints.websecure.address=:443"
            - "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
            - "--certificatesresolvers.letsencrypt.acme.email=${email_address}"
            - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
          ports:
            - "80:80"
            - "443:443"
          volumes:
            - "/var/run/docker.sock:/var/run/docker.sock:ro"
            - "letsencrypt:/letsencrypt"
        
        nginx:
          image: nginx:alpine
          labels:
            - "traefik.enable=true"
            - "traefik.http.routers.nginx.rule=Host(`${domain_name}`)"
            - "traefik.http.routers.nginx.entrypoints=websecure"
            - "traefik.http.routers.nginx.tls.certresolver=letsencrypt"
            - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
            - "traefik.http.routers.nginx-http.rule=Host(`${domain_name}`)"
            - "traefik.http.routers.nginx-http.entrypoints=web"
            - "traefik.http.routers.nginx-http.middlewares=redirect-to-https"
          volumes:
            - "/var/www/html:/usr/share/nginx/html:ro"
      
      volumes:
        letsencrypt:
    permissions: '0644'
 
runcmd:
  - systemctl enable docker
  - systemctl start docker
  - cd /opt/traefik && docker compose up -d
📁 variables.tf (HTTPS)
variable "cloudstack_api_url" {
  type      = string
  sensitive = true
}
 
variable "cloudstack_api_key" {
  type      = string
  sensitive = true
}
 
variable "cloudstack_secret_key" {
  type      = string
  sensitive = true
}
 
variable "ssh_public_key" {
  type = string
}
 
variable "domain_name" {
  type = string
}
 
variable "email_address" {
  type = string
}
 
variable "zone" {
  type    = string
  default = "is1"
}
 
variable "instance_service_offering" {
  type    = string
  default = "Atlas.a4"
}
 
variable "instance_template" {
  type    = string
  default = "Ubuntu 24.04 LTS"
}
 
variable "network_offering" {
  type    = string
  default = "DefaultIsolatedNetworkOfferingWithSourceNatService"
}
 
variable "ssh_allowed_ips" {
  type    = list(string)
  default = ["0.0.0.0/0"]
}

Verify

# Get the URL
terraform output website_url
 
# Test HTTP
curl -I http://$(terraform output -raw webserver_public_ip)
 
# Test HTTPS (after DNS propagation)
curl -I https://your-domain.com

Cleanup

terraform destroy

Troubleshooting

IssueSolution
SSH refusedCheck ssh_allowed_ips includes your IP
SSL failsWait for DNS propagation (5-30 min)
Egress blockedVerify egress firewall rules exist
Container won’t startSSH in and run docker compose logs

Next Steps