## Overview
Deploy a website on Atlas Cloud using Terraform. Two paths:
| Path | Use When | SSL |
| --------- | ------------------------ | ----------------------------- |
| **HTTP** | No domain, quick testing | No |
| **HTTPS** | Have a domain name | Yes (Traefik + Let's Encrypt) |
> **Full example**: [terraform-examples/vm-website](https://github.com/RunAtlas-is/terraform-examples/tree/add-vm-website-example/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](https://sky.runatlas.is)
2. Click your profile (top-right)
3. Copy **API Key** and **Secret Key**
> **Need help?** See [API Credentials](API%20Credentials.md) for detailed instructions.
## Quick Start
### 1. Clone the Example
```bash
git clone https://github.com/RunAtlas-is/terraform-examples.git
cd terraform-examples/vm-website
```
### 2. Configure Variables
Create `terraform.tfvars`:
```hcl
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 = "
[email protected]"
```
### 3. Deploy
```bash
terraform init
terraform apply
```
### 4. Configure DNS (HTTPS only)
Point your domain A record to the output IP:
```bash
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.
<details>
<summary>๐ main.tf</summary>
```hcl
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"
publicport = each.value
privateport = each.value
virtualmachine = 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}/"
}
```
</details>
<details>
<summary>๐ cloud-init-http.yaml</summary>
```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
```
</details>
<details>
<summary>๐ variables.tf</summary>
```hcl
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 = "Atlas-alpha"
}
variable "instance_service_offering"{
type = string
default = "Small Instance"
}
variable "instance_template"{
type = string
default = "Ubuntu 24.04 LTS"
}
variable "network_offering"{
type = string
default = "DefaultSharedNetworkOffering"
}
variable "ssh_allowed_ips"{
type = list(string)
default = ["0.0.0.0/0"]
}
```
</details>
## HTTPS Path (With Domain)
For production with automatic SSL via Traefik and Let's Encrypt.
<details>
<summary>๐ main.tf (HTTPS)</summary>
```hcl
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"
publicport = each.value
privateport = each.value
virtualmachine = 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}/"
}
```
</details>
<details>
<summary>๐ cloud-init-https.yaml</summary>
```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
```
</details>
<details>
<summary>๐ variables.tf (HTTPS)</summary>
```hcl
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 = "Atlas-alpha"
}
variable "instance_service_offering" {
type = string
default = "Small Instance"
}
variable "instance_template" {
type = string
default = "Ubuntu 24.04 LTS"
}
variable "network_offering" {
type = string
default = "DefaultSharedNetworkOffering"
}
variable "ssh_allowed_ips" {
type = list(string)
default = ["0.0.0.0/0"]
}
```
</details>
## Verify
```bash
# 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
```bash
terraform destroy
```
## Troubleshooting
| Issue | Solution |
|-------|----------|
| SSH refused | Check `ssh_allowed_ips` includes your IP |
| SSL fails | Wait for DNS propagation (5-30 min) |
| Egress blocked | Verify egress firewall rules exist |
| Container won't start | SSH in and run `docker compose logs` |
## Next Steps
- [Setting up Remote Terraform State](Setting%20up%20Remote%20Terraform%20State.md) - For team collaboration
- [Website hosting on a VM](Website%20hosting%20on%20a%20VM.md) - Manual console setup