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-deploymentEverything 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:
- Log in to sky.runatlas.is
- Click your profile in the top-right corner
- Find your API keys in the user card (they appear after about 10 seconds)
- If no keys appear, click “Generate new API/Secret keys”
- 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 initThis will download the CloudStack provider plugin and prepare the environment.
Step 5: Review the Deployment Plan
Generate and review the execution plan:
terraform planTerraform will show you what resources will be created:
cloudstack_network.keycloak_network- Network for the Keycloak servercloudstack_instance.keycloak- The Keycloak virtual machinecloudstack_ipaddress.keycloak_ip- Public IP addresscloudstack_port_forward.keycloak_http- HTTP port forwarding rulecloudstack_port_forward.keycloak_https- HTTPS port forwarding rulecloudstack_firewall.keycloak_firewall- Firewall rules for security
Step 6: Deploy the Infrastructure
Apply the configuration to create the resources:
terraform applyType yes when prompted to confirm the deployment. Terraform will:
- Create the network infrastructure
- Provision the virtual machine with Keycloak
- Assign a public IP address
- Configure port forwarding and firewall rules
- 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- Open the admin console over the SSH tunnel (or over HTTPS once TLS is configured)
- Log in with:
- Username:
admin - Password: The password you set in
terraform.tfvars
- Username:
Verify the Setup
- Check that you can access the Keycloak admin console
- Verify the server status in the admin panel
- 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.comSSL/TLS Configuration
For production deployments, terminate TLS before exposing the admin console:
- Obtain a TLS certificate for your domain
- Either configure Keycloak’s own HTTPS listener (
https-certificate-file/https-certificate-key-fileinkeycloak.conf), or place a reverse proxy that terminates TLS in front of it - 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 listUpdate Configuration
- Modify your
.tffiles orterraform.tfvars - Run
terraform planto see changes - Apply updates with
terraform apply
Backup and Restore
Backup Terraform State:
cp terraform.tfstate terraform.tfstate.backupBackup Keycloak Data:
# SSH into the server
ssh ubuntu@<your-public-ip>
# Backup PostgreSQL
sudo -u postgres pg_dump keycloak > keycloak_backup.sqlCleanup
To remove all resources and avoid charges:
terraform destroyType yes to confirm the destruction of all created resources.
Best Practices
- Version Control: Store your Terraform code in Git
- State Management: Use remote state storage for team collaboration
- Security: Use environment variables for sensitive data
- Monitoring: Set up monitoring for your Keycloak instance
- Backups: Regularly backup both Terraform state and Keycloak data
You have now deployed Keycloak using Terraform on Atlas Cloud.