Skip to content

VDI (Virtual Desktop Infrastructure) Module

License: MIT-0

ℹ️ Prerequisites: You need a Windows Server AMI. The examples use Packer-built AMIs from this repo's Packer templates (lightweight/ and ue-gamedev/), but any Windows Server 2019/2022/2025 AMI works. See Amazon DCV Documentation for complete setup guidance.

Features

  • Amazon DCV Integration - High-performance streaming protocol optimized for graphics workloads
  • Dual Connectivity - Public internet or private VPN access with custom DNS support
  • Game Development Ready - GPU instances, high-performance storage, UE-optimized AMIs
  • Intelligent Drive Management - Automatic Windows drive letter assignment and volume lifecycle
  • Complete VDI Infrastructure - EC2 workstations, security, IAM, and user management
  • Security by Default - Least privilege IAM, encrypted storage, restricted network access, termination protection
  • Flexible Authentication - EC2 key pairs (emergency) and Secrets Manager (production)
  • Runtime Software Installation - Automated package installation via SSM and Chocolatey

Connectivity Patterns

Public Connectivity

When: Workstations in public subnets with Internet Gateway routes Access: Direct internet with IP restrictions

workstations = {
  "vdi-001" = {
    preset_key = "my-preset"
    assigned_user = "naruto-uzumaki"
    subnet_id = aws_subnet.public_subnet.id
    allowed_cidr_blocks = ["198.51.100.1/32"]  # Replace with user's public IP
  }
}

Private Connectivity

When: Workstations in private subnets with NAT Gateway routes Access: Via VPN, Direct Connect, or Site-to-Site VPN DNS Requirement: AWS Client VPN connection required to resolve private DNS names (username.vdi.internal)

module "vdi" {
  create_client_vpn = true  # Creates Client VPN infrastructure

  users = {
    "naruto-uzumaki" = {
      given_name = "Naruto"
      family_name = "Uzumaki"
      email = "naruto@example.com"
      type = "administrator"
      use_client_vpn = true  # Gets VPN access + certificates
    }
  }

  workstations = {
    "vdi-001" = {
      preset_key = "my-preset"
      assigned_user = "naruto-uzumaki"
      subnet_id = aws_subnet.private_subnet.id
      allowed_cidr_blocks = ["10.0.0.0/16"]  # VPC CIDR only
    }
  }
}

Architecture

┌─────────────────┐    ┌──────────────────┐    ┌─────────────────┐
│   Remote User   │    │   VPN Client     │    │  VDI Workstation│
│  Web Browser    │───▶│  (Private Mode)  │───▶│   EC2 Instance  │
│                 │    │   .ovpn file     │    │   Windows + DCV │
└─────────────────┘    └──────────────────┘    └─────────────────┘
         │                       │                       │
         │ (Public Mode)         │                       │
         └───────────────────────┼───────────────────────┘
                                 │
                    ┌─────────────▼─────────────┐
                    │         AWS VPC           │
                    │  Security Groups, IAM,    │
                    │  SSM, S3, Secrets Mgr     │
                    └───────────────────────────┘

Account Creation Pattern

Account Type Created When Password Storage Scope Use Case
🆘 Administrator (built-in) Windows boot EC2 Key Pair All workstations Emergency break-glass only
🛡️ Fleet Admin (fleet_administrator) SSM (if defined) Secrets Manager All workstations Fleet management
🔧 Local Admin (administrator) SSM (if defined) Secrets Manager Assigned workstation Local administration
💻 Standard User (user) SSM (if defined) Secrets Manager Assigned workstation Daily usage

Key Point: Only the built-in Administrator exists automatically. All other accounts must be explicitly defined in the users variable.

Prerequisites

  1. AWS Account Setup

  2. AWS CLI configured with deployment permissions

  3. VPC with public and private subnets
  4. Basic understanding of AWS services (VPC, EC2)

  5. Network Planning

  6. Public connectivity: User public IP addresses for security group access
  7. Private connectivity: VPN setup and VPC CIDR planning

Cost Estimates

⚠️ Cost Warning: These examples deploy expensive GPU instances (~$1,430/month per workstation). Review costs before deployment.

Example Configuration Costs (per workstation/month):

  • g4dn.4xlarge instance: ~$1,200/month
  • EBS storage (300GB root + 2TB projects with 3000 IOPS): ~$230/month
  • Total per workstation: ~$1,430/month

