Bootstrap Guide
Complete guide to bootstrapping AWS infrastructure for the NorthBuilt RAG System.
Overview
The bootstrap process sets up foundational AWS resources required before deploying the main Terraform infrastructure. This project uses a hybrid approach where:
- Bootstrap script creates minimal IAM infrastructure (one-time, manual)
- Terraform manages detailed IAM policies (ongoing, via CI/CD)
What Bootstrap Creates
| Resource | Purpose | Managed By |
|---|---|---|
| S3 Bucket | Terraform state storage | Manual (pre-existing) |
| DynamoDB Table | Terraform state locking | Manual (pre-existing) |
| OIDC Provider | GitHub Actions authentication | Bootstrap script |
| IAM Role | GitHub Actions assume role | Bootstrap script |
| Inline Policy | Minimal bootstrap permissions | Bootstrap script |
| 6 IAM Policies | Detailed Terraform permissions | Terraform module |
Why This Design?
The Chicken-and-Egg Problem:
- Terraform needs AWS credentials to run
- GitHub Actions needs an IAM role to get credentials
- The IAM role needs policies to do anything useful
- If Terraform manages those policies, how does the first run work?
Solution: The bootstrap script creates a role with just enough permissions to:
- Access Terraform state (S3 + DynamoDB)
- Create and attach the detailed IAM policies
- Terraform then manages the detailed policies going forward
Benefits:
- Detailed policies are version-controlled and reviewed in PRs
- Terraform detects drift if someone manually changes policies
- Follows principle of least privilege
- No long-lived AWS access keys needed
Prerequisites
- AWS CLI configured with admin credentials
- GitHub CLI (
gh) installed and authenticated - Terraform installed (>= 1.13.0)
- GitHub repository created
Quick Start
# 1. Run bootstrap script (creates OIDC provider + role)
./.github/setup-oidc.sh
# 2. Set GitHub secret
gh secret set AWS_ROLE_ARN --body "arn:aws:iam::$(aws sts get-caller-identity --query Account --output text):role/GitHubActionsOIDCRole"
# 3. Push to main - Terraform creates detailed policies via CI/CD
git push origin main
Detailed Steps
Step 1: Create Terraform State Backend (If Not Exists)
The S3 bucket and DynamoDB table for Terraform state should already exist. If not:
export AWS_REGION="us-east-1"
export STATE_BUCKET="nb-rag-sys-terraform-state"
export LOCK_TABLE="nb-rag-sys-terraform-locks"
# Create S3 bucket
aws s3api create-bucket --bucket $STATE_BUCKET --region $AWS_REGION
# Enable versioning
aws s3api put-bucket-versioning \
--bucket $STATE_BUCKET \
--versioning-configuration Status=Enabled
# Enable encryption
aws s3api put-bucket-encryption \
--bucket $STATE_BUCKET \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
}'
# Block public access
aws s3api put-public-access-block \
--bucket $STATE_BUCKET \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
# Create DynamoDB table
aws dynamodb create-table \
--table-name $LOCK_TABLE \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--region $AWS_REGION
# Wait for table
aws dynamodb wait table-exists --table-name $LOCK_TABLE
Step 2: Run Bootstrap Script
The bootstrap script creates:
- OIDC provider for GitHub Actions
- IAM role with trust policy for your repository
- Minimal inline policy for self-management
./.github/setup-oidc.sh
What the script does:
- Creates OIDC provider (if not exists)
- Creates/updates IAM role trust policy
- Creates
GitHubActionsBootstrapinline policy with:- Terraform state access (S3 + DynamoDB)
- Permission to manage
GitHubActionsTerraform*policies - Permission to attach/detach policies to itself
Step 3: Configure GitHub Secrets
# Required
gh secret set AWS_ROLE_ARN --body "arn:aws:iam::ACCOUNT_ID:role/GitHubActionsOIDCRole"
# For Google OAuth (Cognito)
gh secret set GOOGLE_CLIENT_ID --body "your-client-id"
gh secret set GOOGLE_CLIENT_SECRET --body "your-client-secret"
# For integrations (optional)
gh secret set FATHOM_API_KEY --body "..."
gh secret set HELPSCOUT_APP_ID --body "..."
gh secret set HELPSCOUT_APP_SECRET --body "..."
Step 4: Deploy via GitHub Actions
Push to main branch to trigger the Terraform workflow:
git push origin main
On first run, Terraform will:
- Create the 6 detailed IAM policies
- Attach them to the GitHubActionsOIDCRole
- Deploy all other infrastructure
IAM Policy Architecture
After Terraform runs, the role has:
Inline Policy (from bootstrap)
GitHubActionsBootstrap - Provides permissions for:
- Terraform state: S3 bucket and DynamoDB table for state management
- IAM self-management: Create, attach, and manage GitHubActionsTerraform* policies
- Read-only access: Comprehensive read permissions for all AWS resources managed by Terraform (S3, S3Vectors, Secrets Manager, Lambda, API Gateway, Cognito, DynamoDB, CloudWatch Logs, CloudFront, EC2, Bedrock)
The read permissions are necessary because terraform plan must read existing resource state before the detailed managed policies exist. This solves the chicken-and-egg problem where Terraform needs permissions to create the policies that would give it permissions.
Managed Policies (from Terraform)
| Policy | Purpose |
|——–|———|
| GitHubActionsTerraformState | S3 state bucket + DynamoDB locks |
| GitHubActionsTerraformS3 | Application S3 buckets + S3Vectors |
| GitHubActionsTerraformCompute | Lambda + API Gateway + EventBridge |
| GitHubActionsTerraformIAM | IAM roles/policies for app resources |
| GitHubActionsTerraformLogs | CloudWatch Logs (scoped to app) |
| GitHubActionsTerraformServices | Bedrock, Cognito, DynamoDB, RDS, etc. |
See terraform/modules/github-actions-iam/README.md for detailed permissions.
Importing Existing Policies
If you ran an older version of the setup script that created standalone policies:
cd terraform
./import-github-actions-iam.sh
This imports existing policies into Terraform state so Terraform can manage them.
Troubleshooting
OIDC Provider Already Exists
This is fine - the script will skip creation and use the existing provider.
Permission Denied on First Terraform Run
The bootstrap inline policy should have all necessary permissions. Check:
# View inline policy
aws iam get-role-policy \
--role-name GitHubActionsOIDCRole \
--policy-name GitHubActionsBootstrap
# View attached policies
aws iam list-attached-role-policies \
--role-name GitHubActionsOIDCRole
State Lock Timeout
# List locks
aws dynamodb scan --table-name nb-rag-sys-terraform-locks
# Force unlock (use LockID from scan)
cd terraform
terraform force-unlock LOCK_ID
GitHub Actions Authentication Failed
# Verify OIDC provider
aws iam list-open-id-connect-providers
# Verify trust policy allows your repo
aws iam get-role --role-name GitHubActionsOIDCRole \
--query 'Role.AssumeRolePolicyDocument'
# Check GitHub secret exists
gh secret list
Re-running Bootstrap
The bootstrap script is idempotent - safe to run multiple times:
./.github/setup-oidc.sh
It will:
- Skip OIDC provider if exists
- Update trust policy if changed
- Recreate inline policy (overwrites)
- Detach old managed policies (Terraform will re-attach)
Cleanup
To completely remove bootstrap resources:
# Detach all policies
aws iam list-attached-role-policies --role-name GitHubActionsOIDCRole \
--query 'AttachedPolicies[].PolicyArn' --output text | \
xargs -n1 aws iam detach-role-policy --role-name GitHubActionsOIDCRole --policy-arn
# Delete inline policy
aws iam delete-role-policy \
--role-name GitHubActionsOIDCRole \
--policy-name GitHubActionsBootstrap
# Delete role
aws iam delete-role --role-name GitHubActionsOIDCRole
# Delete OIDC provider
aws iam delete-open-id-connect-provider \
--open-id-connect-provider-arn arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com
# Delete managed policies (run terraform destroy first, or manually)
for policy in State S3 Compute IAM Logs Services; do
aws iam delete-policy \
--policy-arn arn:aws:iam::ACCOUNT_ID:policy/GitHubActionsTerraform${policy}
done
Security Considerations
- Least Privilege: Policies grant minimum required permissions
- No Wildcards: Specific actions instead of
s3:*,lambda:*, etc. - Resource Scoping: Most resources scoped to
nb-rag-sys-*prefix - Tag-Based Access: EC2 security groups require project tag
- Audit Trail: Policy changes visible in git history and Terraform plans
- No Long-Lived Credentials: OIDC provides temporary credentials per workflow run