Keycloak with Pulumi: Infrastructure as Code for IAM

Guilliano Molaire Guilliano Molaire Updated June 6, 2026 9 min read

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_each or count.
  • Testing: Write unit tests for your infrastructure code using standard testing frameworks.

Prerequisites

Before starting, make sure you have:

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

  1. Separate infrastructure from configuration: Keep your Keycloak cluster provisioning (VMs, containers, databases) separate from realm configuration. This mirrors the advice in our Terraform post.

  2. 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.

  3. Encrypt all secrets: Use pulumi config set --secret for client secrets, and never commit unencrypted secrets to version control.

  4. Tag resources consistently: Add metadata to your Pulumi resources for tracking and audit log correlation.

  5. Pin provider versions: Lock your @pulumi/keycloak version in package.json to avoid unexpected changes.

  6. 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.

Guilliano Molaire
Written by Guilliano Molaire Founder

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

Ready to simplify your authentication?

Deploy production-ready Keycloak in minutes. Unlimited users, flat pricing, no SSO tax.

© 2026 Skycloak. All Rights Reserved. Design by Yasser Soliman