3-workstation example total: ~$4,290/month

Cost optimization options:

  • Reduce volume sizes for development/testing
  • Use smaller instance types (g4dn.xlarge, g4dn.2xlarge)
  • Leverage Spot instances for non-production workloads
  • Stop instances manually via AWS Console/CLI when not in use (EBS storage costs continue)

For accurate pricing: Use the AWS Pricing Calculator with your specific requirements and region.

Examples

For a quickstart, please review the examples. They provide complete Terraform configuration with VPC setup, security groups, and detailed connection instructions.

Available Examples:

Quick Start

1. Clone Repository

git clone https://github.com/aws-games/cloud-game-development-toolkit.git
cd cloud-game-development-toolkit

2. Choose AMI

  • Option A: Build a toolkit AMI (e.g., UE GameDev for game development)
  • Option B: Use any existing Windows Server AMI

Example - To build the UE GameDev AMI:

cd assets/packer/virtual-workstations/ue-gamedev/
packer build windows-server-2025-ue-gamedev.pkr.hcl

3. Deploy Example

cd modules/vdi/examples/public-connectivity/  # or private-connectivity/
terraform init
terraform plan
terraform apply

4. Get Connection Info

terraform output connection_info

5. Connect (Private Only)

AWS Client VPN Setup (Private Connectivity)

Why AWS Client VPN is Required:

For private connectivity, AWS Client VPN is essential because:

  • Custom DNS Resolution: Private workstation URLs like https://username.vdi.internal:8443 only resolve when connected to the VPN
  • Network Access: Private subnets are not accessible from the internet - VPN provides secure tunnel access
  • Security: Eliminates need to expose workstations to public internet

Setup Process:

  1. Deploy with VPN enabled: Set create_client_vpn = true in your Terraform configuration
  2. Download VPN configuration: The module automatically generates .ovpn files in S3
  3. Install VPN client: Download AWS VPN Client (recommended)
  4. Import configuration: Use the .ovpn file generated for your user

Using the Generated .ovpn File:

# Download your VPN configuration
aws s3 cp s3://cgd-vdi-vpn-configs-XXXXXXXX/your-username/your-username.ovpn ~/Downloads/

# Import into AWS VPN Client or OpenVPN
# AWS VPN Client: File → Manage Profiles → Add Profile
# OpenVPN: Import the .ovpn file directly

For detailed setup instructions: See AWS Client VPN User Guide

Connection Flow:

  1. Connect to AWS Client VPN using your .ovpn file
  2. Access workstation via private DNS: https://username.vdi.internal:8443
  3. Login with credentials from Secrets Manager

Connection Guide

⚠️ CRITICAL: Wait for Windows Boot

After terraform apply completes, wait 5-10 minutes for Windows initialization before attempting login.

During boot, you'll see:

  • "Wrong username or password" errors (expected)
  • DCV connection failures (expected)
  • Certificate warnings (expected)

Check boot status:

aws ec2 get-console-output --instance-id $(terraform output -json connection_info | jq -r '."vdi-001".instance_id') --latest

Ready when you see:

  • EC2Launch: EC2 Launch has completed
  • User creation script completion
  • DCV service startup messages

Get Credentials

# Get connection info
terraform output connection_info

# Get user password from Secrets Manager
aws secretsmanager get-secret-value \
  --secret-id "cgd/vdi-001/users/your-username" \
  --query SecretString --output text | jq .

Connect via DCV

  1. Download DCV Client (recommended) or use web browser
  2. Connect to VPN (private connectivity only)
  3. Required for private DNS: To access workstations via https://username.vdi.internal:8443, you must be connected to AWS Client VPN
  4. Private DNS resolution: Custom DNS names only resolve when connected to the VPN
  5. Open DCV:
  6. Public: https://workstation-public-ip:8443
  7. Private (VPN required): https://username.vdi.internal:8443 or https://workstation-private-ip:8443
  8. Accept certificate warning (self-signed certificates)
  9. Login with credentials from Secrets Manager

Emergency Access

# Get Administrator password
terraform output -json private_keys | jq -r '."vdi-001"' > temp_key.pem
chmod 600 temp_key.pem
aws ec2 get-password-data \
  --instance-id $(terraform output -json connection_info | jq -r '."vdi-001".instance_id') \
  --priv-launch-key temp_key.pem --query 'PasswordData' --output text
