Keycloak configuration as code with Terraform
Walkthrough for managing Keycloak using the Terraform provider.
It includes:
- Provider pinning and CI
- Drift detection
- A minimal, concrete example: one realm, one OpenID Connect client, and one OIDC Identity Provider
Use this as a starting point and expand as your realm grows.
Prerequisites
- A running Keycloak instance.
- A dedicated “automation” client in the admin realm (usually master) with the minimal realm‑management roles needed for your target realm(s).
- A Git repo for Terraform, and CI secrets for:
- KC_URL, KC_CLIENT_ID, KC_CLIENT_SECRET
Provider
providers.tf
terraform {
required_providers {
keycloak = {
source = "keycloak/keycloak"
version = ">= 5.0.0"
}
}
}
provider "keycloak" {
url = var.keycloak_url # e.g. https://sso.example.com
realm = "master" # admin realm for API auth
client_id = var.automation_client_id
client_secret = var.automation_client_secret
# tls_insecure_skip_verify = false # keep TLS strict in prod
}
variable "keycloak_url" {}
variable "automation_client_id" {}
variable "automation_client_secret" { sensitive = true }
Minimal example: one realm, one client, one IdP
Below is a working trio you can paste into main.tf. It creates:
- Realm app
- Public web client web with standard code flow
- OIDC Identity Provider corp-oidc (generic OIDC, bring your real URLs)
- A simple mapper importing the email claim from the IdP
main.tf
############################
# Realm
############################
resource "keycloak_realm" "app" {
realm = "app"
display_name = "App Realm"
enabled = true
}
############################
# Public OIDC client
############################
variable "web_redirect_uris" {
description = "Allowed redirect URIs for the web client"
type = list(string)
default = ["https://app.example.com/*"]
}
resource "keycloak_openid_client" "web" {
realm_id = keycloak_realm.app.id
client_id = "web"
name = "Web App"
access_type = "PUBLIC" # web app without client secret
standard_flow_enabled = true # authorization code flow
implicit_flow_enabled = false
direct_access_grants_enabled= false # disable password grant for web clients
valid_redirect_uris = var.web_redirect_uris
web_origins = ["+"] # matches redirect URIs (or list exact origins)
root_url = "https://app.example.com"
}
############################
# OIDC Identity Provider
############################
# Replace the URLs below with your IdP’s authorization, token, and userinfo endpoints.
# For example, your corporate IdP or a provider like Authentik, Okta, Azure AD (via OIDC),
# another Keycloak, etc.
resource "keycloak_oidc_identity_provider" "corp" {
realm = keycloak_realm.app.id
alias = "corp-oidc"
display_name = "Corporate OIDC"
authorization_url = "https://login.corp.example.com/oauth2/v1/authorize"
token_url = "https://login.corp.example.com/oauth2/v1/token"
user_info_url = "https://login.corp.example.com/oauth2/v1/userinfo"
jwks_url = "https://login.corp.example.com/oauth2/v1/keys"
client_id = var.corp_oidc_client_id
client_secret = var.corp_oidc_client_secret
default_scopes = "openid profile email"
backchannel_supported = true
enabled = true
store_token = true
trust_email = true
}
variable "corp_oidc_client_id" {}
variable "corp_oidc_client_secret" { sensitive = true }
############################
# Identity Provider Mapper: import email claim to user attribute
############################
resource "keycloak_attribute_importer_identity_provider_mapper" "corp_email" {
realm = keycloak_realm.app.id
name = "email-importer"
identity_provider_alias = keycloak_oidc_identity_provider.corp.alias
user_attribute = "email"
claim_name = "email"
# Keycloak 10+ expects syncMode in extra_config
extra_config = {
syncMode = "INHERIT"
}
}
GitHub Actions
.github/workflows/keycloak.yml
name: keycloak
on:
pull_request:
push:
branches: [ "main" ]
jobs:
terraform:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.6.6
- name: Terraform Init
run: terraform init -input=false
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan (PR)
if: github.event_name == 'pull_request'
env:
TF_VAR_keycloak_url: ${{ secrets.KC_URL }}
TF_VAR_automation_client_id: ${{ secrets.KC_CLIENT_ID }}
TF_VAR_automation_client_secret: ${{ secrets.KC_CLIENT_SECRET }}
TF_VAR_corp_oidc_client_id: ${{ secrets.CORP_OIDC_CLIENT_ID }}
TF_VAR_corp_oidc_client_secret: ${{ secrets.CORP_OIDC_CLIENT_SECRET }}
run: terraform plan -no-color
- name: Terraform Apply (main)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
env:
TF_VAR_keycloak_url: ${{ secrets.KC_URL }}
TF_VAR_automation_client_id: ${{ secrets.KC_CLIENT_ID }}
TF_VAR_automation_client_secret: ${{ secrets.KC_CLIENT_SECRET }}
TF_VAR_corp_oidc_client_id: ${{ secrets.CORP_OIDC_CLIENT_ID }}
TF_VAR_corp_oidc_client_secret: ${{ secrets.CORP_OIDC_CLIENT_SECRET }}
run: terraform apply -auto-approve
Add these GitHub Secrets:
- KC_URL, KC_CLIENT_ID, KC_CLIENT_SECRET
- CORP_OIDC_CLIENT_ID, CORP_OIDC_CLIENT_SECRET
Drift detection
- Drift = any difference between what’s in Terraform vs. what exists live (e.g., someone changed a redirect URI in the console).
- Detect it by running terraform plan on the current main branch regularly. If plan shows changes without a corresponding PR, you’ve got drift.
Example scheduled workflow:
name: keycloak-drift
on:
schedule: [{ cron: "17 4 * * *" }] # daily at 04:17 UTC
workflow_dispatch: {}
jobs:
drift-plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init -input=false
- name: Terraform Plan
env:
TF_VAR_keycloak_url: ${{ secrets.KC_URL }}
TF_VAR_automation_client_id: ${{ secrets.KC_CLIENT_ID }}
TF_VAR_automation_client_secret: ${{ secrets.KC_CLIENT_SECRET }}
TF_VAR_corp_oidc_client_id: ${{ secrets.CORP_OIDC_CLIENT_ID }}
TF_VAR_corp_oidc_client_secret: ${{ secrets.CORP_OIDC_CLIENT_SECRET }}
run: terraform plan -no-color
Enhancements
- Fail the job if the plan contains “- destroy”.
- Post the plan diff as a PR comment or a GitHub issue when non‑empty.
Guardrails and tips
- Least privilege: grant your automation client only the realm‑management roles needed.
- Naming stability: avoid renaming clients/roles/IdPs; prefer additive changes.
- Redirect URIs and web_origins: review changes carefully—these often cause login breakages.
- Secrets: never commit secrets; store in CI secrets or a vault action.
- Version pinning: update provider and Terraform versions intentionally; test upgrades in non‑prod first.
- Backups: back up the Keycloak DB and verify restores periodically.