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-RoleIf 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¶
- Go to CloudFormation > Create stack
- Upload
deploy-role.yaml - 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:
- ACM certificate request — Terraform requests an SSL certificate for
{env}.campuscoreai.com - DNS validation record — Terraform creates a CNAME validation record in Route53
- 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. - 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