rm temp_key.pem

Password Management

Password Details

  • Auto-generated: 16-character secure passwords (letters + numbers + special characters)
  • Initial storage: AWS Secrets Manager (source of truth for first login only)
  • User changes: Users can change passwords in Windows - Secrets Manager will not update without additional configuration/custom logic (out of scope for this module)
  • Lifecycle: Users can manage passwords in Windows or continue using Secrets Manager passwords

Automatic Script Re-execution

The module automatically re-runs configuration scripts when you modify infrastructure. Changes to volumes, users, or software packages trigger the appropriate scripts to run via AWS SSM - no manual intervention required.

Scripts only execute when infrastructure actually changes, providing clean Terraform plans and predictable behavior.

Manual Alternative: For immediate results or troubleshooting, you can RDP to the instance as Administrator and run operations manually.

Volume Configuration

EBS Volume Management

Volume changes do NOT trigger instance replacement. Instances continue running during volume operations.

Required Root Volume

volumes = {
  Root = {                    # ← MUST be exactly "Root" (case-sensitive)
    capacity = 256            # ← Root volume automatically gets C: drive
    type = "gp3"
  }
  # Add additional drives as needed (auto-assigned D:, E:, F:, etc.)
  # Projects = { capacity = 1000, type = "gp3" }
  # Cache = { capacity = 500, type = "gp3" }
}

Drive Letter Assignment

Automatic Assignment: The module uses Windows auto-assignment for all drive letters:

  • Root VolumeC: drive (Windows boot requirement)
  • EBS VolumesAuto-assigned (typically D:, E:, F:, etc.)
  • Instance StoreAuto-assigned (typically next available letter)

G4dn Instance Store Sizes:

  • g4dn.xlarge: 125GB NVMe SSD (auto-assigned)
  • g4dn.2xlarge: 225GB NVMe SSD (auto-assigned)
  • g4dn.4xlarge: 225GB NVMe SSD (auto-assigned)
  • g4dn.8xlarge: 900GB NVMe SSD (auto-assigned)

Benefits: Simple configuration with no drive letter conflicts, Windows native behavior, user customizable via Disk Management, and cost efficiency by utilizing included instance store.

Volume Change Lifecycle

Change Type Automatic Handling Data Safety User Action Required
Add Volume Fully automatic ✅ Safe Wait 5-10 minutes after apply
Increase Size Fully automatic ✅ Safe Wait for AWS optimization + SSM (5-15 min)
Reduce Size BLOCKED BY AWS ⚠️ Not Supported See Volume Size Reduction
Remove Volume Immediate and reliable Volume data lost None (drive letters cleaned up)
Change Volume Type ✅ Auto-applied ✅ Safe Wait for optimization (5-15 min typical, up to 6 hrs)
Rename Volume ✅ Terraform only ✅ Safe None

Volume Limitations

Volume Size Reduction: Not supported by AWS - EBS volumes cannot be reduced in size.

Volume Modification Rate Limit: AWS enforces a 6-hour wait between volume modifications. This is a hard platform limitation that cannot be overridden.

Advanced Configuration

On-Demand Capacity Reservations (ODCR)

Use existing capacity reservations if available. See AWS ODCR Documentation for details.

# Module-level default (applies to all workstations)
module "vdi" {
  capacity_reservation_preference = "open"  # Use ODCR if available

  workstations = {
    "ws1" = { subnet_id = "subnet-123" }  # Inherits "open"
    "ws2" = { subnet_id = "subnet-456" }  # Inherits "open"
  }
}

# Per-workstation control
workstations = {
  "prod-ws" = {
    capacity_reservation_preference = "open"  # Use ODCR
    subnet_id = "subnet-123"
  }
  "dev-ws" = {
    # capacity_reservation_preference = "none"  # Optional - omit if you don't use ODCR
    subnet_id = "subnet-456"
  }
}

Software Installation

Available packages: Any valid Chocolatey package. Common examples: git, vscode, notepadplusplus, 7zip

presets = {
  "ue-dev" = {
    instance_type = "g4dn.2xlarge"
    software_packages = ["git", "vscode", "notepadplusplus"]
  }
}

Troubleshooting

Common Issues

Instance Launch Failures

  • Verify AMI exists: aws ec2 describe-images --owners self --filters "Name=name,Values=*windows-server-2025*"
  • Check AMI is in correct region
  • Ensure Packer build completed successfully

