Skip to content

Instantly share code, notes, and snippets.

@fizz
Created February 12, 2026 18:35
Show Gist options
  • Select an option

  • Save fizz/4d6a4e6a7b77c64ad43139a7b9841f99 to your computer and use it in GitHub Desktop.

Select an option

Save fizz/4d6a4e6a7b77c64ad43139a7b9841f99 to your computer and use it in GitHub Desktop.
Automated SimpleAD User Creation for Amazon WorkSpaces via Terraform — no ClickOps, no RSAT, no WorkDocs hack
# simplead-user-terraform.tf
#
# Automated SimpleAD User Creation for Amazon WorkSpaces via Terraform
# ====================================================================
#
# PROBLEM:
# AWS SimpleAD has NO API for creating directory users. The aws_directory_service
# resource can create the directory itself, and aws_workspaces_workspace can
# create a WorkSpace -- but there is no Terraform resource or AWS API call to
# create the AD user that sits between them.
#
# COMMONLY SUGGESTED WORKAROUNDS (all require manual intervention):
# 1. Enable WorkDocs on the directory via the AWS console, then use the
# WorkDocs CreateUser API. But enabling WorkDocs is a console-only action.
# 2. Launch a domain-joined EC2 Windows instance with RSAT (Remote Server
# Administration Tools), then create the user via AD Users and Computers.
# 3. RDP into a persistent management/jump box and use AD admin tools.
#
# THIS SOLUTION:
# SimpleAD is Samba 4 Active Directory. It speaks LDAP. We use a Terraform
# local-exec provisioner to run kubectl run, which launches a temporary
# Alpine Linux pod on an EKS cluster that has VPC network access to the
# SimpleAD DNS IPs. The pod:
# 1. Installs openldap-clients (~2 seconds on Alpine)
# 2. Runs ldapadd to create the user object in SimpleAD
# 3. Exits and is deleted (--rm flag)
#
# For the password: AD requires LDAPS (TLS) to set passwords via LDAP, and
# SimpleAD does not expose LDAPS by default. However, the AWS Directory Service
# API provides ds:ResetUserPassword which works over the standard AWS API
# (no VPC access needed). So we split it:
# - LDAP (from EKS pod, inside VPC) -> creates the user object
# - DS API (from Terraform host, anywhere) -> sets the password
#
# ALTERNATIVES (if you do not run EKS):
# - VPN/Direct Connect: docker run alpine locally with openldap-clients
# - Lambda in VPC: Python ldap3, invoke via aws_lambda_invocation
# - Fargate task: same approach as EKS, different scheduler
#
# PREREQUISITES:
# - An EKS cluster with VPC network access to SimpleAD DNS IPs
# - kubectl and aws CLI available on the Terraform execution host
# - AWS credentials with eks:DescribeCluster and ds:ResetUserPassword
# - Kubernetes RBAC allowing pod creation in the default namespace
#
# IDEMPOTENCY:
# ldapadd is wrapped with || echo "User may already exist" so re-running
# terraform apply after the user exists will not fail the provisioner.
# ds:ResetUserPassword is inherently idempotent.
#
# AD REPLICATION NOTE:
# sleep 10 between ldapadd and ResetUserPassword gives SimpleAD time to
# replicate the new user object across domain controllers. Without this,
# the password reset can fail with EntityDoesNotExistException.
# ---------------------------------------------------------------------------
# Variables
# ---------------------------------------------------------------------------
variable "region" {
description = "AWS region"
type = string
default = "us-east-1"
}
variable "eks_cluster_name" {
description = "Name of the EKS cluster with VPC access to SimpleAD"
type = string
}
variable "ad_admin_password" {
description = "SimpleAD Administrator password"
type = string
sensitive = true
}
variable "ad_domain" {
description = "SimpleAD domain name (e.g., workspaces.corp.local)"
type = string
default = "workspaces.corp.local"
}
variable "workspace_username" {
description = "sAMAccountName for the new AD user (e.g., jdoe)"
type = string
}
variable "workspace_user_password" {
description = "Initial password (must meet AD complexity requirements)"
type = string
sensitive = true
}
variable "user_display_name" {
description = "Full display name (e.g., Jane Doe)"
type = string
}
variable "user_given_name" {
description = "First name"
type = string
}
variable "user_surname" {
description = "Last name"
type = string
}
# ---------------------------------------------------------------------------
# Locals
# ---------------------------------------------------------------------------
locals {
# Convert "workspaces.corp.local" -> "dc=workspaces,dc=corp,dc=local"
base_dn = join(",", [for part in split(".", var.ad_domain) : "dc=${part}"])
}
# ---------------------------------------------------------------------------
# Create AD user via LDAP from ephemeral EKS pod, set password via DS API
# ---------------------------------------------------------------------------
resource "terraform_data" "ad_user" {
input = {
directory_id = aws_directory_service_directory.this.id
username = var.workspace_username
password = var.workspace_user_password
admin_password = var.ad_admin_password
dns_ip = tolist(aws_directory_service_directory.this.dns_ip_addresses)[0]
}
provisioner "local-exec" {
command = <<-EOT
set -e
aws eks update-kubeconfig \
--name "${var.eks_cluster_name}" \
--region "${var.region}" 2>/dev/null || true
# Step 1: Create the user object via LDAP from an ephemeral EKS pod
#
# The pod runs inside the VPC where it can reach SimpleAD's DNS IPs.
# openldap-clients provides ldapadd. The entire pod lifecycle is ~15s.
#
# userAccountControl: 544 = NORMAL_ACCOUNT (512) + PASSWD_NOTREQD (32)
# We set PASSWD_NOTREQD because we cannot set the password over
# unencrypted LDAP -- the DS API handles that in Step 2.
kubectl run simplead-create-user --rm -i --restart=Never \
--image=alpine:3.19 \
--overrides='{"spec":{"terminationGracePeriodSeconds":0}}' \
--env="AD_PASS=${self.input.admin_password}" \
--env="BASE_DN=${local.base_dn}" \
--env="AD_USER=${self.input.username}" \
--env="LDAP_HOST=${self.input.dns_ip}" \
-- sh -c '
apk add --no-cache -q openldap-clients 2>/dev/null
printf "dn: cn=${var.user_display_name},cn=Users,%s\n\
objectClass: top\n\
objectClass: person\n\
objectClass: organizationalPerson\n\
objectClass: user\n\
cn: ${var.user_display_name}\n\
sAMAccountName: %s\n\
userPrincipalName: %s@${var.ad_domain}\n\
givenName: ${var.user_given_name}\n\
sn: ${var.user_surname}\n\
displayName: ${var.user_display_name}\n\
userAccountControl: 544\n" \
"$BASE_DN" "$AD_USER" "$AD_USER" \
| ldapadd \
-H "ldap://$LDAP_HOST" \
-D "cn=Administrator,cn=Users,$BASE_DN" \
-w "$AD_PASS" \
|| echo "User may already exist, continuing"
'
# Step 2: Set password via DS API (no VPC access needed)
#
# sleep 10 gives SimpleAD time to replicate the new user object
# across domain controllers. Without this delay, the password
# reset can fail with EntityDoesNotExistException.
sleep 10
aws ds reset-user-password \
--directory-id "${self.input.directory_id}" \
--user-name "${self.input.username}" \
--new-password "${self.input.password}" \
--region "${var.region}"
EOT
}
depends_on = [aws_workspaces_directory.this]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment