Skip to content

03 — Deployment

Scope: Per-client. The client deploys their IAM role, we set the final pipeline variable, then trigger the first deployment.

Admin Account ID: 774222656146 | Admin Role ARN: arn:aws:iam::774222656146:role/CampusCore-Admin-Role If these are outdated, get the current values from Admin Account Setup and update them here.


3a. Client Deploys the Deploy Role (Their Account)

Time: ~10 minutes

The client deploys a CloudFormation template in their AWS account that creates the IAM deploy role. We provide:

  • Template: infrastructure/deploy-roles/deploy-role.yaml
  • Input: Our admin role ARN (see banner above)

Via AWS Console

  1. Go to CloudFormation > Create stack
  2. Upload deploy-role.yaml
  3. Acknowledge IAM capability and create (there are no parameters to enter)

Via CLI

aws cloudformation deploy \
  --template-file deploy-role.yaml \
  --stack-name CampusCore-Deploy-Role \
  --capabilities CAPABILITY_NAMED_IAM \
  --no-fail-on-empty-changeset \
  --profile <your-aws-profile>

Replace <your-aws-profile> with the AWS CLI profile for the client's account (e.g., troy-pilot). The template hardcodes the CampusCore admin role ARN in its trust policy, so there is nothing to paste. This is the same command clients follow in Deploy the CampusCore IAM Role - both include the one shared snippet, so it can't drift.

The Deploy Role ARN is derivable

The template hardcodes the role name (CampusCore-Deploy-Role), so the ARN is always arn:aws:iam::<CLIENT_ACCOUNT_ID>:role/CampusCore-Deploy-Role - nothing needs to be fetched from the client. To verify the stack really completed, the client (or we, with a client profile) can confirm the output matches:

aws cloudformation describe-stacks \
  --stack-name CampusCore-Deploy-Role \
  --query 'Stacks[0].Outputs[?OutputKey==`DeployRoleArn`].OutputValue' \
  --output text \
  --profile <client-profile>

Automatic Updates

The deploy role includes CloudFormation self-update permissions. After the initial manual deployment, the CI/CD pipeline automatically updates this stack on every deploy — any permission changes committed to deploy-role.yaml take effect without manual intervention.

IAM Permissions in the Deploy Role

The deploy role (CampusCore-Deploy-Role) grants permissions for:

  • ECS + ECR: Full access (clusters, services, task definitions, repositories)
  • RDS: Full access (instances, subnet groups, parameter groups)
  • S3: Full access (buckets for app storage)
  • SQS: Full access (queues for document processing)
  • EC2/VPC: Dedicated VPC provisioning (VPC, subnets, IGW, NAT, route tables, flow logs, VPC endpoints) + security group management
  • ELB: Full access (ALBs, target groups, listeners)
  • IAM: Create/manage roles and policies (for ECS task roles)
  • CloudWatch Logs: Full access (log groups, log streams)
  • SSM Parameter Store: Full access (runtime config)
  • KMS: Decrypt/Encrypt access (for SSM parameters)
  • Auto Scaling: Full access (ECS service auto-scaling)
  • CloudFormation: Self-update permissions (deploy role updates itself via pipeline)
  • ACM: Certificate management (request, describe, delete certificates for HTTPS)
  • GuardDuty: Threat detection, upload-bucket malware scanning, runtime monitoring (optional security bundle)
  • Security Hub: Posture scoring against the AWS Foundational Security Best Practices standard (optional security bundle)
  • Inspector: CVE scanning of the container images in ECR (optional security bundle)
  • AWS Config: Resource recording that Security Hub controls evaluate against (optional security bundle)

The full policy is defined in infrastructure/deploy-roles/deploy-role.yaml. Clients can review the exact permissions before deploying.


3b. Verify the Deploy Role

The AWS_ROLE_ARN variable was already set in Step 02 from the derived ARN. What remains is confirming the role really exists - ./scripts/onboard-client.sh deploy-role <env> --account-id <id> --client-profile <p> does this (and deploys the stack itself when we hold client credentials). Manual fallback:

gh variable set AWS_ROLE_ARN --env howard --body "arn:aws:iam::CLIENT_ACCOUNT_ID:role/CampusCore-Deploy-Role"

All pipeline configuration from Step 02 is now complete.


3c. Trigger Deployment

./scripts/onboard-client.sh deploy <env> pushes the remote main to the deploy branch (falling back to a workflow dispatch when the branch is already current) and watches the run by step conclusions. Manual fallback:

# Option A: Push to deploy branch
git push origin main:deploy/howard

# Option B: Manual workflow dispatch
gh workflow run deploy-aws.yml -f client=howard

Monitor the deployment in GitHub Actions. The pipeline will: 1. Provision base infrastructure (VPC, RDS, ECS cluster, ECR, ALB, S3, SQS) 2. If SSL_MODE is not off: provision the {env}.campuscoreai.com subdomain and ACM certificate 3. Build and push Docker images 4. Deploy the ECS services (web + worker) 5. Wait for service stability

Expected deployment times

Scenario First deploy Subsequent deploys
SSL_MODE=off ~15 minutes ~5 minutes
SSL_MODE=managed or self_managed 45-75 minutes ~5 minutes

The first deployment is slower because of one-time infrastructure provisioning (VPC, RDS, ECS cluster, ALB). When a subdomain with SSL is provisioned, the deployment takes significantly longer because:

  1. ACM certificate request — Terraform requests an SSL certificate for {env}.campuscoreai.com
  2. DNS validation record — Terraform creates a CNAME validation record in Route53
  3. Certificate validation wait — AWS must verify domain ownership by checking the DNS record. This step alone typically takes 30-45 minutes and can occasionally take longer. Terraform will show aws_acm_certificate_validation.campuscore[0]: Still creating... during this wait — this is normal.
  4. HTTPS listener creation — once the certificate is issued, Terraform creates the ALB HTTPS listener

This is a one-time cost. Once the certificate is issued, it auto-renews and subsequent deployments skip all of these steps.


3d. The Client's Own Domain

After the first deployment completes, the deployment is live at https://{env}.campuscoreai.com. The client's own domain (e.g. ai.university.edu) is added self-service: the admin opens Settings > Custom Domain, enters the domain, and the app shows the exact DNS records to create - see Custom Domain Setup. No pipeline change or redeploy is involved.


Security Defaults

All infrastructure is provisioned with SOC 2-compliant encryption enabled by default. No additional configuration is required.

Encryption at rest: - RDS PostgreSQL — storage encrypted with AWS-managed KMS key - S3 buckets — AES-256 server-side encryption (SSE-S3) - SQS queues — AWS-managed server-side encryption (SSE-SQS)

Encryption in transit: - Database connections use TLS (sslmode=require) in cloud environments - Django enforces HTTPS via SSL redirect, HSTS (1 year), and Secure cookie flags - ALB serves HTTPS whenever SSL_MODE is not off, and its HTTP listener answers plain-HTTP requests with a 301 to HTTPS rather than forwarding them; only SSL_MODE=off environments serve application traffic over HTTP

Application-level encryption: - Connector credentials (OAuth tokens, API keys) are Fernet-encrypted before database storage

For full details, see Security & SOC 2 Compliance.


Next: 04 — Post-Deployment