Drive Letter Issues

  • Check drive assignment: Get-Disk | Format-Table Number, Size, BusType
  • Volume scripts re-run automatically when volume configuration changes

Connection Timeouts

  • Check security group allows your IP: curl https://checkip.amazonaws.com/
  • Verify instance is running: aws ec2 describe-instances
  • Test port connectivity: telnet <instance-ip> 8443

Password Retrieval Issues

  • Wait 5-10 minutes after instance launch for password generation
  • Check Secrets Manager if user passwords not available
  • Use S3 backup key if Terraform output fails

DCV "Connecting" Spinner

  • Connect via SSM: aws ssm start-session --target <instance-id>
  • Check DCV sessions: dcv list-sessions
  • Restart DCV service: Restart-Service dcvserver

VPN Connection Issues

  • Check VPN endpoint DNS resolves: nslookup [endpoint].prod.clientvpn.us-east-1.amazonaws.com
  • Wait 5-15 minutes for AWS to activate endpoint
  • Check for CIDR conflicts with local network
  • Disconnect from other VPNs

User Accounts Not Created

  • Check SSM command status: aws ssm list-command-invocations --instance-id <id>
  • Check user creation status: aws ssm get-parameter --name "/{project}/{workstation}/users/{username}/status_user_creation"
  • Scripts re-run automatically when user configuration changes

Volume Initialization Issues

  • Check volume status: aws ssm get-parameter --name "/{project}/{workstation}/volume_status"
  • Check volume messages: aws ssm get-parameter --name "/{project}/{workstation}/volume_message"
  • Scripts re-run automatically when volume configuration changes

Volume Resize Issues

  • Check disk sizes vs partition sizes: Get-Disk | Format-Table Number, Size, BusType
  • Check partition sizes: Get-Partition | Format-Table DiskNumber, DriveLetter, Size
  • Manual partition extension: Resize-Partition -DriveLetter F -Size (Get-PartitionSupportedSize -DriveLetter F).SizeMax

Manual Volume Initialization (if SSM script failed)

# Initialize any RAW disks
Get-Disk | Where-Object { $_.PartitionStyle -eq 'RAW' } |
Initialize-Disk -PartitionStyle MBR -PassThru |
New-Partition -AssignDriveLetter -UseMaximumSize |
Format-Volume -FileSystem NTFS -Confirm:$false

Software Installation Problems

  • Check software status: aws ssm get-parameter --name "/{project}/{workstation}/software_status"
  • Check failed packages: aws ssm get-parameter --name "/{project}/{workstation}/software_message"
  • Scripts re-run automatically when software configuration changes

Debug Commands

# Basic connectivity
curl https://checkip.amazonaws.com/
telnet <instance-ip> 8443

# SSM access (no network needed)
aws ssm start-session --target <instance-id>

# VPN testing
ping naruto-uzumaki.vdi.internal
nslookup naruto-uzumaki.vdi.internal

# Volume troubleshooting
INSTANCE_ID=$(terraform output -json connection_info | jq -r '."vdi-001".instance_id')
aws ssm list-command-invocations --instance-id $INSTANCE_ID --filters Key=DocumentName,Values=cgd-dev-initialize-volumes
aws ssm get-command-invocation --command-id <COMMAND_ID> --instance-id $INSTANCE_ID

Password Retrieval

# Administrator password
terraform output -json private_keys | jq -r '."vdi-001"' > temp_key.pem
aws ec2 get-password-data --instance-id <id> --priv-launch-key temp_key.pem

# User passwords
aws secretsmanager get-secret-value --secret-id "cgd/vdi-001/users/naruto-uzumaki"

Contributing

See the Contributing Guidelines for information on how to contribute to this project.

License

This project is licensed under the MIT-0 License. See the LICENSE file for details.

Requirements

Name Version
terraform >= 1.13
aws ~> 6.0
awscc ~> 1.0
http ~> 3.0
null ~> 3.0
random ~> 3.0
time ~> 0.9
tls ~> 4.0

Providers

Name Version
aws ~> 6.0
awscc ~> 1.0
random ~> 3.0
time ~> 0.9
tls ~> 4.0

Modules

No modules.

Resources

Name Type
aws_acm_certificate.client_vpn_ca resource
aws_acm_certificate.client_vpn_server resource
aws_cloudwatch_log_group.client_vpn_logs resource
aws_cloudwatch_log_group.vdi_logs resource
aws_cloudwatch_log_stream.client_vpn_logs resource
aws_ebs_volume.workstation_volumes resource
aws_ec2_client_vpn_authorization_rule.vdi resource
aws_ec2_client_vpn_endpoint.vdi resource
aws_ec2_client_vpn_network_association.vdi resource
aws_eip.workstation_eips resource
aws_iam_instance_profile.vdi_instance_profile resource
aws_iam_role.vdi_instance_role resource
aws_iam_role_policy.vdi_instance_access resource
aws_iam_role_policy_attachment.additional_policies resource
aws_iam_role_policy_attachment.vdi_cloudwatch_agent resource
aws_iam_role_policy_attachment.vdi_ssm_managed_instance_core resource
aws_instance.workstations resource
aws_key_pair.workstation_keys resource
aws_route53_record.user_dns_records resource
aws_route53_zone.private resource
aws_route53_zone.vdi_internal resource
aws_s3_bucket.keys resource
aws_s3_bucket.scripts resource
aws_s3_bucket.vpn_configs resource
aws_s3_bucket_public_access_block.keys resource
aws_s3_bucket_public_access_block.scripts resource
aws_s3_bucket_public_access_block.vpn_configs resource
aws_s3_bucket_server_side_encryption_configuration.keys resource
aws_s3_bucket_server_side_encryption_configuration.scripts resource
aws_s3_bucket_versioning.keys resource
aws_s3_bucket_versioning.scripts resource
aws_s3_object.emergency_private_keys resource
aws_s3_object.user_ca_certificates resource
aws_s3_object.user_certificates resource
aws_s3_object.user_private_keys resource
aws_s3_object.vpn_client_configs resource
aws_security_group.workstation resource
aws_ssm_association.software_installation resource
aws_ssm_association.vdi_user_creation resource
aws_ssm_association.volume_initialization resource
aws_ssm_document.create_vdi_users resource
aws_ssm_document.initialize_volumes resource
aws_ssm_document.install_software resource
aws_ssm_parameter.vdi_dns resource
aws_volume_attachment.workstation_volume_attachments resource
aws_vpc_security_group_egress_rule.all_outbound resource
aws_vpc_security_group_ingress_rule.dcv_https_access resource
aws_vpc_security_group_ingress_rule.dcv_quic_access resource
aws_vpc_security_group_ingress_rule.https_access resource
aws_vpc_security_group_ingress_rule.rdp_access resource
aws_vpc_security_group_ingress_rule.rdp_access_additional resource
awscc_secretsmanager_secret.user_passwords resource
random_id.suffix resource
random_string.bucket_suffix resource
time_sleep.wait_for_ssm_agent resource
tls_cert_request.client_vpn_server resource
tls_cert_request.client_vpn_users resource
tls_locally_signed_cert.client_vpn_server resource
tls_locally_signed_cert.client_vpn_users resource
tls_private_key.client_vpn_ca resource
tls_private_key.client_vpn_server resource
tls_private_key.client_vpn_users resource
tls_private_key.workstation_keys resource
tls_self_signed_cert.client_vpn_ca resource
aws_iam_policy_document.vdi_instance_access data source
aws_region.current data source
aws_subnet.workstation_subnets data source
aws_vpc.selected data source

Inputs

Name Description Type Default Required
region AWS region for deployment string n/a yes
vpc_id VPC ID where VDI instances will be deployed string n/a yes
capacity_reservation_preference Capacity reservation preference for EC2 instances string null no
client_vpn_config Client VPN configuration for private connectivity
object({
client_cidr_block = optional(string, "192.168.0.0/16")
generate_client_configs = optional(bool, true)
split_tunnel = optional(bool, true)
})
{} no
create_client_vpn Create AWS Client VPN endpoint infrastructure (VPN endpoint, certificates, S3 bucket for configs) bool false no
create_default_security_groups Create default security groups for VDI workstations bool true no
debug Enable debug mode. When true, disables termination protection for CI/CD environments. When false, enables termination protection for production environments. bool false no
ebs_kms_key_id KMS key ID for EBS encryption (if encryption enabled) string null no
enable_centralized_logging Enable centralized logging with CloudWatch log groups following CGD Toolkit patterns bool false no
environment Environment name (dev, staging, prod, etc.) string "dev" no
log_retention_days CloudWatch log retention period in days number 30 no
presets Configuration blueprints defining instance types and named volumes with Windows drive mapping.

