From 996312e6f723acd089a322b1e8324a4b1f69f21b Mon Sep 17 00:00:00 2001 From: Gabriel Batista Date: Fri, 31 Oct 2025 16:49:25 -0400 Subject: [PATCH] feat(samples): add Unity build pipeline with TeamCity integration --- .gitignore | 3 + modules/perforce/README.md | 2 + .../modules/p4-code-review/elasticache.tf | 1 + modules/perforce/outputs.tf | 10 + modules/teamcity/local.tf | 6 +- samples/unity-build-pipeline/README.md | 1064 +++++++++++++++++ samples/unity-build-pipeline/dns.tf | 121 ++ samples/unity-build-pipeline/docker/README.md | 271 +++++ .../teamcity-unity-build-agent/Dockerfile | 149 +++ .../build-and-push.sh | 76 ++ .../teamcity-agent-startup.sh | 37 + samples/unity-build-pipeline/locals.tf | 52 + samples/unity-build-pipeline/main.tf | 162 +++ samples/unity-build-pipeline/outputs.tf | 84 ++ samples/unity-build-pipeline/providers.tf | 16 + samples/unity-build-pipeline/security.tf | 81 ++ .../terraform.tfvars.example | 13 + samples/unity-build-pipeline/variables.tf | 21 + samples/unity-build-pipeline/versions.tf | 18 + samples/unity-build-pipeline/vpc.tf | 142 +++ 20 files changed, 2326 insertions(+), 3 deletions(-) create mode 100644 samples/unity-build-pipeline/README.md create mode 100644 samples/unity-build-pipeline/dns.tf create mode 100644 samples/unity-build-pipeline/docker/README.md create mode 100644 samples/unity-build-pipeline/docker/teamcity-unity-build-agent/Dockerfile create mode 100755 samples/unity-build-pipeline/docker/teamcity-unity-build-agent/build-and-push.sh create mode 100644 samples/unity-build-pipeline/docker/teamcity-unity-build-agent/teamcity-agent-startup.sh create mode 100644 samples/unity-build-pipeline/locals.tf create mode 100644 samples/unity-build-pipeline/main.tf create mode 100644 samples/unity-build-pipeline/outputs.tf create mode 100644 samples/unity-build-pipeline/providers.tf create mode 100644 samples/unity-build-pipeline/security.tf create mode 100644 samples/unity-build-pipeline/terraform.tfvars.example create mode 100644 samples/unity-build-pipeline/variables.tf create mode 100644 samples/unity-build-pipeline/versions.tf create mode 100644 samples/unity-build-pipeline/vpc.tf 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 +}