Introduction

The companion Setting up Keycloak for IAM tutorial sets Keycloak up by hand. This one does the same job with Terraform, so the whole stack, VM, network, public IP, firewall, and the Keycloak install itself, comes up from code you can version, review, and tear down with a single command. If you expect to rebuild this environment more than once, the Terraform route pays for itself quickly.

Prerequisites

You’ll want Terraform on your machine, an Atlas Cloud account with API access, and a CloudStack API key and secret. A working grasp of Terraform basics (providers, resources, variables) makes the rest read more easily.

Terraform Overview

By the time terraform apply finishes, you’ll have an Ubuntu 24.04 VM running Keycloak against a local PostgreSQL database, plus the network around it: a public IP, port forwarding for HTTP and HTTPS, and firewall rules. Keycloak is configured during first boot by cloud-init, so there’s nothing to SSH in and run afterward.

Step 1: Set Up Your Working Directory

Start with a clean directory to hold the configuration:

mkdir keycloak-deployment
cd keycloak-deployment

Everything you need is in the files below; there’s nothing to download separately.

Step 2: Get Your Atlas API Credentials

Before configuring Terraform, you need your Atlas Cloud Platform API credentials:

  1. Log in to sky.runatlas.is
  2. Click your profile in the top-right corner
  3. Find your API keys in the user card (they appear after about 10 seconds)
  4. If no keys appear, click “Generate new API/Secret keys”
  5. Copy the API Key and Secret Key from the UI

Step 3: Create Terraform Configuration Files

Create these four files in your project directory.

main.tf

terraform {
  required_providers {
    cloudstack = {
      source  = "cloudstack/cloudstack"
      version = "0.6.0"
    }
  }
}
 
provider "cloudstack" {
  api_url    = var.cloudstack_api_url
  api_key    = var.cloudstack_api_key
  secret_key = var.cloudstack_secret_key
}
 
# Create a network for Keycloak
resource "cloudstack_network" "keycloak_network" {
  name             = "keycloak-network"
  cidr             = "10.1.0.0/24"
  network_offering = var.network_offering
  zone             = var.zone
}
 
# Create a virtual machine for Keycloak
resource "cloudstack_instance" "keycloak" {
  name             = "keycloak-server"
  service_offering = var.instance_service_offering
  template         = var.instance_template
  zone             = var.zone
  network_id       = cloudstack_network.keycloak_network.id
 
  # User data script for initial setup
  user_data = templatefile("${path.module}/cloud-init.yaml", {
    admin_password = var.keycloak_admin_password
    db_password    = var.keycloak_db_password
  })
}
 
# Associate public IP address
resource "cloudstack_ipaddress" "keycloak_ip" {
  network = cloudstack_network.keycloak_network.name
}
 
# Create port forwarding rules for Keycloak
resource "cloudstack_port_forward" "keycloak_http" {
  ip_address_id = cloudstack_ipaddress.keycloak_ip.id
  forward {
    protocol     = "tcp"
    publicport   = 80
    privateport  = 8080
    virtualmachine = cloudstack_instance.keycloak.id
  }
}
 
resource "cloudstack_port_forward" "keycloak_https" {
  ip_address_id = cloudstack_ipaddress.keycloak_ip.id
  forward {
    protocol     = "tcp"
    publicport   = 443
    privateport  = 8443
    virtualmachine = cloudstack_instance.keycloak.id
  }
}
 
# Create firewall rules
resource "cloudstack_firewall" "keycloak_firewall" {
  ip_address_id = cloudstack_ipaddress.keycloak_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
  }
}
 
# Output the public IP and access information
output "keycloak_public_ip" {
  description = "Public IP address of the Keycloak server"
  value       = cloudstack_ipaddress.keycloak_ip.ip_address
}
 
output "keycloak_admin_url" {
  description = "URL for Keycloak admin console. Reach it over an SSH tunnel until TLS is in place; do not log in over plain HTTP."
  value       = "https://${cloudstack_ipaddress.keycloak_ip.ip_address}/admin"
}
 
