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