KEY BECOMES PRESET NAME: The map key (e.g., "ue-developer") becomes the preset name referenced by workstations.

Presets provide reusable configurations that can be referenced by multiple workstations via preset_key.

Example:
presets = {
"ue-developer" = { # ← This key becomes the preset name
instance_type = "g4dn.2xlarge"
gpu_enabled = true
volumes = {
Root = { capacity = 256, type = "gp3" } # Root volume automatically gets C:
Projects = { capacity = 1024, type = "gp3", windows_drive = "Z:" } # Specify drive letter
Cache = { capacity = 500, type = "gp3" } # Auto-assigned high-alphabet letter (Y:, X:, etc.)
}
}
"basic-workstation" = { # ← Another preset name
instance_type = "g4dn.xlarge"
gpu_enabled = true
volumes = {
Root = { capacity = 200, type = "gp3" } # Root volume automatically gets C:
UserData = { capacity = 500, type = "gp3" } # Auto-assigned high-alphabet letter
}
}
}

# Referenced by workstations:
workstations = {
"alice-ws" = {
preset_key = "ue-developer" # ← References preset by key
}
}

Valid volume types: "gp2", "gp3", "io1", "io2"
Drive letters are auto-assigned by Windows (typically C: for root, D:, E:, F:, etc. for additional volumes).

additional_policy_arns: List of additional IAM policy ARNs to attach to the VDI instance role.
Example: ["arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess", "arn:aws:iam::123456789012:policy/MyCustomPolicy"]
map(object({
# Core compute configuration
instance_type = string
ami = optional(string, null)

# Hardware configuration
gpu_enabled = optional(bool, true)

# Named volumes with auto-assigned drive letters
volumes = map(object({
capacity = number
type = string
iops = optional(number, 3000)
throughput = optional(number, 125)
encrypted = optional(bool, true)
}))

# Optional configuration
iam_instance_profile = optional(string, null)
additional_policy_arns = optional(list(string), []) # Additional IAM policy ARNs to attach to the VDI instance role
software_packages = optional(list(string), null)
tags = optional(map(string), {})
}))
{} no
project_prefix Prefix for resource names string "cgd" no
tags Tags to apply to resources. map(any)
{
"IaC": "Terraform",
"ModuleBy": "CGD-Toolkit",
"ModuleName": "terraform-aws-vdi",
"ModuleSource": "https://github.com/aws-games/cloud-game-development-toolkit/tree/main/modules/vdi",
"RootModuleName": "-"
}
no
users Local Windows user accounts with Windows group types and network connectivity (managed via Secrets Manager)

KEY BECOMES WINDOWS USERNAME: The map key (e.g., "john-doe") becomes the actual Windows username created on VDI instances.

type options (Windows groups):
- "fleet_administrator": User added to Windows Administrators group, created on ALL workstations (fleet management)
- "administrator": User added to Windows Administrators group, created only on assigned workstation
- "user": User added to Windows Users group, created only on assigned workstation

use_client_vpn options (VPN access):
- false: User accesses VDI via public internet or external VPN (default)
- true: User accesses VDI via module's Client VPN (generates VPN config)

Example:
users = {
"vdiadmin" = { # ← This key becomes Windows username "vdiadmin"
given_name = "VDI"
family_name = "Administrator"
email = "admin@example.com"
type = "fleet_administrator" # Windows Administrators group on ALL workstations
use_client_vpn = false # Accesses via public internet/external VPN
}
"alice" = { # ← Public connectivity user
given_name = "Alice"
family_name = "Smith"
email = "alice@example.com"
type = "user" # Windows Users group
use_client_vpn = false # Accesses via public internet (allowed_cidr_blocks)
}
"bob" = { # ← Private connectivity user
given_name = "Bob"
family_name = "Johnson"
email = "bob@example.com"
type = "user" # Windows Users group
use_client_vpn = true # Accesses via module's Client VPN
}
}

# User assignment is now direct:
# assigned_user = "naruto-uzumaki" # References users{} key directly in workstation
map(object({
given_name = string
family_name = string
email = string
type = optional(string, "user") # "administrator" or "user" (Windows group)
use_client_vpn = optional(bool, false) # Whether this user connects via module's Client VPN
tags = optional(map(string), {})
}))
{} no
workstations Physical infrastructure instances with template references and placement configuration.

