Keycloak with Pulumi: Infrastructure as Code for IAM
Last updated: March 2026
Managing identity infrastructure manually through admin consoles does not scale. As your organization grows, you need repeatable, version-controlled, and auditable processes for provisioning Keycloak realms, clients, roles, and authentication flows. Pulumi offers a compelling approach to this problem by letting you use familiar programming languages instead of domain-specific configuration languages.
In this guide, we will walk through using Pulumi to manage Keycloak infrastructure as code, covering TypeScript and Python examples, CI/CD integration, and how this approach compares to the more traditional Terraform workflow.
Why Pulumi for Keycloak?
If you have used Terraform to manage Keycloak, you know the benefits of infrastructure as code for IAM. Pulumi takes that concept further by letting you write infrastructure definitions in general-purpose programming languages like TypeScript, Python, Go, and C#.
This unlocks several advantages:
- Familiar tooling: Use your existing IDE, linters, test frameworks, and package managers.
- Type safety: TypeScript and Go provide compile-time checks that catch configuration errors before deployment.
- Abstraction: Build reusable functions and classes to encapsulate complex Keycloak patterns.
- Conditionals and loops: Use native language constructs instead of workarounds like HCL’s
for_eachorcount. - Testing: Write unit tests for your infrastructure code using standard testing frameworks.
Prerequisites
Before starting, make sure you have:
- A running Keycloak instance (or a managed Keycloak cluster on Skycloak)
- Pulumi CLI installed
- Node.js 18+ (for TypeScript) or Python 3.9+ (for Python)
- A Pulumi account or local backend configured
Setting Up a Pulumi Project
Start by creating a new Pulumi project. We will use TypeScript for the primary examples.
mkdir keycloak-iac && cd keycloak-iac
pulumi new typescript
Install the Keycloak provider:
npm install @pulumi/keycloak
Pulumi.yaml Configuration
Your Pulumi.yaml defines the project metadata:
name: keycloak-iac
runtime: nodejs
description: Keycloak infrastructure as code with Pulumi
Stack Configuration
Configure your Keycloak connection details per stack. Create a Pulumi.dev.yaml for your development environment:
config:
keycloak:url: "https://my-cluster.skycloak.io"
keycloak:clientId: "admin-cli"
keycloak:clientSecret:
secure: "v1:encrypted-secret-here"
Use pulumi config set --secret to encrypt sensitive values:
pulumi config set keycloak:clientSecret "your-secret" --secret
This ensures secrets are encrypted at rest in your stack configuration, a meaningful improvement over plain-text .tfvars files.
Provisioning a Realm with TypeScript
Here is a complete index.ts that provisions a realm, an OpenID Connect client, roles, and a user:
import * as pulumi from "@pulumi/pulumi";
import * as keycloak from "@pulumi/keycloak";
// Create a realm
const realm = new keycloak.Realm("app-realm", {
realm: "my-application",
enabled: true,
displayName: "My Application",
loginTheme: "keycloak",
sslRequired: "external",
registrationAllowed: false,
loginWithEmailAllowed: true,
duplicateEmailsAllowed: false,
editUsernameAllowed: false,
bruteForceDetected: true,
permanentLockout: false,
maxFailureWaitSeconds: 900,
failureFactor: 5,
passwordPolicy: "length(12) and upperCase(1) and lowerCase(1) and specialChars(1) and digits(1)",
internationalization: {
supportedLocales: ["en", "fr", "de"],
defaultLocale: "en",
},
securityDefenses: {
headers: {
contentSecurityPolicy: "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
xContentTypeOptions: "nosniff",
xFrameOptions: "SAMEORIGIN",
xRobotsTag: "none",
},
bruteForceDetection: {
permanentLockout: false,
maxLoginFailures: 5,
waitIncrementSeconds: 60,
maxFailureWaitSeconds: 900,
},
},
});
// Create an OIDC client for a frontend SPA
const frontendClient = new keycloak.openid.Client("frontend-client", {
realmId: realm.id,
clientId: "frontend-app",
name: "Frontend Application",
enabled: true,
accessType: "PUBLIC",
standardFlowEnabled: true,
directAccessGrantsEnabled: false,
validRedirectUris: [
"https://app.example.com/*",
"http://localhost:3000/*",
],
webOrigins: [
"https://app.example.com",
"http://localhost:3000",
],
loginTheme: "keycloak",
pkceCodeChallengeMethod: "S256",
});
// Create an OIDC client for the backend API
const backendClient = new keycloak.openid.Client("backend-client", {
realmId: realm.id,
clientId: "backend-api",
name: "Backend API",
enabled: true,
accessType: "CONFIDENTIAL",
standardFlowEnabled: false,
serviceAccountsEnabled: true,
directAccessGrantsEnabled: false,
});
// Create realm roles
const adminRole = new keycloak.Role("admin-role", {
realmId: realm.id,
name: "app-admin",
description: "Application administrator with full access",
});
const userRole = new keycloak.Role("user-role", {
realmId: realm.id,
name: "app-user",
description: "Standard application user",
});
const editorRole = new keycloak.Role("editor-role", {
realmId: realm.id,
name: "app-editor",
description: "Content editor with write access",
});
// Create a composite role
const managerRole = new keycloak.Role("manager-role", {
realmId: realm.id,
name: "app-manager",
description: "Manager role combining editor and user",
compositeRoles: [
userRole.id,
editorRole.id,
],
});
// Export useful values
export const realmName = realm.realm;
export const frontendClientId = frontendClient.clientId;
export const backendClientId = backendClient.clientId;
Run the deployment:
pulumi up
Pulumi shows you a preview of changes before applying them, similar to terraform plan. You can review exactly what will be created, updated, or deleted before confirming.
Building Reusable Components
One of Pulumi’s strongest advantages is the ability to create reusable components using standard language patterns. Here is a ComponentResource that encapsulates a common Keycloak application pattern:
import * as pulumi from "@pulumi/pulumi";
import * as keycloak from "@pulumi/keycloak";
interface KeycloakAppArgs {
realmId: pulumi.Input<string>;
appName: string;
redirectUris: string[];
webOrigins: string[];
roles: string[];
enableServiceAccount?: boolean;
}
class KeycloakApp extends pulumi.ComponentResource {
public readonly client: keycloak.openid.Client;
public readonly roles: keycloak.Role[];
public readonly clientScope: keycloak.openid.ClientScope;
constructor(name: string, args: KeycloakAppArgs, opts?: pulumi.ComponentResourceOptions) {
super("custom:keycloak:App", name, {}, opts);
// Create a client scope for this application
this.clientScope = new keycloak.openid.ClientScope(`${name}-scope`, {
realmId: args.realmId,
name: `${args.appName}-scope`,
description: `Scope for ${args.appName}`,
includeInTokenScope: true,
}, { parent: this });
// Create the OIDC client
this.client = new keycloak.openid.Client(`${name}-client`, {
realmId: args.realmId,
clientId: args.appName,
name: args.appName,
enabled: true,
accessType: args.enableServiceAccount ? "CONFIDENTIAL" : "PUBLIC",
standardFlowEnabled: true,
serviceAccountsEnabled: args.enableServiceAccount ?? false,
directAccessGrantsEnabled: false,
validRedirectUris: args.redirectUris,
webOrigins: args.webOrigins,
pkceCodeChallengeMethod: "S256",
}, { parent: this });
// Create roles for this application
this.roles = args.roles.map(roleName =>
new keycloak.Role(`${name}-role-${roleName}`, {
realmId: args.realmId,
name: `${args.appName}:${roleName}`,
description: `${roleName} role for ${args.appName}`,
}, { parent: this })
);
this.registerOutputs({
clientId: this.client.clientId,
roles: this.roles.map(r => r.name),
});
}
}
export { KeycloakApp, KeycloakAppArgs };
Now you can provision multiple applications with minimal code:
import { KeycloakApp } from "./keycloak-app";
const dashboardApp = new KeycloakApp("dashboard", {
realmId: realm.id,
appName: "dashboard",
redirectUris: ["https://dashboard.example.com/*"],
webOrigins: ["https://dashboard.example.com"],
roles: ["viewer", "editor", "admin"],
});
const portalApp = new KeycloakApp("portal", {
realmId: realm.id,
appName: "customer-portal",
redirectUris: ["https://portal.example.com/*"],
webOrigins: ["https://portal.example.com"],
roles: ["customer", "support"],
enableServiceAccount: true,
});
This pattern is far more ergonomic than Terraform modules for teams that already work in TypeScript. For organizations heavily invested in role-based access control, this composability is especially valuable.
Python Example
For teams that prefer Python, the same concepts apply with the pulumi_keycloak package:
pulumi new python
pip install pulumi-keycloak
import pulumi
import pulumi_keycloak as keycloak
# Create realm
realm = keycloak.Realm("app-realm",
realm="my-application",
enabled=True,
display_name="My Application",
login_with_email_allowed=True,
duplicate_emails_allowed=False,
ssl_required="external",
password_policy="length(12) and upperCase(1) and lowerCase(1) and digits(1)",
)
# Create OIDC client
client = keycloak.openid.Client("api-client",
realm_id=realm.id,
client_id="api-gateway",
name="API Gateway",
enabled=True,
access_type="CONFIDENTIAL",
standard_flow_enabled=False,
service_accounts_enabled=True,
direct_access_grants_enabled=False,
)
# Define roles from a list
role_definitions = [
("read", "Read-only access"),
("write", "Read and write access"),
("admin", "Full administrative access"),
]
roles = {}
for role_name, description in role_definitions:
roles[role_name] = keycloak.Role(f"role-{role_name}",
realm_id=realm.id,
name=role_name,
description=description,
)
# Configure identity provider for social login
google_idp = keycloak.oidc.GoogleIdentityProvider("google-idp",
realm=realm.realm,
client_id="google-client-id",
client_secret="google-client-secret",
trust_email=True,
first_broker_login_flow_alias="first broker login",
extra_config={
"syncMode": "INHERIT",
},
)
pulumi.export("realm_name", realm.realm)
pulumi.export("client_id", client.client_id)
Python works particularly well for data-driven configurations where you pull client definitions from a YAML file or database and iterate over them programmatically. For more on configuring social identity providers like Google, see our dedicated feature page.
State Management
Pulumi stores state that tracks the mapping between your code and the actual Keycloak resources. You have several backend options:
Pulumi Cloud (Default)
The managed Pulumi Cloud service handles state storage, encryption, access control, and history. This is the simplest option for teams:
pulumi login
Local Backend
For air-gapped environments or cost-sensitive setups:
pulumi login --local
State is stored in ~/.pulumi/ by default.
S3 or Azure Blob Backend
For teams that want self-managed remote state:
pulumi login s3://my-pulumi-state-bucket
Whichever backend you choose, Pulumi encrypts secrets in state by default. This is a notable improvement over Terraform, where state files contain secrets in plain text unless you use additional tooling.
CI/CD Integration
Pulumi integrates naturally into CI/CD pipelines. Here is a GitHub Actions workflow that deploys Keycloak configuration on merge to main:
name: Deploy Keycloak Config
on:
push:
branches: [main]
paths:
- 'keycloak-iac/**'
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: keycloak-iac
run: npm ci
- uses: pulumi/actions@v5
with:
command: up
stack-name: production
work-dir: keycloak-iac
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
KEYCLOAK_URL: ${{ secrets.KEYCLOAK_URL }}
KEYCLOAK_CLIENT_ID: ${{ secrets.KEYCLOAK_CLIENT_ID }}
KEYCLOAK_CLIENT_SECRET: ${{ secrets.KEYCLOAK_CLIENT_SECRET }}
For pull request previews, add a separate job that runs pulumi preview:
preview:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: keycloak-iac
run: npm ci
- uses: pulumi/actions@v5
with:
command: preview
stack-name: production
work-dir: keycloak-iac
comment-on-pr: true
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
This posts a preview of infrastructure changes as a comment on your pull request, giving reviewers visibility into exactly what will change.
Multi-Environment Management with Stacks
Pulumi stacks map naturally to environments. Create separate stacks for development, staging, and production:
pulumi stack init dev
pulumi stack init staging
pulumi stack init production
Use stack-specific configuration to vary settings per environment:
import * as pulumi from "@pulumi/pulumi";
import * as keycloak from "@pulumi/keycloak";
const config = new pulumi.Config();
const env = pulumi.getStack();
const realm = new keycloak.Realm("realm", {
realm: `app-${env}`,
enabled: true,
displayName: `Application (${env})`,
registrationAllowed: env === "dev",
bruteForceDetected: env === "production",
passwordPolicy: env === "production"
? "length(14) and upperCase(1) and lowerCase(1) and specialChars(1) and digits(1) and notUsername"
: "length(8)",
});
This approach keeps environment differences explicit in your code rather than scattered across variable files.
Comparing Pulumi and Terraform for Keycloak
Both tools work well for managing Keycloak. Here is a practical comparison:
| Aspect | Pulumi | Terraform |
|---|---|---|
| Language | TypeScript, Python, Go, C# | HCL |
| Type Safety | Full (with TypeScript/Go) | Limited |
| Reusable Abstractions | Classes, functions, packages | Modules |
| Secret Handling | Encrypted in state by default | Plain text in state by default |
| Testing | Standard test frameworks | Terratest, built-in test command |
| State Backend | Cloud, S3, Azure Blob, local | Cloud, S3, Azure Blob, local, Consul |
| Community Provider | Community-maintained | Community-maintained |
| Learning Curve | Low if you know the language | Low to moderate (new language) |
If your team already uses Terraform extensively, the advanced Terraform patterns post covers modules, workspaces, and CI/CD integration with HCL. If your team works primarily in TypeScript or Python, Pulumi will feel more natural.
Importing Existing Keycloak Resources
If you already have a Keycloak instance with manually configured resources, you can import them into Pulumi:
pulumi import keycloak:index/realm:Realm my-realm my-realm-id
Pulumi generates the corresponding code for the imported resource, which you can then add to your project. This is invaluable for bringing existing configurations under version control without recreating them.
For quickly scaffolding a Keycloak configuration to import, the Keycloak Config Generator can help you prototype realm and client settings before codifying them in Pulumi.
Best Practices
-
Separate infrastructure from configuration: Keep your Keycloak cluster provisioning (VMs, containers, databases) separate from realm configuration. This mirrors the advice in our Terraform post.
-
Use stack references: When your cluster infrastructure and Keycloak configuration live in different projects, use Pulumi stack references to share outputs like URLs and credentials.
-
Encrypt all secrets: Use
pulumi config set --secretfor client secrets, and never commit unencrypted secrets to version control. -
Tag resources consistently: Add metadata to your Pulumi resources for tracking and audit log correlation.
-
Pin provider versions: Lock your
@pulumi/keycloakversion inpackage.jsonto avoid unexpected changes. -
Test before deploying: Write unit tests that validate your resource configurations without deploying them.
Conclusion
Pulumi brings the full power of general-purpose programming languages to Keycloak infrastructure management. Whether you are provisioning realms, configuring single sign-on clients, setting up multi-factor authentication policies, or managing RBAC hierarchies, Pulumi lets you express these configurations in a type-safe, testable, and reusable way. Refer to the Keycloak Server Administration Guide for details on the resources you can manage programmatically.
For teams that want the benefits of infrastructure as code without the operational burden of managing Keycloak servers, Skycloak’s managed hosting handles the infrastructure layer so you can focus on configuring identity workflows. Check out our pricing page to see what plan fits your needs.
Ready to simplify your authentication?
Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.