Created
February 12, 2026 18:35
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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