output "keycloak_login_url" {
  description = "URL for Keycloak login page"
  value       = "https://${cloudstack_ipaddress.keycloak_ip.ip_address}/"
}

variables.tf

variable "cloudstack_api_url" {
  description = "CloudStack API URL"
  type        = string
  sensitive   = true
}
 
variable "cloudstack_api_key" {
  description = "CloudStack API key"
  type        = string
  sensitive   = true
}
 
variable "cloudstack_secret_key" {
  description = "CloudStack secret key"
  type        = string
  sensitive   = true
}
 
variable "zone" {
  description = "CloudStack zone name"
  type        = string
  default     = "is1"
}
 
variable "instance_service_offering" {
  description = "Instance service offering (CPU, memory configuration). List available offerings with `cmk list serviceofferings`."
  type        = string
  default     = "Atlas.a6"
}
 
variable "ssh_allowed_ips" {
  description = "CIDR ranges allowed to reach SSH (port 22). Restrict this to your own admin IPs, e.g. [\"203.0.113.10/32\"]. Do not open SSH to 0.0.0.0/0."
  type        = list(string)
}
 
variable "instance_template" {
  description = "Instance template (OS image)"
  type        = string
  default     = "Ubuntu 24.04 LTS"
}
 
# Atlas isolated guest networks support the Source NAT public IP, port
# forwarding, and egress firewall used below. The offering name is fixed for
# Atlas Cloud; egress is denied by default until you add egress firewall rules.
variable "network_offering" {
  description = "Isolated network offering for the Keycloak network (Source NAT + port forwarding + firewall, egress deny-by-default)."
  type        = string
  default     = "DefaultIsolatedNetworkOfferingWithSourceNatService"
}
 
variable "keycloak_admin_password" {
  description = "Password for Keycloak admin user"
  type        = string
  sensitive   = true
}
 
variable "keycloak_db_password" {
  description = "Password for Keycloak PostgreSQL database"
  type        = string
  sensitive   = true
}

cloud-init.yaml

#cloud-config
package_update: true
package_upgrade: true
 
packages:
  - openjdk-17-jdk
  - postgresql
  - postgresql-contrib
  - wget
  - unzip
 
# Write the database and admin secrets to a root-only file so they never
# appear on a command line or in the process table. cloud-init logs
# (/var/log/cloud-init-output.log) may still record templated values, so
# scrub those logs and rotate these secrets after first boot.
write_files:
  - path: /etc/keycloak/secrets.env
    permissions: '0600'
    owner: root:root
    content: |
      KC_DB_PASSWORD=${db_password}
      KEYCLOAK_ADMIN=admin
      KEYCLOAK_ADMIN_PASSWORD=${admin_password}
 
runcmd:
  # Create keycloak user
  - useradd -r -s /bin/false keycloak
 
  # Download and install Keycloak
  - cd /tmp
  - wget -q https://github.com/keycloak/keycloak/releases/download/23.0.0/keycloak-23.0.0.tar.gz
  - tar -xzf keycloak-23.0.0.tar.gz
  - mv keycloak-23.0.0 /opt/keycloak
  - chown -R keycloak:keycloak /opt/keycloak
 
  # Create the database role and database. The role password is read from the
  # root-only secrets file and fed to psql entirely on stdin (the \set and the
  # ALTER), so it never appears on a command line or in the process table.
  - sudo -u postgres psql -c "CREATE USER keycloak;"
  - sudo -u postgres psql -c "CREATE DATABASE keycloak OWNER keycloak;"
  - sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE keycloak TO keycloak;"
  - bash -c '. /etc/keycloak/secrets.env; { echo "\\set pw $KC_DB_PASSWORD"; echo "ALTER USER keycloak WITH PASSWORD :'"'"'pw'"'"';"; } | sudo -u postgres psql -v ON_ERROR_STOP=1'
 
  # Bind PostgreSQL to localhost only. Keep the database off the network and
  # never expose port 5432 publicly.
  - echo "listen_addresses = 'localhost'" >> /etc/postgresql/16/main/postgresql.conf
  - systemctl restart postgresql
 
  # Create Keycloak configuration. The DB password is supplied at runtime via
  # the KC_DB_PASSWORD environment variable from the secrets file (referenced
  # below), not written into keycloak.conf in cleartext.
  - cat > /opt/keycloak/conf/keycloak.conf << 'EOF'
db=postgres
db-url=jdbc:postgresql://localhost:5432/keycloak
db-username=keycloak
http-port=8080
https-port=8443
hostname=localhost
EOF
  - chown keycloak:keycloak /opt/keycloak/conf/keycloak.conf
 
  # Create systemd service for Keycloak. The first start reads
  # KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD from the secrets file to create
  # the bootstrap admin, which is how Quarkus-based Keycloak 23 creates the
  # initial admin account. KC_DB_PASSWORD supplies the DB password.
  - cat > /etc/systemd/system/keycloak.service << 'EOF'
[Unit]
Description=Keycloak Server
After=network.target postgresql.service
 
[Service]
Type=idle
User=keycloak
Group=keycloak
EnvironmentFile=/etc/keycloak/secrets.env
ExecStart=/opt/keycloak/bin/kc.sh start
TimeoutStartSec=600
TimeoutStopSec=600
Restart=always
RestartSec=10
 
[Install]
WantedBy=multi-user.target
EOF
  - chmod 0640 /etc/keycloak/secrets.env
  - chown root:keycloak /etc/keycloak/secrets.env
 
  # Enable and start services
  - systemctl daemon-reload
  - systemctl enable postgresql
  - systemctl enable keycloak
  - systemctl start keycloak
 
final_message: "Keycloak IAM server has been configured."

terraform.tfvars

# CloudStack Configuration
cloudstack_api_url    = "https://sky.runatlas.is/client/api"
cloudstack_api_key    = "your-copied-api-key"
cloudstack_secret_key = "your-copied-secret-key"
 
# Infrastructure Configuration
zone                      = "is1"
instance_service_offering = "Atlas.a6"
instance_template         = "Ubuntu 24.04 LTS"
 
# Isolated network offering (Source NAT + port forwarding + firewall,
# egress deny-by-default). This is the offering Atlas Cloud uses.
network_offering          = "DefaultIsolatedNetworkOfferingWithSourceNatService"
 
# Restrict SSH to your own admin IPs. Replace the placeholder with your /32.
ssh_allowed_ips = ["203.0.113.10/32"]
 
# Keycloak Configuration
# Generate your own (e.g. `openssl rand -base64 24`); do not reuse these placeholders.
keycloak_admin_password = "<CHANGE_ME>"
keycloak_db_password    = "<CHANGE_ME>"

Important: Replace the placeholder values with your actual Atlas Cloud Platform configuration. terraform.tfvars holds secrets, so add it to .gitignore and never commit it. As an alternative, supply secrets via TF_VAR_keycloak_admin_password / TF_VAR_keycloak_db_password environment variables instead of a file.

Step 4: Initialize Terraform

Initialize the Terraform working directory:

terraform init

This will download the CloudStack provider plugin and prepare the environment.

Step 5: Review the Deployment Plan

Generate and review the execution plan:

terraform plan

Terraform will show you what resources will be created:

  • cloudstack_network.keycloak_network - Network for the Keycloak server
  • cloudstack_instance.keycloak - The Keycloak virtual machine
  • cloudstack_ipaddress.keycloak_ip - Public IP address
  • cloudstack_port_forward.keycloak_http - HTTP port forwarding rule
  • cloudstack_port_forward.keycloak_https - HTTPS port forwarding rule
  • cloudstack_firewall.keycloak_firewall - Firewall rules for security

Step 6: Deploy the Infrastructure

Apply the configuration to create the resources:

terraform apply

Type yes when prompted to confirm the deployment. Terraform will:

  1. Create the network infrastructure
  2. Provision the virtual machine with Keycloak
  3. Assign a public IP address
  4. Configure port forwarding and firewall rules
  5. Execute the cloud-init script for automated setup

Step 7: Access Your Keycloak Server