KEY BECOMES WORKSTATION NAME: The map key (e.g., "alice-workstation") becomes the workstation identifier used throughout the module.

Workstations inherit configuration from templates via preset_key reference.

Example:
workstations = {
# Public connectivity - user accesses via internet
"alice-workstation" = {
preset_key = "ue-developer"
subnet_id = "subnet-public-123" # Public subnet
security_groups = ["sg-vdi-public"]
assigned_user = "alice"
allowed_cidr_blocks = ["203.0.113.1/32"] # Alice's home IP
}
# Private connectivity - user accesses via VPN
"bob-workstation" = {
preset_key = "basic-workstation"
subnet_id = "subnet-private-456" # Private subnet
security_groups = ["sg-vdi-private"]
assigned_user = "bob"
# No allowed_cidr_blocks - accessed via Client VPN
}
# Additional volumes at workstation level
"dev-workstation" = {
preset_key = "basic-workstation"
subnet_id = "subnet-private-789"
security_groups = ["sg-vdi-private"]
volumes = {
ExtraStorage = { capacity = 2000, type = "gp3", windows_drive = "Y:" }
}
}
}

# User assignment is now direct:
# assigned_user = "alice" # References users{} key directly in workstation

Drive letters are auto-assigned by Windows. Users can reassign them via Disk Management if needed.

additional_policy_arns: List of additional IAM policy ARNs to attach to the VDI instance role.
Example: ["arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess", "arn:aws:iam::123456789012:policy/MyCustomPolicy"]
map(object({
# Preset reference (optional - can use direct config instead)
preset_key = optional(string, null)

# Infrastructure placement
subnet_id = string
security_groups = list(string)
assigned_user = optional(string, null) # User assigned to this workstation (for administrator/user types only)

# Direct configuration (used when preset_key is null or as overrides)
ami = optional(string, null)
instance_type = optional(string, null)
gpu_enabled = optional(bool, null)
volumes = optional(map(object({
capacity = number
type = string
iops = optional(number, 3000)
throughput = optional(number, 125)
encrypted = optional(bool, true)
})), null)
iam_instance_profile = optional(string, null)
additional_policy_arns = optional(list(string), []) # Additional IAM policy ARNs to attach to the VDI instance role
software_packages = optional(list(string), null)

# Optional overrides
allowed_cidr_blocks = optional(list(string), null)
capacity_reservation_preference = optional(string, null)
tags = optional(map(string), null)
}))
{} no

Outputs

Name Description
ami_id AMI ID used for workstations
connection_info Complete connection information for VDI workstations
emergency_key_paths S3 paths for emergency private keys
private_keys Private keys for emergency access (sensitive)
private_zone_id Private hosted zone ID for creating additional VPC associations
private_zone_name Private hosted zone name
public_ips Map of workstation public IP addresses
vpn_configs_bucket S3 bucket name for VPN configuration files

Volume Management

Dynamic Volume Operations

Adding/Resizing Volumes:

  1. Add or modify volumes in Terraform configuration
  2. Run terraform apply
  3. Wait 5-10 minutes for automatic SSM volume script execution
  4. Verify volumes are initialized via RDP

How it works:

  • Fully automated - Lifecycle rules handle all triggering
  • Reliable triggering - Automatically detects volume changes
  • Predictable timing - 5-10 minutes for script execution
  • Proper cleanup - Drive letters managed automatically
  • Clean plans - No continuous Terraform drift

Alternative: Manual Administration

For immediate results, you can skip waiting for SSM and manually initialize volumes:

  1. RDP to instance as Administrator after terraform apply
  2. Run PowerShell commands to initialize volumes immediately
  3. Complete in under 2 minutes with full control

Volume Limitations

Volume Size Reduction - NOT SUPPORTED

AWS Limitation: EBS volumes cannot be reduced in size. This is an AWS platform limitation, not a module limitation.

What Happens: If you reduce volume capacity in Terraform (e.g., 500GB → 200GB):

terraform apply
# ❌ Error: InvalidParameterValue: Cannot decrease volume size from 500 to 200
# ❌ The apply will FAIL IMMEDIATELY - no waiting required

Workaround for Size Reduction:

  1. Create new smaller volume in Terraform config
  2. Manually migrate data from old to new volume via RDP
  3. Remove old volume from Terraform config
  4. Apply changes - old volume will be deleted