diff --git a/.gitignore b/.gitignore
index af5a0463..72557eb6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -76,3 +76,6 @@ id_ed25519.pub
# Kiro context files
.kiro
+
+# Claude Code configuration
+CLAUDE.md
diff --git a/modules/perforce/README.md b/modules/perforce/README.md
index 6b05a414..05c5cfc5 100644
--- a/modules/perforce/README.md
+++ b/modules/perforce/README.md
@@ -258,5 +258,7 @@ packer build perforce_x86.pkr.hcl
| [p4\_server\_super\_user\_password\_secret\_arn](#output\_p4\_server\_super\_user\_password\_secret\_arn) | The ARN of the AWS Secrets Manager secret holding your P4 Server super user's username. |
| [p4\_server\_super\_user\_username\_secret\_arn](#output\_p4\_server\_super\_user\_username\_secret\_arn) | The ARN of the AWS Secrets Manager secret holding your P4 Server super user's password. |
| [shared\_application\_load\_balancer\_arn](#output\_shared\_application\_load\_balancer\_arn) | The ARN of the shared application load balancer. |
+| [shared\_application\_load\_balancer\_dns\_name](#output\_shared\_application\_load\_balancer\_dns\_name) | The DNS name of the shared application load balancer. |
+| [shared\_application\_load\_balancer\_zone\_id](#output\_shared\_application\_load\_balancer\_zone\_id) | The zone ID of the shared application load balancer. |
| [shared\_network\_load\_balancer\_arn](#output\_shared\_network\_load\_balancer\_arn) | The ARN of the shared network load balancer. |
diff --git a/modules/perforce/modules/p4-code-review/elasticache.tf b/modules/perforce/modules/p4-code-review/elasticache.tf
index d4320dad..6c3b53d7 100644
--- a/modules/perforce/modules/p4-code-review/elasticache.tf
+++ b/modules/perforce/modules/p4-code-review/elasticache.tf
@@ -7,6 +7,7 @@ resource "aws_elasticache_subnet_group" "subnet_group" {
# Single Node Elasticache Cluster for P4 Code Review
resource "aws_elasticache_cluster" "cluster" {
+ #checkov:skip=CKV_AWS_134:Automatic backups not required for development/sample environments
count = var.existing_redis_connection != null ? 0 : 1
cluster_id = "${local.name_prefix}-elasticache-redis-cluster"
engine = "redis"
diff --git a/modules/perforce/outputs.tf b/modules/perforce/outputs.tf
index 39a44825..44cb8792 100644
--- a/modules/perforce/outputs.tf
+++ b/modules/perforce/outputs.tf
@@ -8,6 +8,16 @@ output "shared_application_load_balancer_arn" {
description = "The ARN of the shared application load balancer."
}
+output "shared_application_load_balancer_dns_name" {
+ value = var.create_shared_application_load_balancer ? aws_lb.perforce_web_services[0].dns_name : null
+ description = "The DNS name of the shared application load balancer."
+}
+
+output "shared_application_load_balancer_zone_id" {
+ value = var.create_shared_application_load_balancer ? aws_lb.perforce_web_services[0].zone_id : null
+ description = "The zone ID of the shared application load balancer."
+}
+
# P4 Server
output "p4_server_eip_public_ip" {
value = var.p4_server_config != null ? module.p4_server[0].eip_public_ip : null
diff --git a/modules/teamcity/local.tf b/modules/teamcity/local.tf
index 268fc08c..370e175c 100644
--- a/modules/teamcity/local.tf
+++ b/modules/teamcity/local.tf
@@ -17,6 +17,9 @@ locals {
efs_file_system_arn = var.efs_id != null ? data.aws_efs_file_system.efs_file_system[0].arn : aws_efs_file_system.teamcity_efs_file_system[0].arn
efs_access_point_id = var.efs_access_point_id != null ? var.efs_access_point_id : aws_efs_access_point.teamcity_efs_data_access_point[0].id
+ # Service Connect namespace
+ service_connect_namespace_arn = aws_service_discovery_http_namespace.teamcity.arn
+
# TeamCity Server Information
# Set environment variables
base_env = [
@@ -40,9 +43,6 @@ locals {
value = local.database_master_password
}
] : []
-
- # Service Connect namespace
- service_connect_namespace_arn = aws_service_discovery_http_namespace.teamcity.arn
}
data "aws_region" "current" {}
diff --git a/samples/unity-build-pipeline/README.md b/samples/unity-build-pipeline/README.md
new file mode 100644
index 00000000..99f84716
--- /dev/null
+++ b/samples/unity-build-pipeline/README.md
@@ -0,0 +1,1064 @@
+# Unity Build Pipeline
+
+This sample demonstrates how to deploy a complete Unity build pipeline on AWS using the Cloud Game Development Toolkit. This configuration is designed for production Unity game development workflows and includes version control, CI/CD, asset acceleration, license management, and artifact storage.
+
+## Architecture Overview
+
+This Unity build pipeline consists of the following components:
+
+```
+┌─────────────────────────────────────────────────────────────────────┐
+│ Unity Build Pipeline │
+├─────────────────────────────────────────────────────────────────────┤
+│ │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ Perforce │ │ TeamCity │ │ Unity │ │
+│ │ │ │ │ │ │ │
+│ │ • P4 Server │ │ • Server │ │ • Accelera- │ │
+│ │ • P4 Swarm │ │ • Agents │ │ tor │ │
+│ │ │ │ │ │ • License │ │
+│ │ │ │ │ │ Server │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ │
+│ │
+│ ┌──────────────────────┐ │
+│ │ S3 Artifacts │ │
+│ │ Bucket │ │
+│ └──────────────────────┘ │
+│ │
+└─────────────────────────────────────────────────────────────────────┘
+```
+
+### Components
+
+1. **Perforce (Version Control)**
+ - P4 Server (Helix Core) for source code management
+ - P4 Swarm (Code Review) for peer code reviews
+ - Deployed on EC2 (P4 Server) and ECS (Swarm) with persistent EBS storage
+
+2. **TeamCity (CI/CD)**
+ - TeamCity Server for build orchestration
+ - Auto-scaling build agents on Fargate
+ - Integration with Perforce for source control
+
+3. **Unity Accelerator**
+ - Caches Unity Library folder artifacts
+ - Reduces import times for distributed teams
+ - Deployed on ECS with persistent storage
+
+4. **Unity Floating License Server**
+ - Manages Unity Pro/Enterprise licenses
+ - Supports concurrent license checkout
+ - Deployed on ECS with high availability
+
+5. **S3 Artifacts Bucket**
+ - Stores build outputs (executables, asset bundles)
+ - Lifecycle policies for cost optimization
+ - Versioning enabled for rollback capability
+
+## Features
+
+- **Complete CI/CD Pipeline**: From code commit to build artifact
+- **Version Control**: Enterprise-grade Perforce deployment
+- **Build Acceleration**: Unity Accelerator for faster iteration
+- **License Management**: Floating license server for team collaboration
+- **Secure Access**: HTTPS everywhere with SSL/TLS certificates
+- **DNS Integration**: Custom domain names for all services
+- **High Availability**: Multi-AZ deployments where applicable
+- **Cost Optimized**: Auto-scaling and right-sized resources
+
+## Prerequisites
+
+Before deploying this pipeline, ensure you have:
+
+1. **Tools Installed**
+ - [Terraform CLI](https://developer.hashicorp.com/terraform/install) (>= 1.0)
+ - [Packer CLI](https://developer.hashicorp.com/packer/install) (for AMI creation)
+ - [AWS CLI v2](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
+
+2. **AWS Account**
+ - AWS account with appropriate permissions
+ - AWS CLI configured with credentials
+ - Sufficient service quotas for ECS, EC2, and RDS
+
+3. **Domain and DNS**
+ - Route53 hosted zone for your domain
+ - Ability to validate SSL certificates via DNS
+
+4. **Unity Licensing**
+ - Unity Pro or Enterprise license with floating license entitlement
+ - Unity Floating License Server binaries (download from https://id.unity.com/)
+ - Valid `services-config.json` license file from Unity
+
+## Deployment Guide
+
+### Phase 1: Predeployment
+
+#### Step 1: Create Perforce Server AMI
+
+The Perforce module requires a custom AMI with Perforce Helix Core pre-installed.
+
+> **Important**: If building on Windows, use WSL or a Unix-based system to avoid line ending issues with shell scripts.
+
+```bash
+# Navigate to the Packer template directory
+cd assets/packer/perforce/p4-server
+
+# Initialize Packer
+packer init perforce_x86.pkr.hcl
+
+# Build the AMI (this will use your default AWS region)
+packer build perforce_x86.pkr.hcl
+```
+
+Take note of the AMI ID from the output.
+
+**Verify the AMI was created successfully:**
+
+```bash
+# Verify the AMI is available in your account
+aws ec2 describe-images \
+ --owners self \
+ --filters "Name=name,Values=p4_al2023*" \
+ --query 'Images[0].[ImageId,Name,CreationDate]' \
+ --output table
+```
+
+The Terraform configuration will automatically discover this AMI by searching for images with the name pattern `p4_al2023*` owned by your account.
+
+> **Note**: The AMI must be created in the same AWS region where you plan to deploy the pipeline.
+
+#### Step 2: Create Route53 Hosted Zone
+
+This pipeline requires a Route53 hosted zone for DNS records and SSL certificate validation.
+
+**Option A: Register a new domain with Route53**
+- Follow the [Route53 domain registration guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register.html)
+- A hosted zone is automatically created
+
+**Option B: Use an existing domain**
+- Follow the guide to [make Route53 the DNS service](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/MigratingDNS.html)
+- Create a hosted zone for your domain
+
+Your services will be accessible at subdomains:
+- `perforce.yourdomain.com` - Perforce server
+- `swarm.yourdomain.com` - P4 Swarm (Code Review)
+- `teamcity.yourdomain.com` - TeamCity server
+- `unity-accelerator.yourdomain.com` - Unity Accelerator
+- `unity-license.yourdomain.com` - Unity License Server
+
+#### Step 3: Download Unity License Server Binaries
+
+Download the Unity Floating License Server installer:
+
+1. Log in to https://id.unity.com/
+2. Navigate to "Organizations" → Select your organization → "Subscriptions"
+3. Download the **Linux version** of the Unity Floating License Server (e.g., `Unity.Licensing.Server.linux-x64-v2.1.0.zip`)
+4. Save the file to a known location on your machine - you'll reference this path in `terraform.tfvars`
+
+> **Important - License Server Binding**: The Unity License Server binds itself to the machine's identity on first startup, including:
+> - MAC address
+> - Operating system
+> - Number of processor cores
+> - Server name
+>
+> This module uses an **Elastic Network Interface (ENI)** to provide a stable MAC address, and enables **EC2 termination protection** by default. However, if the EC2 instance is destroyed and recreated (not just stopped/started), you will need to contact Unity Support to revoke the old registration before deploying a new one. This process can take several days.
+>
+> To protect against accidental deletion, consider adding `prevent_destroy` lifecycle rules after initial deployment (see Phase 3, Step 5).
+
+#### Step 4: Build Unity TeamCity Agent Docker Image
+
+> **Critical**: This step must be completed BEFORE running `terraform apply`. The TeamCity agent deployment requires a valid Docker image URI.
+
+The sample includes a Dockerfile for building a Unity TeamCity build agent. This image combines Unity Editor, TeamCity agent runtime, and necessary tools (Perforce P4 CLI, AWS CLI, Git).
+
+**Quick build steps:**
+
+```bash
+# Navigate to the Docker directory
+cd docker/teamcity-unity-build-agent/
+
+# Step 1: Create ECR repository
+aws ecr create-repository --repository-name unity-teamcity-agent --region us-east-1
+
+# Step 2: Log in to ECR
+aws ecr get-login-password --region us-east-1 | \
+ docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com
+
+# Step 3: Build the image (takes 15-30 minutes)
+docker build \
+ --build-arg UNITY_VERSION=6000.0.23f1 \
+ --build-arg UNITY_CHANGESET=bd20d88e54b8 \
+ -t unity-teamcity-agent:latest \
+ .
+
+# Step 4: Tag and push to ECR
+docker tag unity-teamcity-agent:latest \
+ $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest
+
+docker push $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest
+
+# Step 5: Get your image URI for terraform.tfvars
+echo "$(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest"
+```
+
+Copy the image URI from the output - you'll need it for your `terraform.tfvars` file in the next phase.
+
+> **Note**: For detailed instructions including how to find Unity version/changeset information and building different Unity versions, see the comprehensive guide in `docker/README.md`.
+
+### Phase 2: Deployment
+
+#### Step 1: Configure Variables
+
+Navigate to the unity-build-pipeline directory and create your `terraform.tfvars` file from the example:
+
+```bash
+cd samples/unity-build-pipeline
+cp terraform.tfvars.example terraform.tfvars
+```
+
+Edit `terraform.tfvars` with your values:
+
+```hcl
+# Your Route53 hosted zone domain name
+route53_public_hosted_zone_name = "yourdomain.com"
+
+# Path to Unity License Server zip file (from Phase 1, Step 3)
+unity_license_server_file_path = "/path/to/Unity.Licensing.Server.linux-x64-v2.1.0.zip"
+
+# Unity TeamCity agent Docker image URI (from Phase 1, Step 4)
+unity_teamcity_agent_image = "123456789012.dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest"
+```
+
+#### Step 2: Review and Customize locals.tf (optional)
+Note that the values in `locals.tf` are sensible defaults and will work without changes. The option to change them is there for those who want to further customize the deployment.
+
+Edit `locals.tf` to customize:
+- **Project prefix**: `project_prefix = "ubp"` - used as a prefix for resource names
+- **Subdomain names**: Change service subdomains (e.g., `perforce_subdomain = "p4"`)
+- **VPC CIDR blocks**: Adjust network ranges if needed to avoid conflicts with existing networks
+- **Tags**: Add or modify resource tags for cost tracking or organization
+
+#### Step 3: Initialize Terraform
+
+```bash
+terraform init
+```
+
+This downloads required providers and modules.
+
+#### Step 4: Deploy the Pipeline
+
+```bash
+# Review the planned changes
+terraform plan
+
+# Apply the configuration
+terraform apply
+```
+
+The deployment takes approximately 15-20 minutes. Terraform will:
+1. Create VPC and networking infrastructure
+2. Deploy Perforce with P4 Server and P4 Swarm (Code Review)
+3. Deploy TeamCity server and agents
+4. Deploy Unity Accelerator and License Server
+5. Create S3 bucket for artifacts
+6. Configure DNS records and SSL certificates
+
+#### Step 5: Verify Deployment
+
+After `terraform apply` completes, verify all services deployed successfully:
+
+```bash
+# View all Terraform outputs
+terraform output
+
+# Check ECS services are running
+aws ecs list-services --cluster $(terraform output -raw ecs_cluster_name) | jq
+
+# Verify ECS service health
+aws ecs describe-services \
+ --cluster $(terraform output -raw ecs_cluster_name) \
+ --services $(aws ecs list-services --cluster $(terraform output -raw ecs_cluster_name) --query 'serviceArns[*]' --output text) \
+ --query 'services[*].[serviceName,status,runningCount,desiredCount]' \
+ --output table
+
+# Test DNS resolution for your services
+nslookup perforce.yourdomain.com
+nslookup swarm.yourdomain.com
+nslookup teamcity.yourdomain.com
+nslookup unity-accelerator.yourdomain.com
+nslookup unity-license.yourdomain.com
+```
+
+All services should show `ACTIVE` status with `runningCount` matching `desiredCount`.
+
+### Phase 3: Postdeployment
+
+#### Step 1: Configure Perforce (P4 Server)
+
+This section walks you through the initial Perforce setup, creating your first depot, stream, and workspace.
+
+**Step 1.1: Retrieve Administrator Credentials**
+
+```bash
+# Get the Perforce super user username
+aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw perforce_super_user_username_secret_arn) \
+ --query SecretString \
+ --output text
+
+# Get the Perforce super user password
+aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw perforce_super_user_password_secret_arn) \
+ --query SecretString \
+ --output text
+```
+
+**Step 1.2: Initial Connection and Login**
+
+```bash
+# Set your P4PORT environment variable
+export P4PORT=$(terraform output -raw p4_server_connection_string)
+
+# Set your P4USER to the super user
+export P4USER=
+
+# Login with super user credentials (enter password when prompted)
+p4 login
+
+# Verify connection
+p4 info
+```
+
+**Step 1.3: Create a Stream Depot**
+
+Stream depots are recommended for modern Perforce workflows and work well with Unity projects.
+
+```bash
+# Create a new stream depot named "game"
+p4 depot -t stream -o game | p4 depot -i
+
+# Verify the depot was created
+p4 depots
+```
+
+**Step 1.4: Create a Mainline Stream**
+
+```bash
+# Create the mainline stream
+p4 stream -t mainline -o //game/main | p4 stream -i
+
+# Verify the stream was created
+p4 streams //game/...
+```
+
+**Step 1.5: Create a Workspace and Submit Initial Files**
+
+```bash
+# Create a workspace mapped to the mainline stream
+p4 client -o -S //game/main | p4 client -i
+
+# Get the workspace name (usually _)
+p4 client -o | grep '^Client:'
+
+# Sync the workspace (will be empty initially)
+p4 sync
+
+# Navigate to your workspace root
+cd ~/perforce//
+
+# Create a README file
+echo "# Unity Game Project" > README.md
+
+# Add the file to Perforce
+p4 add README.md
+
+# Submit the changelist
+p4 submit -d "Initial commit: Add README"
+
+# Verify the file was submitted
+p4 files //game/main/...
+```
+
+**Step 1.6: Create Additional Users**
+
+```bash
+# Create a new user (replace 'developer1')
+p4 user -o developer1 | p4 user -i -f
+
+# Set a password for the new user
+p4 passwd developer1
+
+# Grant appropriate permissions (optional: edit protections table)
+p4 protect
+```
+
+Your Perforce server is now ready for your team to use. Users can connect using P4V or P4 CLI with the connection string from `terraform output p4_server_connection_string`.
+
+#### Step 2: Configure P4 Swarm (Code Review)
+
+P4 Swarm provides web-based code review for your Perforce projects. It automatically connects to your P4 Server and configures itself on first access.
+
+**Step 2.1: Initial Access**
+
+Visit `https://swarm.yourdomain.com` in your browser. On first access, Swarm will:
+1. Automatically connect to the P4 Server
+2. Install required triggers on the P4 Server
+3. Complete initial setup
+
+**Step 2.2: Log In**
+
+Log in with your Perforce credentials (the same username/password you use for P4V or P4 CLI). The super user credentials can be retrieved with:
+
+```bash
+# Get Perforce super user credentials
+aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw perforce_super_user_username_secret_arn) \
+ --query SecretString --output text
+
+aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw perforce_super_user_password_secret_arn) \
+ --query SecretString --output text
+```
+
+**Step 2.3: Verify Setup**
+
+Once logged in:
+1. Navigate to the Projects page - you should see your Perforce depot(s)
+2. Create a test review to verify functionality:
+ - Make a shelved changelist in Perforce
+ - In Swarm, create a review from the shelved changelist
+ - Add reviewers and verify email notifications work
+
+Your team can now use Swarm for code reviews before submitting changes to mainline!
+
+#### Step 3: Configure TeamCity
+
+TeamCity is deployed with an Aurora Serverless PostgreSQL database that is automatically configured via environment variables.
+
+**Step 3.1: Complete Initial Setup Wizard**
+
+1. Visit `https://teamcity.yourdomain.com`
+
+2. On first access, TeamCity will display the setup wizard. Follow the prompts:
+ - **Data Directory**: Pre-configured, click "Proceed"
+ - **Database Connection**: Automatically configured, click "Proceed"
+ - **License Agreement**: Accept the JetBrains agreement
+ - **Create Administrator Account**: Set up your admin user credentials
+
+3. TeamCity will initialize the database and complete setup (takes 2-3 minutes)
+
+**Step 3.2: Authorize Build Agents**
+
+The Unity build agents you deployed will automatically register with the TeamCity server but require authorization.
+
+1. Navigate to **Agents** → **Unauthorized**
+2. You should see your Unity build agents listed (e.g., `unity-builder-xxxx`)
+3. Click on each agent and click **Authorize**
+4. (Optional) Assign agents to specific agent pools based on your build requirements
+
+The agents are now ready to accept build jobs.
+
+**Step 3.3: Configure Perforce VCS Root**
+
+To connect TeamCity to your Perforce server:
+
+1. Navigate to **Administration** → **VCS Roots**
+2. Click **Create VCS Root**
+3. Select **Perforce** as the VCS type
+4. Configure the connection:
+ - **VCS root name**: `Perforce Main`
+ - **Port**: `ssl:perforce.yourdomain.com:1666`
+ - **Stream**: `//game/main` (or your depot/stream path)
+ - **Authentication**: Enter Perforce username and password
+5. Click **Test Connection** to verify
+6. Click **Create**
+
+Your TeamCity server is now connected to Perforce and ready to run builds.
+
+#### Step 4: Configure Unity Accelerator
+
+**Access Unity Accelerator Dashboard**:
+
+1. **Get the dashboard credentials**:
+ ```bash
+ # Get username
+ aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw unity_accelerator_dashboard_username_secret_arn) \
+ --query SecretString --output text
+
+ # Get password
+ aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw unity_accelerator_dashboard_password_secret_arn) \
+ --query SecretString --output text
+ ```
+
+2. **Log in to the dashboard**:
+ - Visit `https://unity-accelerator.yourdomain.com`
+ - Enter the username and password from above
+ - Review cache statistics and configuration
+
+**Configure Unity Editor to Use Accelerator**:
+
+In Unity Editor preferences:
+1. Go to Preferences → Asset Pipeline → Cache Server
+2. Set mode to "Remote"
+3. Enter IP: `unity-accelerator.yourdomain.com`
+4. Port: `10080`
+5. Enable "Download" and "Upload"
+
+The Accelerator will cache Unity Library folder artifacts and dramatically reduce import times for your team.
+
+#### Step 5: Configure Unity License Server
+
+The Unity Floating License Server requires a multi-step registration process with Unity to activate your floating licenses.
+
+**Step 5.1: Download Server Registration Request**
+
+After deployment, the license server creates a registration request file containing the server's machine binding information.
+
+```bash
+# Download the server registration request file
+wget $(terraform output -raw unity_license_server_registration_request_url) \
+ -O server-registration-request.xml
+```
+
+> **Note**: This presigned URL is valid for 1 hour. If expired, regenerate with `terraform refresh`.
+
+**Step 5.2: Register Server with Unity**
+
+1. Log in to https://id.unity.com/
+2. Navigate to "Organizations" → Select your organization → "Subscriptions"
+3. Upload the `server-registration-request.xml` file
+4. Download the licenses zip file Unity provides (e.g., `Unity_v2024.x_Linux.zip`)
+
+> **Important**: Do not rename the licenses zip file - Unity expects the original filename.
+
+**Step 5.3: Upload Licenses to S3**
+
+```bash
+# Upload licenses zip (replace with your actual filename)
+aws s3 cp Unity_v2024.x_Linux.zip \
+ s3://$(terraform output -raw unity_license_server_s3_bucket)/
+```
+
+The license server monitors this bucket and will automatically import licenses within 60 seconds.
+
+**Step 5.4: Verify License Import**
+
+```bash
+# Get dashboard URL and password
+echo $(terraform output -raw unity_license_server_url)
+
+aws secretsmanager get-secret-value \
+ --secret-id $(terraform output -raw unity_license_server_dashboard_password_secret_arn) \
+ --query SecretString --output text
+```
+
+Visit the dashboard URL and log in with username `admin` and the password from above. Verify your licenses appear with status "Available".
+
+**Step 5.5: Configure Client Access to License Server**
+
+Unity clients require a `services-config.json` file to connect to the license server.
+
+```bash
+# Download services-config.json
+wget $(terraform output -raw unity_license_server_services_config_url) \
+ -O services-config.json
+```
+
+**For TeamCity build agents (Docker containers):**
+
+The agents in this sample run as Docker containers in ECS. To add the services-config.json:
+
+1. **Option A: Add to Docker image** (recommended)
+
+ Edit `docker/teamcity-unity-build-agent/Dockerfile` to copy the file during build:
+ ```dockerfile
+ COPY services-config.json /usr/share/unity3d/config/services-config.json
+ ```
+
+ Then rebuild and push your Docker image.
+
+2. **Option B: Download at runtime**
+
+ Modify your container's entrypoint script to download from S3 on startup.
+
+**For developer workstations:**
+
+Deploy `services-config.json` to:
+- **Windows**: `%PROGRAMDATA%\Unity\config\services-config.json`
+- **Linux**: `/usr/share/unity3d/config/services-config.json`
+- **macOS**: `/Library/Application Support/Unity/config/services-config.json`
+
+Without this file in the correct location, Unity will not be able to connect to the license server.
+
+**Step 5.6: Protect License Server from Accidental Deletion (CRITICAL)**
+
+> **Critical**: The Unity License Server binds to its machine's MAC address. If the EC2 instance is destroyed, you must contact Unity Support to revoke the registration before deploying a new server. **Unity Support response can take up to 48 hours**, during which your team will not have access to Unity licenses.
+
+**Terraform does not respect EC2 termination protection.** Even though the module enables instance termination protection by default, running `terraform destroy` will still destroy the instance. You must add a Terraform lifecycle block to prevent deletion.
+
+Edit `main.tf` and add the lifecycle block to the Unity License Server module:
+
+```hcl
+module "unity_license_server" {
+ count = var.unity_license_server_file_path != null ? 1 : 0
+ source = "../../modules/unity/floating-license-server"
+
+ # ... existing configuration ...
+
+ lifecycle {
+ prevent_destroy = true
+ }
+}
+```
+
+Apply the change:
+
+```bash
+terraform apply
+```
+
+With this protection in place, `terraform destroy` will fail if it tries to destroy the license server, preventing accidental deletion. To intentionally destroy the license server later, you must first remove this lifecycle block.
+
+#### Step 6: Create Your First Build Configuration
+
+This section walks through creating a Unity build configuration in TeamCity.
+
+**Step 6.1: Create a TeamCity Project**
+
+1. In TeamCity, click **Administration** → **Projects**
+2. Click **Create project**
+3. Select **Manually**
+4. Enter:
+ - **Name**: `Unity Game Project`
+ - **Project ID**: `UnityGameProject` (auto-generated)
+5. Click **Create**
+
+**Step 6.2: Create a Build Configuration**
+
+1. Inside your new project, click **Create build configuration**
+2. Enter:
+ - **Name**: `Build Android`
+ - **Build configuration ID**: `BuildAndroid` (auto-generated)
+3. Click **Create**
+
+**Step 6.3: Attach VCS Root**
+
+1. Click **VCS Roots** in the left sidebar
+2. Click **Attach VCS root**
+3. Select the Perforce VCS root you created in Step 3.3
+4. Click **Attach**
+
+**Step 6.4: Add Build Steps**
+
+1. Click **Build Steps** in the left sidebar
+2. Click **Add build step**
+3. Select **Command Line** as the runner type
+4. Configure:
+ - **Step name**: `Unity Build`
+ - **Run**: `Custom script`
+ - **Custom script**:
+ ```bash
+ # Unity build command
+ /opt/unity/Editor/Unity \
+ -quit \
+ -batchmode \
+ -nographics \
+ -projectPath . \
+ -buildTarget Android \
+ -logFile - \
+ -executeMethod BuildScript.Build
+ ```
+5. Click **Save**
+
+> **Note**: The `-executeMethod BuildScript.Build` assumes you have a static method in your Unity project at `Assets/Editor/BuildScript.cs`. You'll need to create this script in your Unity project to define the build logic.
+
+**Step 6.5: (Optional) Add Artifact Upload to S3**
+
+1. Click **Add build step**
+2. Select **Command Line**
+3. Configure:
+ - **Step name**: `Upload to S3`
+ - **Custom script**:
+ ```bash
+ aws s3 cp build/ s3://YOUR_BUCKET_NAME/builds/%build.number%/ --recursive
+ ```
+ - Replace `YOUR_BUCKET_NAME` with your artifacts bucket (or use a TeamCity parameter)
+4. Click **Save**
+
+**Step 6.6: Configure Build Triggers (Optional)**
+
+1. Click **Triggers** in the left sidebar
+2. Click **Add new trigger**
+3. Select **VCS Trigger**
+4. Configure to trigger on every Perforce commit
+5. Click **Save**
+
+Your build configuration is ready!
+
+#### Step 7: Verify End-to-End Pipeline
+
+Test that the entire pipeline works:
+
+1. **Commit a Unity project to Perforce** (if you haven't already)
+2. **Trigger a build** in TeamCity (Run → Run Build)
+3. **Monitor the build log** and verify:
+ - Perforce syncs successfully
+ - Unity license is checked out
+ - Unity build completes
+ - Build finishes with success status
+
+If the build completes successfully, your Unity build pipeline is fully operational!
+
+## Architecture Details
+
+### Networking
+
+```
+VPC (10.0.0.0/16)
+├── Public Subnets (10.0.1.0/24, 10.0.2.0/24)
+│ └── NAT Gateways, Load Balancers
+├── Private Subnets (10.0.3.0/24, 10.0.4.0/24)
+│ └── ECS Services, RDS, Build Agents
+└── DNS
+ └── Private Route53 zone for internal service discovery
+```
+
+### Security
+
+- **Encryption**: All data encrypted at rest and in transit
+- **Network Isolation**: Services deployed in private subnets
+- **Security Groups**: Least privilege access between services
+- **Secrets Management**: Credentials stored in AWS Secrets Manager
+- **HTTPS**: All public endpoints use TLS 1.2+
+- **IAM Roles**: Task-specific roles with minimal permissions
+
+### High Availability
+
+- **Multi-AZ**: RDS and load balancers span availability zones
+- **Auto Scaling**: TeamCity agents scale based on queue depth
+- **Health Checks**: Load balancer health checks for all services
+- **Backup**: Automated RDS snapshots for Perforce metadata
+- **Storage**: EBS volumes with snapshots for critical data
+
+### Cost Optimization
+
+- **Right-Sizing**: Instance types optimized for workload
+- **Auto Scaling**: Agents scale down when idle
+- **S3 Lifecycle**: Artifacts transition to cheaper storage tiers
+- **Spot Instances**: Optional for TeamCity build agents
+- **Scheduling**: Can stop/start non-critical services outside work hours
+
+## Build Agent Storage Strategies
+
+TeamCity build agents need access to source code and Unity assets. This sample uses ephemeral storage by default, but larger studios should consider persistent storage for performance.
+
+### Ephemeral Storage (Current Default)
+
+**How it works:** Each agent gets 50GB that's wiped when the container stops. Every new agent downloads the full repository and re-imports all Unity assets.
+
+**Best for:** Small teams, infrequent builds, demos
+**Cost:** $0/month
+**Build overhead:** 5-15 minutes per build (full P4 sync + Unity import)
+
+### EFS Persistent Storage
+
+**How it works:** Mount a shared EFS volume to `/opt/buildAgent/work`. TeamCity creates a unique working directory per agent (hash-based naming like `a54a2cadb9b4d269`). Each agent maintains its own Perforce workspace and Unity Library cache on EFS, persisting across container restarts.
+
+**Storage pattern:** With 5 agents, you'll have 5 separate directories on EFS, each containing a full P4 workspace + Unity cache. Total storage = (project size + Unity cache) × number of agents.
+
+**Setup:**
+1. Create EFS file system in your VPC
+2. Mount EFS to `/opt/buildAgent/work` in agent task definition
+3. TeamCity automatically isolates each agent to its own subdirectory
+4. First build per agent syncs fully; subsequent builds sync incrementally
+
+**Best for:** Medium teams (10-50 devs), frequent builds
+**Cost:** ~$5-30/month (15-100GB per agent × agent count)
+**Build overhead:** 30 seconds - 2 minutes (incremental sync + Unity cache reuse)
+
+### NetApp ONTAP FlexClone
+
+**How it works:** Run a scheduled job (Lambda/ECS task) that maintains a "golden" FlexVol on FSx for NetApp ONTAP—fully synced to latest Perforce changelist with Unity Library pre-imported. When a build starts, create an instant FlexClone (writable snapshot) and attach it to the agent. Agent works on the clone in isolation. After the build, delete the clone.
+
+**Storage pattern:** One golden volume (repository + Unity cache) + thin clones for active builds. 100GB repo with 10 parallel builds = 100GB parent + ~10GB deltas (not 1TB).
+
+**Setup:**
+1. Deploy FSx for NetApp ONTAP in your VPC
+2. Create golden volume update automation (nightly or on-demand)
+3. Integrate TeamCity with NetApp API to create/destroy clones per build
+4. Mount clone as `/opt/buildAgent/work` when agent starts
+
+**Update strategy:** Golden volume updates on schedule (e.g., nightly) or triggered by significant P4 changes. Builds use snapshot from last update, plus incremental P4 sync for any new changes.
+
+**Best for:** Large teams (50+ devs), high build frequency, large repos (100GB+), snapshot-based testing
+**Cost:** ~$230+/month (1TB minimum)
+**Build overhead:** < 30 seconds (clone creation + small P4 delta sync)
+
+### Quick Comparison
+
+| Approach | Monthly Cost | Storage Per Agent | Best Build Frequency |
+|----------|--------------|-------------------|---------------------|
+| **Ephemeral** | $0 | 0 (wiped) | < 10/day |
+| **EFS** | $5-30/agent | Full copy per agent | 10-100/day |
+| **NetApp** | $230+ | Shared + thin deltas | 100+/day |
+
+**Recommendation:** Start with ephemeral. Add EFS when builds exceed 10/day. Consider NetApp only at enterprise scale (50+ agents, 100GB+ repos).
+
+## Maintenance
+
+### Updating Components
+
+```bash
+# Update Terraform configuration
+git pull
+
+# Review changes
+terraform plan
+
+# Apply updates
+terraform apply
+```
+
+### Scaling
+
+**TeamCity Agents**:
+Edit `locals.tf` and modify `teamcity_agent_count` or enable auto-scaling.
+
+**Unity Accelerator**:
+Increase cache size by modifying `unity_accelerator_cache_size_gb` in `locals.tf`.
+
+**Perforce Storage**:
+Extend EBS volume size through AWS Console or CLI, then resize filesystem.
+
+### Monitoring
+
+Key metrics to monitor:
+- TeamCity build queue depth and agent utilization
+- Unity Accelerator cache hit rate
+- Perforce disk usage and connection count
+- S3 bucket size and request rates
+- RDS CPU, memory, and connection count
+
+### Backup and Disaster Recovery
+
+**Perforce**:
+- Daily automated snapshots of EBS volumes
+- RDS automated backups (7-day retention)
+- Export metadata with `p4 admin checkpoint`
+
+**TeamCity**:
+- RDS automated backups
+- Configuration exported to S3 (manual)
+
+**S3 Artifacts**:
+- Versioning enabled
+- Cross-region replication (optional)
+
+## Troubleshooting
+
+### Common Issues
+
+**Perforce connection refused**:
+- Check security group rules allow port 1666
+- Verify P4 Server service is running in ECS
+- Check DNS resolution
+
+**TeamCity agents not connecting**:
+- Verify server URL in agent configuration
+- Check security groups allow agent-to-server communication
+- Review agent logs in CloudWatch
+
+**Unity Accelerator not caching**:
+- Verify Unity Editor configuration
+- Check accelerator logs for errors
+- Ensure port 10080 is accessible
+
+**License server not responding**:
+- Verify service is running in ECS
+- Check license file validity
+- Review license server logs
+
+### Logs
+
+All services log to CloudWatch Logs:
+
+```bash
+# View Perforce logs
+aws logs tail /ecs/perforce-server --follow
+
+# View TeamCity logs
+aws logs tail /ecs/teamcity-server --follow
+
+# View Unity Accelerator logs
+aws logs tail /ecs/unity-accelerator --follow
+```
+
+## Cleanup
+
+To destroy all resources:
+
+```bash
+# Destroy all Terraform-managed resources
+terraform destroy
+```
+
+**Note**: This will NOT delete:
+- AMIs created with Packer
+- Route53 hosted zone
+- Manual secrets in Secrets Manager
+- EBS snapshots (if retention configured)
+
+Delete these manually if needed.
+
+## Cost Estimate
+
+Approximate monthly costs (us-east-1, assuming 8x5 usage):
+
+| Component | Configuration | Monthly Cost |
+|-----------|--------------|--------------|
+| Perforce (ECS + RDS) | db.t3.medium + EBS | ~$150 |
+| TeamCity (ECS + RDS) | 2x agents, db.t3.medium | ~$200 |
+| Unity Accelerator | ECS + 500GB EBS | ~$80 |
+| Unity License Server | ECS | ~$50 |
+| S3 Artifacts | 1TB storage | ~$25 |
+| VPC & Networking | NAT Gateway, data transfer | ~$45 |
+| **Total** | | **~$550/month** |
+
+*Costs vary based on usage, region, and configuration. Enable auto-scaling and stop non-critical services outside work hours to reduce costs.*
+
+## Security Considerations
+
+### Secrets Management
+- Never commit credentials to Git
+- Rotate secrets regularly
+- Use AWS Secrets Manager for all credentials
+- Enable audit logging for secret access
+
+### Network Security
+- Deploy in private subnets
+- Use security groups for least privilege access
+- Enable VPC Flow Logs for traffic analysis
+- Consider AWS PrivateLink for service endpoints
+
+### Access Control
+- Use IAM roles instead of access keys
+- Enable MFA for administrative access
+- Implement least privilege principle
+- Configure Perforce user permissions with protections table
+
+### Compliance
+- Enable CloudTrail for audit logging
+- Use AWS Config for compliance monitoring
+- Encrypt all data at rest
+- Implement backup and retention policies
+
+## Support and Resources
+
+- **CGD Toolkit Documentation**: https://aws-games.github.io/cloud-game-development-toolkit/
+- **Report Issues**: https://github.com/aws-games/cloud-game-development-toolkit/issues
+- **Discussions**: https://github.com/aws-games/cloud-game-development-toolkit/discussions
+- **Perforce Documentation**: https://www.perforce.com/manuals/p4sag/
+- **TeamCity Documentation**: https://www.jetbrains.com/help/teamcity/
+- **Unity Documentation**: https://docs.unity3d.com/
+
+## License
+
+This sample is part of the Cloud Game Development Toolkit and is licensed under MIT-0. See [LICENSE](../../LICENSE) for details.
+
+---
+
+**Built for game developers, by game developers** 🎮
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [aws](#requirement\_aws) | 6.6.0 |
+| [http](#requirement\_http) | 3.5.0 |
+| [netapp-ontap](#requirement\_netapp-ontap) | 2.3.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | 6.6.0 |
+| [http](#provider\_http) | 3.5.0 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [perforce](#module\_perforce) | ../../modules/perforce | n/a |
+| [teamcity](#module\_teamcity) | ../../modules/teamcity | n/a |
+| [unity\_accelerator](#module\_unity\_accelerator) | ../../modules/unity/accelerator | n/a |
+| [unity\_license\_server](#module\_unity\_license\_server) | ../../modules/unity/floating-license-server | n/a |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_acm_certificate.shared](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/acm_certificate) | resource |
+| [aws_acm_certificate_validation.shared](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/acm_certificate_validation) | resource |
+| [aws_default_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/default_security_group) | resource |
+| [aws_ecs_cluster.unity_pipeline_cluster](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/ecs_cluster) | resource |
+| [aws_ecs_cluster_capacity_providers.providers](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/ecs_cluster_capacity_providers) | resource |
+| [aws_eip.nat_gateway_eip](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/eip) | resource |
+| [aws_internet_gateway.igw](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/internet_gateway) | resource |
+| [aws_nat_gateway.nat_gateway](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/nat_gateway) | resource |
+| [aws_route.private_nat_access](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route) | resource |
+| [aws_route.public_internet_access](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route) | resource |
+| [aws_route53_record.certificate_validation](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route53_record) | resource |
+| [aws_route53_record.p4_server_public](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route53_record) | resource |
+| [aws_route53_record.p4_swarm_public](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route53_record) | resource |
+| [aws_route53_record.teamcity_public](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route53_record) | resource |
+| [aws_route53_record.unity_accelerator_public](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route53_record) | resource |
+| [aws_route53_record.unity_license_server_public](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route53_record) | resource |
+| [aws_route_table.private_rt](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route_table) | resource |
+| [aws_route_table.public_rt](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route_table) | resource |
+| [aws_route_table_association.private_rt_asso](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route_table_association) | resource |
+| [aws_route_table_association.public_rt_asso](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/route_table_association) | resource |
+| [aws_security_group.allow_my_ip](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/security_group) | resource |
+| [aws_subnet.private_subnets](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/subnet) | resource |
+| [aws_subnet.public_subnets](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/subnet) | resource |
+| [aws_vpc.unity_pipeline_vpc](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc) | resource |
+| [aws_vpc_security_group_ingress_rule.allow_https](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc_security_group_ingress_rule) | resource |
+| [aws_vpc_security_group_ingress_rule.allow_perforce](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc_security_group_ingress_rule) | resource |
+| [aws_vpc_security_group_ingress_rule.perforce_from_vpc](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc_security_group_ingress_rule) | resource |
+| [aws_vpc_security_group_ingress_rule.unity_license_server_from_vpc](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc_security_group_ingress_rule) | resource |
+| [aws_vpc_security_group_ingress_rule.unity_license_server_http](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc_security_group_ingress_rule) | resource |
+| [aws_vpc_security_group_ingress_rule.unity_license_server_https](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/resources/vpc_security_group_ingress_rule) | resource |
+| [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/data-sources/availability_zones) | data source |
+| [aws_route53_zone.root](https://registry.terraform.io/providers/hashicorp/aws/6.6.0/docs/data-sources/route53_zone) | data source |
+| [http_http.my_ip](https://registry.terraform.io/providers/hashicorp/http/3.5.0/docs/data-sources/http) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [route53\_public\_hosted\_zone\_name](#input\_route53\_public\_hosted\_zone\_name) | The fully qualified domain name of your existing Route53 Hosted Zone (e.g., 'example.com'). | `string` | n/a | yes |
+| [unity\_license\_server\_file\_path](#input\_unity\_license\_server\_file\_path) | Local path to the Linux version of the Unity Floating License Server zip file. Download from Unity ID portal at https://id.unity.com/. Set to null to skip Unity License Server deployment. | `string` | `null` | no |
+| [unity\_teamcity\_agent\_image](#input\_unity\_teamcity\_agent\_image) | Container image URI for Unity TeamCity build agents. Must include Unity Hub and Unity Editor. Build your own using the Dockerfile in docker/teamcity-unity-build-agent/, or set to null to skip Unity agent deployment. | `string` | `null` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [ecs\_cluster\_name](#output\_ecs\_cluster\_name) | The name of the shared ECS cluster |
+| [p4\_server\_connection\_string](#output\_p4\_server\_connection\_string) | The connection string for the P4 Server. Set your P4PORT environment variable to this value. |
+| [p4\_swarm\_url](#output\_p4\_swarm\_url) | The URL for the P4 Swarm (Code Review) service. |
+| [perforce\_super\_user\_password\_secret\_arn](#output\_perforce\_super\_user\_password\_secret\_arn) | ARN of the secret containing Perforce super user password |
+| [perforce\_super\_user\_username\_secret\_arn](#output\_perforce\_super\_user\_username\_secret\_arn) | ARN of the secret containing Perforce super user username |
+| [teamcity\_url](#output\_teamcity\_url) | The URL for the TeamCity server. |
+| [unity\_accelerator\_dashboard\_password\_secret\_arn](#output\_unity\_accelerator\_dashboard\_password\_secret\_arn) | ARN of the secret containing Unity Accelerator dashboard password |
+| [unity\_accelerator\_dashboard\_username\_secret\_arn](#output\_unity\_accelerator\_dashboard\_username\_secret\_arn) | ARN of the secret containing Unity Accelerator dashboard username |
+| [unity\_accelerator\_url](#output\_unity\_accelerator\_url) | The URL for the Unity Accelerator dashboard. |
+| [unity\_license\_server\_dashboard\_password\_secret\_arn](#output\_unity\_license\_server\_dashboard\_password\_secret\_arn) | ARN of the secret containing Unity License Server dashboard password (if deployed) |
+| [unity\_license\_server\_registration\_request\_url](#output\_unity\_license\_server\_registration\_request\_url) | Presigned URL for downloading the server-registration-request.xml file (valid for 1 hour, if deployed) |
+| [unity\_license\_server\_services\_config\_url](#output\_unity\_license\_server\_services\_config\_url) | Presigned URL for downloading the services-config.json file (valid for 1 hour, if deployed) |
+| [unity\_license\_server\_url](#output\_unity\_license\_server\_url) | The URL for the Unity License Server dashboard (if deployed). |
+
diff --git a/samples/unity-build-pipeline/dns.tf b/samples/unity-build-pipeline/dns.tf
new file mode 100644
index 00000000..d348642c
--- /dev/null
+++ b/samples/unity-build-pipeline/dns.tf
@@ -0,0 +1,121 @@
+##########################################
+# Route53 DNS Records
+##########################################
+
+# Public route53 hosted zone for external DNS resolution
+data "aws_route53_zone" "root" {
+ name = var.route53_public_hosted_zone_name
+ private_zone = false
+}
+
+# Public P4 Server Record
+resource "aws_route53_record" "p4_server_public" {
+ zone_id = data.aws_route53_zone.root.zone_id
+ name = local.perforce_fqdn
+ type = "A"
+ ttl = 300
+ #checkov:skip=CKV2_AWS_23:The attached resource is managed by CGD Toolkit
+ records = [module.perforce.p4_server_eip_public_ip]
+}
+
+# Public TeamCity Record
+resource "aws_route53_record" "teamcity_public" {
+ zone_id = data.aws_route53_zone.root.zone_id
+ name = local.teamcity_fqdn
+ type = "A"
+
+ alias {
+ name = module.teamcity.external_alb_dns_name
+ zone_id = module.teamcity.external_alb_zone_id
+ evaluate_target_health = true
+ }
+}
+
+# Public Unity Accelerator Record
+resource "aws_route53_record" "unity_accelerator_public" {
+ zone_id = data.aws_route53_zone.root.zone_id
+ name = local.unity_accelerator_fqdn
+ type = "A"
+
+ alias {
+ name = module.unity_accelerator.alb_dns_name
+ zone_id = module.unity_accelerator.alb_zone_id
+ evaluate_target_health = true
+ }
+}
+
+# Public P4 Swarm (Code Review) Record
+resource "aws_route53_record" "p4_swarm_public" {
+ zone_id = data.aws_route53_zone.root.zone_id
+ name = local.p4_swarm_fqdn
+ type = "A"
+
+ alias {
+ name = module.perforce.shared_application_load_balancer_dns_name
+ zone_id = module.perforce.shared_application_load_balancer_zone_id
+ evaluate_target_health = true
+ }
+}
+
+# Public Unity License Server Record
+resource "aws_route53_record" "unity_license_server_public" {
+ count = var.unity_license_server_file_path != null ? 1 : 0
+ zone_id = data.aws_route53_zone.root.zone_id
+ name = local.unity_license_fqdn
+ type = "A"
+
+ alias {
+ name = module.unity_license_server[0].alb_dns_name
+ zone_id = module.unity_license_server[0].alb_zone_id
+ evaluate_target_health = true
+ }
+}
+
+##########################################
+# Certificate Management
+##########################################
+
+resource "aws_acm_certificate" "shared" {
+ domain_name = var.route53_public_hosted_zone_name
+ subject_alternative_names = [
+ local.perforce_fqdn,
+ local.p4_swarm_fqdn,
+ local.teamcity_fqdn,
+ local.unity_accelerator_fqdn,
+ local.unity_license_fqdn
+ ]
+ validation_method = "DNS"
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-certificate"
+ })
+
+ lifecycle {
+ create_before_destroy = true
+ }
+}
+
+resource "aws_route53_record" "certificate_validation" {
+ for_each = {
+ for dvo in aws_acm_certificate.shared.domain_validation_options : dvo.domain_name => {
+ name = dvo.resource_record_name
+ record = dvo.resource_record_value
+ type = dvo.resource_record_type
+ }
+ }
+
+ allow_overwrite = true
+ name = each.value.name
+ records = [each.value.record]
+ ttl = 60
+ type = each.value.type
+ zone_id = data.aws_route53_zone.root.id
+}
+
+resource "aws_acm_certificate_validation" "shared" {
+ timeouts {
+ create = "15m"
+ }
+ certificate_arn = aws_acm_certificate.shared.arn
+ validation_record_fqdns = [for record in aws_route53_record.certificate_validation : record.fqdn]
+}
diff --git a/samples/unity-build-pipeline/docker/README.md b/samples/unity-build-pipeline/docker/README.md
new file mode 100644
index 00000000..5092ad86
--- /dev/null
+++ b/samples/unity-build-pipeline/docker/README.md
@@ -0,0 +1,271 @@
+# Unity + TeamCity Build Agent Docker Image
+
+This directory contains a Docker image that combines Unity Editor with TeamCity Build Agent for automated Unity builds.
+
+The image uses official Unity Hub and Unity Editor installations.
+
+## What's Included
+
+- **Unity Hub** (latest stable version from official repository)
+- **Unity Editor** (optional - specify version at build time or install at runtime)
+- **TeamCity Build Agent** runtime
+- **Perforce P4 CLI** for source control integration
+- **Git + Git LFS** for additional VCS support
+- **AWS CLI v2** for artifact management to S3
+- **Java 17** for TeamCity agent runtime
+
+All components are installed from official sources using standard package managers and installers.
+
+> **Note:** Unity Editor installation is optional. You can either install a specific version at build time or install it at runtime using Unity Hub. The build script includes example values for Unity 6 LTS.
+
+## Building the Image
+
+### Prerequisites
+
+- Docker Desktop or Docker Engine
+- AWS CLI v2 configured with credentials
+- AWS account with ECR access
+
+### Finding Unity Version and Changeset
+
+Unity Editor installation requires both a **version** and **changeset**. To find these:
+
+1. Visit [Unity Download Archive](https://unity.com/releases/editor/archive)
+2. Click on the version you want (e.g., "6000.0.23f1")
+3. Look at the URL or release notes page - it contains the changeset
+
+**Example:** For Unity 6000.0.23f1
+- URL: `https://unity.com/releases/editor/whats-new/6000.0.23f1#bd20d88e54b8`
+- Version: `6000.0.23f1`
+- Changeset: `bd20d88e54b8` (found in the URL after the `#`)
+
+**Tip:** You can also leave version and changeset empty to build an image with only Unity Hub, then install specific editor versions at runtime.
+
+### Manual Build and Push
+
+Follow these steps to build and push the Docker image to ECR using Unity 6 LTS (default). Adjust version numbers as needed for different Unity versions.
+
+**Step 1: Create ECR repository (if it doesn't exist)**
+
+```bash
+aws ecr describe-repositories --repository-names unity-teamcity-agent --region us-east-1 2>/dev/null || \
+ aws ecr create-repository --repository-name unity-teamcity-agent --region us-east-1
+```
+
+**Step 2: Log in to ECR**
+
+```bash
+aws ecr get-login-password --region us-east-1 | \
+ docker login --username AWS --password-stdin $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com
+```
+
+**Step 3: Build the Docker image**
+
+```bash
+cd teamcity-unity-build-agent/
+
+# With Unity Editor (example: Unity 6 LTS)
+docker build \
+ --build-arg UNITY_VERSION=6000.0.23f1 \
+ --build-arg UNITY_CHANGESET=bd20d88e54b8 \
+ -t unity-teamcity-agent:latest \
+ .
+
+# OR: Hub only (no editor pre-installed)
+docker build \
+ -t unity-teamcity-agent:latest \
+ .
+```
+
+This will take 15-30 minutes if installing Unity Editor, or ~5 minutes for Hub-only build, depending on your internet connection and system performance.
+
+**Step 4: Tag and push to ECR**
+
+```bash
+docker tag unity-teamcity-agent:latest \
+ $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest
+
+docker push $(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest
+```
+
+**Step 5: Get your image URI for Terraform**
+
+```bash
+echo "$(aws sts get-caller-identity --query Account --output text).dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest"
+```
+
+Copy this URI to use in your `terraform.tfvars` file.
+
+### Building Different Unity Versions
+
+To build with a different Unity version, adjust the build args:
+
+```bash
+# Unity 2022 LTS
+docker build \
+ --build-arg UNITY_VERSION=2022.3.50f1 \
+ --build-arg UNITY_CHANGESET=cc9fd8c8b302 \
+ -t unity-teamcity-agent:unity-2022-lts \
+ .
+
+# Unity 2023 LTS
+docker build \
+ --build-arg UNITY_VERSION=2023.2.20f1 \
+ --build-arg UNITY_CHANGESET=b00aa8a6c14f \
+ -t unity-teamcity-agent:unity-2023-lts \
+ .
+```
+
+Find more versions and their changesets at: https://unity.com/releases/editor/archive
+
+### Using the Build Script (Recommended)
+
+For convenience, a `build-and-push.sh` script is provided that automates all the above steps:
+
+```bash
+cd teamcity-unity-build-agent/
+./build-and-push.sh
+```
+
+The script includes example values (Unity 6 LTS) that work out of the box. You can customize the build with environment variables:
+
+```bash
+# Build with Unity 2022 LTS
+UNITY_VERSION=2022.3.50f1 \
+UNITY_CHANGESET=cc9fd8c8b302 \
+IMAGE_TAG=unity-2022-lts \
+./build-and-push.sh
+
+# Build Hub-only (no editor)
+UNITY_VERSION="" \
+UNITY_CHANGESET="" \
+./build-and-push.sh
+```
+
+**Available environment variables:**
+- `UNITY_VERSION` - Unity Editor version (e.g., `6000.0.23f1`) or empty for Hub-only
+- `UNITY_CHANGESET` - Unity changeset hash (e.g., `bd20d88e54b8`) or empty for Hub-only
+- `IMAGE_TAG` - Docker image tag (default: `latest`)
+- `ECR_REPOSITORY_NAME` - ECR repository name (default: `unity-teamcity-agent`)
+- `AWS_REGION` - AWS region (default: `us-east-1`)
+
+## Using the Image in Terraform
+
+After building and pushing your image, update `../../main.tf` with your ECR image URI:
+
+```hcl
+module "teamcity" {
+ source = "../../modules/teamcity"
+
+ # ... other configuration ...
+
+ build_farm_config = {
+ "unity-builder" = {
+ image = ".dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest"
+ cpu = 4096 # 4 vCPU recommended for Unity builds
+ memory = 8192 # 8 GB RAM recommended
+ desired_count = 2
+ environment = {
+ UNITY_LICENSE_SERVER_URL = "http://:8080"
+ }
+ }
+ }
+}
+```
+
+Then apply with Terraform:
+
+```bash
+cd ../../ # Back to unity-build-pipeline root
+terraform apply
+```
+
+## TeamCity Agent Behavior
+
+The agents automatically:
+1. Download TeamCity agent binaries from your TeamCity server on first startup
+2. Register with the TeamCity server
+3. Start accepting build jobs
+
+Configuration is handled via environment variables in the ECS task definition.
+
+## Testing Locally
+
+To test the image locally before deploying:
+
+```bash
+# Pull your image from ECR
+aws ecr get-login-password --region us-east-1 | \
+ docker login --username AWS --password-stdin .dkr.ecr.us-east-1.amazonaws.com
+
+docker pull .dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest
+
+# Run locally (requires TeamCity server URL)
+docker run -e SERVER_URL=https://teamcity.yourdomain.com \
+ -e AGENT_NAME=local-test \
+ .dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest
+```
+
+## Advanced Customization
+
+### Adding Additional Unity Modules
+
+Edit `teamcity-unity-build-agent/Dockerfile` to add more modules to the Unity Editor installation:
+
+```dockerfile
+RUN xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
+ unityhub --headless install \
+ --version ${UNITY_VERSION} \
+ --changeset ${UNITY_CHANGESET} \
+ --module linux-il2cpp \
+ --module android \
+ --module webgl \
+ --module ios
+```
+
+Available modules: `linux-il2cpp`, `windows-mono`, `mac-mono`, `android`, `ios`, `webgl`, `linux-server`
+
+## Troubleshooting
+
+### Build fails during Unity installation
+
+**Issue:** Unity Hub installation fails or Unity Editor download times out
+
+**Solution:**
+- Check your internet connection
+- Verify the Unity version and changeset are correct
+- Try a different Unity version (some versions may have download issues)
+- Check Unity Download Archive for version availability
+
+### ECR push fails
+
+**Issue:** Cannot push image to ECR
+
+**Solution:**
+- Verify AWS credentials: `aws sts get-caller-identity`
+- Check ECR permissions in your IAM policy
+- Ensure you're logged into ECR: `aws ecr get-login-password | docker login ...`
+
+### Unity license activation issues
+
+**Issue:** Unity requires activation in container
+
+**Solution:**
+- Unity builds use the Unity License Server for license management
+- Ensure `UNITY_LICENSE_SERVER_URL` environment variable is set in TeamCity agent config
+- Verify the license server is accessible from agent containers
+- Check license server logs for connection issues
+
+### Image size concerns
+
+**Note:** The image is large (~10-15GB) due to Unity Editor installation. This is expected and normal for Unity containerized builds.
+
+## Alternative Approaches
+
+If build time is a critical concern, consider the community-maintained [GameCI project](https://game.ci/) which provides pre-built Unity Docker images. Note that you would still need to add TeamCity agent integration on top of the GameCI Unity images.
+
+## Support
+
+- **Unity Hub Documentation**: https://docs.unity3d.com/hub/manual/index.html
+- **TeamCity Agent Documentation**: https://www.jetbrains.com/help/teamcity/build-agent.html
+- **CGD Toolkit Issues**: https://github.com/aws-games/cloud-game-development-toolkit/issues
diff --git a/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/Dockerfile b/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/Dockerfile
new file mode 100644
index 00000000..a0e7d312
--- /dev/null
+++ b/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/Dockerfile
@@ -0,0 +1,149 @@
+# Unity + TeamCity Build Agent
+# Uses official Unity Hub and Unity Editor installations
+# Incorporates best practices from GameCI for Unity containerization
+
+FROM ubuntu:22.04
+
+# Prevent interactive prompts during package installation
+ENV DEBIAN_FRONTEND=noninteractive
+
+# Set locale for proper character encoding
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+
+# Define Unity installation path
+ENV UNITY_PATH=/opt/unity
+
+# Install system dependencies required by Unity and TeamCity
+RUN apt-get update && apt-get install -y \
+ # Core Unity runtime dependencies (from GameCI)
+ libasound2 \
+ libcap2 \
+ libgconf-2-4 \
+ libglu1 \
+ libgtk-3-0 \
+ libncurses5 \
+ libnotify4 \
+ libnss3 \
+ libxtst6 \
+ libxss1 \
+ # X11 support for headless operation
+ libx11-6 \
+ libxcursor1 \
+ libxinerama1 \
+ libxrandr2 \
+ xvfb \
+ # System utilities
+ ca-certificates \
+ cpio \
+ lsb-release \
+ xz-utils \
+ # Unity Hub dependencies
+ wget \
+ libgbm1 \
+ # TeamCity and build tools
+ openjdk-17-jdk \
+ curl \
+ git \
+ git-lfs \
+ unzip \
+ zip \
+ jq \
+ openssh-client \
+ # Utilities
+ gnupg \
+ && rm -rf /var/lib/apt/lists/*
+
+# Git LFS system-wide installation
+RUN git lfs install --system
+
+# Disable sound card (headless environment)
+RUN echo "pcm.!default { type plug slave.pcm \"null\" }" > /etc/asound.conf
+
+# Create machine-id for Unity activation
+RUN rm -f /etc/machine-id && \
+ dbus-uuidgen > /etc/machine-id && \
+ chmod 444 /etc/machine-id
+
+# Set Java environment variables for TeamCity
+ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64
+ENV PATH="$JAVA_HOME/bin:$PATH"
+
+# Install Unity Hub via official APT repository
+RUN wget -qO - https://hub.unity3d.com/linux/keys/public | gpg --dearmor | tee /usr/share/keyrings/Unity_Technologies_ApS.gpg > /dev/null && \
+ sh -c 'echo "deb [signed-by=/usr/share/keyrings/Unity_Technologies_ApS.gpg] https://hub.unity3d.com/linux/repos/deb stable main" > /etc/apt/sources.list.d/unityhub.list' && \
+ apt-get update && \
+ apt-get install -y unityhub && \
+ rm -rf /var/lib/apt/lists/*
+
+# Install Unity Editor via Unity Hub (optional)
+#
+# Unity Editor installation requires both version and changeset to be specified.
+# To find available versions and their changesets, visit:
+# - Unity Archive: https://unity.com/releases/editor/archive
+# - Unity Release Notes: https://unity.com/releases/editor/whats-new
+# - Click on any version, the URL will contain the changeset (e.g., /6000.0.23f1/bd20d88e54b8)
+#
+# Example: For Unity 6000.0.23f1:
+# Version: 6000.0.23f1
+# Changeset: bd20d88e54b8 (found in the URL)
+#
+# Usage:
+# docker build --build-arg UNITY_VERSION=6000.0.23f1 --build-arg UNITY_CHANGESET=bd20d88e54b8 .
+#
+# If no version is specified, only Unity Hub is installed and you can install
+# Unity Editor at runtime using: unityhub --headless install --version --changeset
+ARG UNITY_VERSION=""
+ARG UNITY_CHANGESET=""
+
+# Install Unity Editor with Linux build support if version is specified
+# Using xvfb-run to provide virtual display for headless installation
+RUN if [ -n "${UNITY_VERSION}" ] && [ -n "${UNITY_CHANGESET}" ]; then \
+ echo "Installing Unity Editor ${UNITY_VERSION} with changeset ${UNITY_CHANGESET}..."; \
+ xvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \
+ unityhub --headless install \
+ --version ${UNITY_VERSION} \
+ --changeset ${UNITY_CHANGESET} \
+ --module linux-il2cpp; \
+ else \
+ echo "No Unity Editor version specified - only Unity Hub is installed."; \
+ echo "To install an editor at build time, pass:"; \
+ echo " --build-arg UNITY_VERSION= --build-arg UNITY_CHANGESET="; \
+ echo "Find versions at: https://unity.com/releases/editor/archive"; \
+ fi
+
+# Install AWS CLI v2 for artifact management
+RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \
+ unzip awscliv2.zip && \
+ ./aws/install && \
+ rm -rf aws awscliv2.zip
+
+# Install Perforce P4 CLI for source control
+RUN wget -q https://filehost.perforce.com/perforce/r25.1/bin.linux26x86_64/p4 \
+ -O /usr/local/bin/p4 && \
+ chmod +x /usr/local/bin/p4
+
+# TeamCity agent configuration
+ENV TEAMCITY_SERVER_URL=""
+ENV AGENT_NAME="unity-builder"
+ENV AGENT_WORK_DIR="/opt/buildAgent/work"
+
+# Create buildagent user and directories
+RUN useradd -m -d /opt/buildAgent -s /bin/bash buildagent && \
+ mkdir -p /opt/buildAgent/work /opt/buildAgent/temp /opt/buildAgent/tools /opt/buildAgent/plugins && \
+ chown -R buildagent:buildagent /opt/buildAgent
+
+# Copy startup script
+COPY teamcity-agent-startup.sh /opt/buildAgent/
+RUN chmod +x /opt/buildAgent/teamcity-agent-startup.sh && \
+ chown buildagent:buildagent /opt/buildAgent/teamcity-agent-startup.sh
+
+# Switch to buildagent user for running the agent
+USER buildagent
+WORKDIR /opt/buildAgent
+
+# Expose TeamCity agent port (optional - agents typically connect outbound)
+EXPOSE 9090
+
+# Start TeamCity agent
+ENTRYPOINT ["/opt/buildAgent/teamcity-agent-startup.sh"]
diff --git a/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/build-and-push.sh b/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/build-and-push.sh
new file mode 100755
index 00000000..903d5753
--- /dev/null
+++ b/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/build-and-push.sh
@@ -0,0 +1,76 @@
+#!/bin/bash
+set -e
+
+# Configuration
+AWS_REGION="${AWS_REGION:-us-east-1}"
+AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
+ECR_REPOSITORY_NAME="${ECR_REPOSITORY_NAME:-unity-teamcity-agent}"
+IMAGE_TAG="${IMAGE_TAG:-latest}"
+
+# Unity Editor version configuration (example values - update as needed)
+# Find versions and changesets at: https://unity.com/releases/editor/archive
+# Click on a version, the URL will contain the changeset (e.g., /6000.0.23f1/bd20d88e54b8)
+#
+# Set to empty string to skip Unity Editor installation (only Unity Hub will be installed)
+# Example: UNITY_VERSION="" UNITY_CHANGESET="" ./build-and-push.sh
+UNITY_VERSION="${UNITY_VERSION:-6000.0.23f1}"
+UNITY_CHANGESET="${UNITY_CHANGESET:-bd20d88e54b8}"
+
+# Full image name
+ECR_REGISTRY="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
+FULL_IMAGE_NAME="${ECR_REGISTRY}/${ECR_REPOSITORY_NAME}:${IMAGE_TAG}"
+
+echo "Building Unity + TeamCity agent image..."
+echo "Registry: ${ECR_REGISTRY}"
+echo "Repository: ${ECR_REPOSITORY_NAME}"
+echo "Image Tag: ${IMAGE_TAG}"
+echo ""
+echo "Unity Hub: Latest stable (from official repository)"
+if [ -n "${UNITY_VERSION}" ] && [ -n "${UNITY_CHANGESET}" ]; then
+ echo "Unity Editor: ${UNITY_VERSION} (changeset: ${UNITY_CHANGESET})"
+else
+ echo "Unity Editor: Not installed (Hub only - install editors at runtime)"
+fi
+echo ""
+
+# Check if ECR repository exists, create if it doesn't
+if ! aws ecr describe-repositories --repository-names "${ECR_REPOSITORY_NAME}" --region "${AWS_REGION}" >/dev/null 2>&1; then
+ echo "Creating ECR repository: ${ECR_REPOSITORY_NAME}"
+ aws ecr create-repository \
+ --repository-name "${ECR_REPOSITORY_NAME}" \
+ --region "${AWS_REGION}" \
+ --image-scanning-configuration scanOnPush=true \
+ --encryption-configuration encryptionType=AES256
+ echo ""
+fi
+
+# Login to ECR
+echo "Logging in to Amazon ECR..."
+aws ecr get-login-password --region "${AWS_REGION}" | \
+ docker login --username AWS --password-stdin "${ECR_REGISTRY}"
+echo ""
+
+# Build the Docker image for AMD64 platform (Fargate runs on x86_64)
+echo "Building Docker image for linux/amd64 platform..."
+echo "NOTE: This build takes 15-30 minutes due to Unity Editor installation"
+docker build --platform linux/amd64 \
+ --build-arg UNITY_VERSION="${UNITY_VERSION}" \
+ --build-arg UNITY_CHANGESET="${UNITY_CHANGESET}" \
+ -t "${ECR_REPOSITORY_NAME}:${IMAGE_TAG}" .
+echo ""
+
+# Tag for ECR
+echo "Tagging image for ECR..."
+docker tag "${ECR_REPOSITORY_NAME}:${IMAGE_TAG}" "${FULL_IMAGE_NAME}"
+echo ""
+
+# Push to ECR
+echo "Pushing image to ECR..."
+docker push "${FULL_IMAGE_NAME}"
+echo ""
+
+echo "✅ Successfully built and pushed image:"
+echo " ${FULL_IMAGE_NAME}"
+echo ""
+echo "To use this image, update your main.tf with:"
+echo " image = \"${FULL_IMAGE_NAME}\""
diff --git a/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/teamcity-agent-startup.sh b/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/teamcity-agent-startup.sh
new file mode 100644
index 00000000..faa2f1f5
--- /dev/null
+++ b/samples/unity-build-pipeline/docker/teamcity-unity-build-agent/teamcity-agent-startup.sh
@@ -0,0 +1,37 @@
+#!/bin/bash
+set -e
+
+echo "Starting TeamCity Build Agent..."
+
+# Check if SERVER_URL is set
+if [ -z "$SERVER_URL" ]; then
+ echo "ERROR: SERVER_URL environment variable is not set"
+ exit 1
+fi
+
+# Download TeamCity agent if not already present
+if [ ! -f /opt/buildAgent/bin/agent.sh ]; then
+ echo "Downloading TeamCity agent from $SERVER_URL..."
+
+ # Download buildAgent.zip from TeamCity server
+ wget -q -O /tmp/buildAgent.zip "${SERVER_URL}/update/buildAgent.zip"
+
+ # Extract to buildAgent directory
+ unzip -q /tmp/buildAgent.zip -d /opt/buildAgent
+ rm /tmp/buildAgent.zip
+
+ echo "TeamCity agent downloaded and extracted"
+fi
+
+# Configure agent name if provided
+if [ -n "$AGENT_NAME" ]; then
+ echo "Setting agent name to: $AGENT_NAME"
+ echo "name=$AGENT_NAME" > /opt/buildAgent/conf/buildAgent.properties
+fi
+
+# Set server URL in buildAgent.properties
+echo "serverUrl=$SERVER_URL" >> /opt/buildAgent/conf/buildAgent.properties
+
+# Start the agent
+echo "Starting TeamCity agent..."
+exec /opt/buildAgent/bin/agent.sh run
diff --git a/samples/unity-build-pipeline/locals.tf b/samples/unity-build-pipeline/locals.tf
new file mode 100644
index 00000000..7eb84e3e
--- /dev/null
+++ b/samples/unity-build-pipeline/locals.tf
@@ -0,0 +1,52 @@
+##################################################
+# Data Sources
+##################################################
+
+data "aws_availability_zones" "available" {}
+
+data "http" "my_ip" {
+ url = "https://checkip.amazonaws.com"
+}
+
+##################################################
+# Local Variables
+##################################################
+
+locals {
+ # Project Configuration
+ project_prefix = "ubp"
+ azs = slice(data.aws_availability_zones.available.names, 0, 2)
+ my_ip = chomp(data.http.my_ip.response_body)
+
+ # Subdomain Configuration
+ # All services will be accessible at: .
+ perforce_subdomain = "perforce"
+ p4_swarm_subdomain = "swarm"
+ teamcity_subdomain = "teamcity"
+ unity_accelerator_subdomain = "unity-accelerator"
+ unity_license_subdomain = "unity-license"
+
+ # Fully Qualified Domain Names
+ perforce_fqdn = "${local.perforce_subdomain}.${var.route53_public_hosted_zone_name}"
+ p4_swarm_fqdn = "${local.p4_swarm_subdomain}.${var.route53_public_hosted_zone_name}"
+ teamcity_fqdn = "${local.teamcity_subdomain}.${var.route53_public_hosted_zone_name}"
+ unity_accelerator_fqdn = "${local.unity_accelerator_subdomain}.${var.route53_public_hosted_zone_name}"
+ unity_license_fqdn = "${local.unity_license_subdomain}.${var.route53_public_hosted_zone_name}"
+
+ ##################################################
+ # VPC & Networking Configuration
+ ##################################################
+
+ vpc_cidr_block = "10.0.0.0/16"
+ public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
+ private_subnet_cidrs = ["10.0.3.0/24", "10.0.4.0/24"]
+
+ ##################################################
+ # Tags
+ ##################################################
+
+ tags = {
+ Project = "unity-build-pipeline"
+ ManagedBy = "terraform"
+ }
+}
diff --git a/samples/unity-build-pipeline/main.tf b/samples/unity-build-pipeline/main.tf
new file mode 100644
index 00000000..3d0e1900
--- /dev/null
+++ b/samples/unity-build-pipeline/main.tf
@@ -0,0 +1,162 @@
+##########################################
+# Shared ECS Cluster for Services
+##########################################
+
+resource "aws_ecs_cluster" "unity_pipeline_cluster" {
+ name = "${local.project_prefix}-cluster"
+
+ setting {
+ name = "containerInsights"
+ value = "enabled"
+ }
+
+ tags = local.tags
+}
+
+resource "aws_ecs_cluster_capacity_providers" "providers" {
+ cluster_name = aws_ecs_cluster.unity_pipeline_cluster.name
+
+ capacity_providers = ["FARGATE"]
+
+ default_capacity_provider_strategy {
+ base = 1
+ weight = 100
+ capacity_provider = "FARGATE"
+ }
+}
+
+##########################################
+# Perforce
+##########################################
+
+module "perforce" {
+ source = "../../modules/perforce"
+
+ # - Shared -
+ project_prefix = local.project_prefix
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+
+ create_route53_private_hosted_zone = true
+ route53_private_hosted_zone_name = local.perforce_fqdn
+ create_shared_application_load_balancer = true
+ shared_alb_subnets = aws_subnet.public_subnets[*].id
+ create_shared_network_load_balancer = true
+ shared_nlb_subnets = aws_subnet.public_subnets[*].id
+ existing_ecs_cluster_name = aws_ecs_cluster.unity_pipeline_cluster.name
+ certificate_arn = aws_acm_certificate.shared.arn
+
+ # - P4 Server Configuration -
+ p4_server_config = {
+ # General
+ name = "p4-server"
+ fully_qualified_domain_name = local.perforce_fqdn
+
+ # Compute
+ p4_server_type = "p4d_commit"
+
+ # Storage
+ depot_volume_size = 128
+ metadata_volume_size = 32
+ logs_volume_size = 32
+
+ # Networking & Security
+ instance_subnet_id = aws_subnet.public_subnets[0].id
+ existing_security_groups = [aws_security_group.allow_my_ip.id]
+ }
+
+ # - P4 Code Review (Swarm) Configuration -
+ p4_code_review_config = {
+ # General
+ name = "p4-code-review"
+ fully_qualified_domain_name = local.p4_swarm_fqdn
+ service_subnets = aws_subnet.private_subnets[*].id
+ }
+
+ depends_on = [aws_acm_certificate_validation.shared]
+
+ tags = local.tags
+}
+
+##########################################
+# TeamCity
+##########################################
+
+module "teamcity" {
+ source = "../../modules/teamcity"
+
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+ service_subnets = aws_subnet.private_subnets[*].id
+ alb_subnets = aws_subnet.public_subnets[*].id
+ alb_certificate_arn = aws_acm_certificate.shared.arn
+
+ cluster_name = aws_ecs_cluster.unity_pipeline_cluster.name
+ environment = "dev"
+
+ build_farm_config = var.unity_teamcity_agent_image != null ? {
+ "unity-builder" = {
+ image = var.unity_teamcity_agent_image
+ cpu = 4096 # 4 vCPU recommended for Unity builds
+ memory = 8192 # 8 GB RAM recommended for Unity builds
+ desired_count = 2
+ environment = var.unity_license_server_file_path != null ? {
+ UNITY_LICENSE_SERVER_URL = "http://${module.unity_license_server[0].instance_private_ip}:${module.unity_license_server[0].unity_license_server_port}"
+ } : {}
+ }
+ } : {}
+
+ depends_on = [aws_acm_certificate_validation.shared]
+
+ tags = local.tags
+}
+
+##########################################
+# Unity Accelerator
+##########################################
+
+module "unity_accelerator" {
+ source = "../../modules/unity/accelerator"
+
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+ service_subnets = aws_subnet.private_subnets[*].id
+ lb_subnets = aws_subnet.public_subnets[*].id
+
+ cluster_name = aws_ecs_cluster.unity_pipeline_cluster.name
+ alb_certificate_arn = aws_acm_certificate.shared.arn
+ environment = "dev"
+
+ depends_on = [aws_acm_certificate_validation.shared]
+
+ tags = local.tags
+}
+
+##########################################
+# Unity Floating License Server
+##########################################
+
+module "unity_license_server" {
+ count = var.unity_license_server_file_path != null ? 1 : 0
+ source = "../../modules/unity/floating-license-server"
+
+ name = "unity-license-server"
+ unity_license_server_file_path = var.unity_license_server_file_path
+
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+ vpc_subnet = aws_subnet.private_subnets[0].id
+
+ # Deploy ALB for dashboard access
+ create_alb = true
+ alb_is_internal = false
+ alb_subnets = aws_subnet.public_subnets[*].id
+ alb_certificate_arn = aws_acm_certificate.shared.arn
+ enable_alb_deletion_protection = false
+
+ # Don't add public IP to the ENI since we're using ALB
+ add_eni_public_ip = false
+
+ depends_on = [
+ aws_acm_certificate_validation.shared,
+ aws_nat_gateway.nat_gateway
+ ]
+
+ tags = local.tags
+}
diff --git a/samples/unity-build-pipeline/outputs.tf b/samples/unity-build-pipeline/outputs.tf
new file mode 100644
index 00000000..8b2c6f27
--- /dev/null
+++ b/samples/unity-build-pipeline/outputs.tf
@@ -0,0 +1,84 @@
+##########################################
+# ECS Cluster Outputs
+##########################################
+
+output "ecs_cluster_name" {
+ description = "The name of the shared ECS cluster"
+ value = aws_ecs_cluster.unity_pipeline_cluster.name
+}
+
+##########################################
+# Perforce Outputs
+##########################################
+
+output "p4_server_connection_string" {
+ description = "The connection string for the P4 Server. Set your P4PORT environment variable to this value."
+ value = "ssl:${local.perforce_fqdn}:1666"
+}
+
+output "p4_swarm_url" {
+ description = "The URL for the P4 Swarm (Code Review) service."
+ value = "https://${local.p4_swarm_fqdn}"
+}
+
+output "perforce_super_user_password_secret_arn" {
+ description = "ARN of the secret containing Perforce super user password"
+ value = module.perforce.p4_server_super_user_password_secret_arn
+}
+
+output "perforce_super_user_username_secret_arn" {
+ description = "ARN of the secret containing Perforce super user username"
+ value = module.perforce.p4_server_super_user_username_secret_arn
+}
+
+##########################################
+# TeamCity Outputs
+##########################################
+
+output "teamcity_url" {
+ description = "The URL for the TeamCity server."
+ value = "https://${local.teamcity_fqdn}"
+}
+
+##########################################
+# Unity Accelerator Outputs
+##########################################
+
+output "unity_accelerator_url" {
+ description = "The URL for the Unity Accelerator dashboard."
+ value = "https://${local.unity_accelerator_fqdn}"
+}
+
+output "unity_accelerator_dashboard_username_secret_arn" {
+ description = "ARN of the secret containing Unity Accelerator dashboard username"
+ value = module.unity_accelerator.unity_accelerator_dashboard_username_arn
+}
+
+output "unity_accelerator_dashboard_password_secret_arn" {
+ description = "ARN of the secret containing Unity Accelerator dashboard password"
+ value = module.unity_accelerator.unity_accelerator_dashboard_password_arn
+}
+
+##########################################
+# Unity License Server Outputs
+##########################################
+
+output "unity_license_server_url" {
+ description = "The URL for the Unity License Server dashboard (if deployed)."
+ value = var.unity_license_server_file_path != null ? "https://${local.unity_license_fqdn}" : "Not deployed - set unity_license_server_file_path variable to deploy"
+}
+
+output "unity_license_server_dashboard_password_secret_arn" {
+ description = "ARN of the secret containing Unity License Server dashboard password (if deployed)"
+ value = var.unity_license_server_file_path != null ? module.unity_license_server[0].dashboard_password_secret_arn : null
+}
+
+output "unity_license_server_services_config_url" {
+ description = "Presigned URL for downloading the services-config.json file (valid for 1 hour, if deployed)"
+ value = var.unity_license_server_file_path != null ? module.unity_license_server[0].services_config_presigned_url : null
+}
+
+output "unity_license_server_registration_request_url" {
+ description = "Presigned URL for downloading the server-registration-request.xml file (valid for 1 hour, if deployed)"
+ value = var.unity_license_server_file_path != null ? module.unity_license_server[0].registration_request_presigned_url : null
+}
diff --git a/samples/unity-build-pipeline/providers.tf b/samples/unity-build-pipeline/providers.tf
new file mode 100644
index 00000000..9dbcdd8c
--- /dev/null
+++ b/samples/unity-build-pipeline/providers.tf
@@ -0,0 +1,16 @@
+##########################################
+# Providers
+##########################################
+
+# Placeholder provider required by Perforce module for FSxN support
+# Not used when storage_type = "EBS" (the default)
+provider "netapp-ontap" {
+ connection_profiles = [
+ {
+ name = "null"
+ hostname = "null"
+ username = "null"
+ password = "null"
+ }
+ ]
+}
diff --git a/samples/unity-build-pipeline/security.tf b/samples/unity-build-pipeline/security.tf
new file mode 100644
index 00000000..965f4207
--- /dev/null
+++ b/samples/unity-build-pipeline/security.tf
@@ -0,0 +1,81 @@
+##########################################
+# Security Groups
+##########################################
+
+# Security group for allowing access from user's IP
+resource "aws_security_group" "allow_my_ip" {
+ name = "${local.project_prefix}-allow-my-ip"
+ description = "Allow inbound traffic from my IP"
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-allow-my-ip"
+ })
+}
+
+# Allow HTTPS traffic from user's IP
+resource "aws_vpc_security_group_ingress_rule" "allow_https" {
+ security_group_id = aws_security_group.allow_my_ip.id
+ description = "Allow HTTPS traffic from personal IP"
+ from_port = 443
+ to_port = 443
+ ip_protocol = "tcp"
+ cidr_ipv4 = "${local.my_ip}/32"
+}
+
+# Allow Perforce traffic from user's IP
+resource "aws_vpc_security_group_ingress_rule" "allow_perforce" {
+ security_group_id = aws_security_group.allow_my_ip.id
+ description = "Allow Perforce traffic from personal IP"
+ from_port = 1666
+ to_port = 1666
+ ip_protocol = "tcp"
+ cidr_ipv4 = "${local.my_ip}/32"
+}
+
+# Allow Perforce traffic from VPC (includes TeamCity agents)
+resource "aws_vpc_security_group_ingress_rule" "perforce_from_vpc" {
+ security_group_id = aws_security_group.allow_my_ip.id
+ description = "Allow Perforce traffic from VPC (TeamCity agents)"
+ from_port = 1666
+ to_port = 1666
+ ip_protocol = "tcp"
+ cidr_ipv4 = local.vpc_cidr_block
+}
+
+##########################################
+# Unity License Server Security Rules
+##########################################
+
+# Allow HTTP traffic from user's IP to Unity License Server ALB (redirects to HTTPS)
+resource "aws_vpc_security_group_ingress_rule" "unity_license_server_http" {
+ count = var.unity_license_server_file_path != null ? 1 : 0
+ security_group_id = module.unity_license_server[0].alb_security_group_id
+ description = "Allow HTTP traffic from personal IP (redirects to HTTPS)"
+ from_port = 8080
+ to_port = 8080
+ ip_protocol = "tcp"
+ cidr_ipv4 = "${local.my_ip}/32"
+}
+
+# Allow HTTPS traffic from user's IP to Unity License Server ALB (dashboard access)
+resource "aws_vpc_security_group_ingress_rule" "unity_license_server_https" {
+ count = var.unity_license_server_file_path != null ? 1 : 0
+ security_group_id = module.unity_license_server[0].alb_security_group_id
+ description = "Allow HTTPS traffic from personal IP for dashboard access"
+ from_port = 443
+ to_port = 443
+ ip_protocol = "tcp"
+ cidr_ipv4 = "${local.my_ip}/32"
+}
+
+# Allow Unity License Server access from VPC (TeamCity agents and other services)
+resource "aws_vpc_security_group_ingress_rule" "unity_license_server_from_vpc" {
+ count = var.unity_license_server_file_path != null ? 1 : 0
+ security_group_id = module.unity_license_server[0].created_unity_license_server_security_group_id
+ description = "Allow Unity License Server access from VPC (build agents)"
+ from_port = 8080
+ to_port = 8080
+ ip_protocol = "tcp"
+ cidr_ipv4 = local.vpc_cidr_block
+}
diff --git a/samples/unity-build-pipeline/terraform.tfvars.example b/samples/unity-build-pipeline/terraform.tfvars.example
new file mode 100644
index 00000000..28e5ef4d
--- /dev/null
+++ b/samples/unity-build-pipeline/terraform.tfvars.example
@@ -0,0 +1,13 @@
+# Copy this file to terraform.tfvars and update with your values
+# cp terraform.tfvars.example terraform.tfvars
+
+# Required: Your Route53 hosted zone domain name
+route53_public_hosted_zone_name = "example.com"
+
+# Required: Path to Unity Floating License Server zip file
+# Download from https://id.unity.com/
+unity_license_server_file_path = "/path/to/Unity.Licensing.Server.linux-x64-v2.1.0.zip"
+
+# Required: Unity TeamCity agent Docker image URI
+# Build your own using docker/teamcity-unity-build-agent/ directory
+unity_teamcity_agent_image = "YOUR_ACCOUNT_ID.dkr.ecr.us-east-1.amazonaws.com/unity-teamcity-agent:latest"
diff --git a/samples/unity-build-pipeline/variables.tf b/samples/unity-build-pipeline/variables.tf
new file mode 100644
index 00000000..09699c93
--- /dev/null
+++ b/samples/unity-build-pipeline/variables.tf
@@ -0,0 +1,21 @@
+variable "route53_public_hosted_zone_name" {
+ type = string
+ description = "The fully qualified domain name of your existing Route53 Hosted Zone (e.g., 'example.com')."
+
+ validation {
+ condition = can(regex("^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z]{2,}$", var.route53_public_hosted_zone_name))
+ error_message = "Must be a valid domain name (e.g., example.com)"
+ }
+}
+
+variable "unity_license_server_file_path" {
+ type = string
+ description = "Local path to the Linux version of the Unity Floating License Server zip file. Download from Unity ID portal at https://id.unity.com/. Set to null to skip Unity License Server deployment."
+ default = null
+}
+
+variable "unity_teamcity_agent_image" {
+ type = string
+ description = "Container image URI for Unity TeamCity build agents. Must include Unity Hub and Unity Editor. Build your own using the Dockerfile in docker/teamcity-unity-build-agent/, or set to null to skip Unity agent deployment."
+ default = null
+}
diff --git a/samples/unity-build-pipeline/versions.tf b/samples/unity-build-pipeline/versions.tf
new file mode 100644
index 00000000..bc68f7b2
--- /dev/null
+++ b/samples/unity-build-pipeline/versions.tf
@@ -0,0 +1,18 @@
+terraform {
+ required_version = ">= 1.0"
+
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = "6.6.0"
+ }
+ http = {
+ source = "hashicorp/http"
+ version = "3.5.0"
+ }
+ netapp-ontap = {
+ source = "NetApp/netapp-ontap"
+ version = "2.3.0"
+ }
+ }
+}
diff --git a/samples/unity-build-pipeline/vpc.tf b/samples/unity-build-pipeline/vpc.tf
new file mode 100644
index 00000000..fc670862
--- /dev/null
+++ b/samples/unity-build-pipeline/vpc.tf
@@ -0,0 +1,142 @@
+##################################################
+# VPC
+##################################################
+
+resource "aws_vpc" "unity_pipeline_vpc" {
+ cidr_block = local.vpc_cidr_block
+ enable_dns_hostnames = true
+ enable_dns_support = true
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-vpc"
+ })
+
+ #checkov:skip=CKV2_AWS_11: VPC flow logging disabled by design for cost optimization
+}
+
+# Set default security group to restrict all traffic
+resource "aws_default_security_group" "default" {
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-default-sg"
+ })
+}
+
+##################################################
+# Subnets
+##################################################
+
+resource "aws_subnet" "public_subnets" {
+ count = length(local.public_subnet_cidrs)
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+ cidr_block = element(local.public_subnet_cidrs, count.index)
+ availability_zone = element(local.azs, count.index)
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-public-subnet-${count.index + 1}"
+ Tier = "public"
+ })
+}
+
+resource "aws_subnet" "private_subnets" {
+ count = length(local.private_subnet_cidrs)
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+ cidr_block = element(local.private_subnet_cidrs, count.index)
+ availability_zone = element(local.azs, count.index)
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-private-subnet-${count.index + 1}"
+ Tier = "private"
+ })
+}
+
+##################################################
+# Internet Gateway
+##################################################
+
+resource "aws_internet_gateway" "igw" {
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-igw"
+ })
+}
+
+##################################################
+# NAT Gateway
+##################################################
+
+resource "aws_eip" "nat_gateway_eip" {
+ domain = "vpc"
+ depends_on = [aws_internet_gateway.igw]
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-nat-eip"
+ })
+
+ #checkov:skip=CKV2_AWS_19: EIP associated with NAT Gateway through association ID
+}
+
+resource "aws_nat_gateway" "nat_gateway" {
+ allocation_id = aws_eip.nat_gateway_eip.id
+ subnet_id = aws_subnet.public_subnets[0].id
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-nat"
+ })
+
+ depends_on = [aws_internet_gateway.igw]
+}
+
+##################################################
+# Route Tables
+##################################################
+
+# Public Route Table
+resource "aws_route_table" "public_rt" {
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-public-rt"
+ Tier = "public"
+ })
+}
+
+# Public route to internet
+resource "aws_route" "public_internet_access" {
+ route_table_id = aws_route_table.public_rt.id
+ destination_cidr_block = "0.0.0.0/0"
+ gateway_id = aws_internet_gateway.igw.id
+}
+
+# Associate public subnets with public route table
+resource "aws_route_table_association" "public_rt_asso" {
+ count = length(aws_subnet.public_subnets)
+ route_table_id = aws_route_table.public_rt.id
+ subnet_id = aws_subnet.public_subnets[count.index].id
+}
+
+# Private Route Table
+resource "aws_route_table" "private_rt" {
+ vpc_id = aws_vpc.unity_pipeline_vpc.id
+
+ tags = merge(local.tags, {
+ Name = "${local.project_prefix}-private-rt"
+ Tier = "private"
+ })
+}
+
+# Private route to internet through NAT gateway
+resource "aws_route" "private_nat_access" {
+ route_table_id = aws_route_table.private_rt.id
+ destination_cidr_block = "0.0.0.0/0"
+ nat_gateway_id = aws_nat_gateway.nat_gateway.id
+}
+
+# Associate private subnets with private route table
+resource "aws_route_table_association" "private_rt_asso" {
+ count = length(aws_subnet.private_subnets)
+ route_table_id = aws_route_table.private_rt.id
+ subnet_id = aws_subnet.private_subnets[count.index].id
+}