Once Terraform completes the deployment:

Access the Admin Console

Once deployment completes, Terraform will output the access information. The IP shown below is an example; your value comes from terraform output keycloak_public_ip:

keycloak_public_ip = "203.0.113.123"
keycloak_admin_url = "https://203.0.113.123/admin"
keycloak_login_url = "https://203.0.113.123/"

The admin console exposes credentials and session cookies. Never log in to it over plain HTTP. Until you have placed a TLS certificate in front of Keycloak, reach the admin console through an SSH tunnel rather than over the public IP:

ssh -L 8443:localhost:8443 ubuntu@<your-public-ip>
# then browse to https://localhost:8443/admin
  1. Open the admin console over the SSH tunnel (or over HTTPS once TLS is configured)
  2. Log in with:
    • Username: admin
    • Password: The password you set in terraform.tfvars

Verify the Setup

  1. Check that you can access the Keycloak admin console
  2. Verify the server status in the admin panel
  3. Test creating a test realm and user

Understanding the Configuration

Main Components

Virtual Machine Configuration (main.tf):

resource "cloudstack_instance" "keycloak" {
  name             = "keycloak-server"
  service_offering = var.instance_service_offering
  template         = var.instance_template
  zone             = var.zone
  network_id       = cloudstack_network.keycloak_network.id
  user_data        = templatefile("${path.module}/cloud-init.yaml", {...})
}

Note: All network configuration including public IP, port forwarding, and firewall rules is handled automatically by Terraform.

Network Setup:

resource "cloudstack_network" "keycloak_network" {
  name             = "keycloak-network"
  cidr             = "10.1.0.0/24"
  network_offering = var.network_offering
  zone             = var.zone
}

Port Forwarding:

resource "cloudstack_port_forward" "keycloak_http" {
  ip_address_id = cloudstack_ipaddress.keycloak_ip.id
  forward {
    protocol     = "tcp"
    publicport   = 80
    privateport  = 8080
    virtualmachine = cloudstack_instance.keycloak.id
  }
}

Cloud-Init Automation

The cloud-init.yaml script handles:

  • Package installation (Java, PostgreSQL, Keycloak)
  • Database setup and configuration
  • Keycloak service configuration
  • Bootstrap admin user creation on first start (via KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD)

Customization Options

Scaling Resources

Modify the terraform.tfvars file to adjust resources:

# For higher performance (8 vCPU / 32 GiB)
instance_service_offering = "Atlas.a7"

List the offerings available in your account with cmk list serviceofferings.

Custom Domain

To use a custom domain, update the cloud-init script:

# In cloud-init.yaml
hostname=your-domain.com

SSL/TLS Configuration

For production deployments, terminate TLS before exposing the admin console:

  1. Obtain a TLS certificate for your domain
  2. Either configure Keycloak’s own HTTPS listener (https-certificate-file / https-certificate-key-file in keycloak.conf), or place a reverse proxy that terminates TLS in front of it
  3. Forward the public 443 port to the TLS listener and stop publishing the admin console over plain HTTP

Managing Your Deployment

Check Status

# Check Terraform state
terraform show
 
# List all resources
terraform state list

Update Configuration

  1. Modify your .tf files or terraform.tfvars
  2. Run terraform plan to see changes
  3. Apply updates with terraform apply

Backup and Restore

Backup Terraform State:

cp terraform.tfstate terraform.tfstate.backup

Backup Keycloak Data:

# SSH into the server
ssh ubuntu@<your-public-ip>
 
# Backup PostgreSQL
sudo -u postgres pg_dump keycloak > keycloak_backup.sql

Cleanup

To remove all resources and avoid charges:

terraform destroy

Type yes to confirm the destruction of all created resources.

Best Practices

  1. Version Control: Store your Terraform code in Git
  2. State Management: Use remote state storage for team collaboration
  3. Security: Use environment variables for sensitive data
  4. Monitoring: Set up monitoring for your Keycloak instance
  5. Backups: Regularly backup both Terraform state and Keycloak data

You have now deployed Keycloak using Terraform on Atlas Cloud.