← All Articles
April 27, 2026
7 min read
SRE · DevOps · AWS
The Solo Consultant’s Guide to Production-Grade Deployments
You’re building AI systems for clients. You’re the architect, the engineer, and on-call support. There is no ops team. The stakes are high — your client’s business runs on your code, and broken deployments mean downtime, lost revenue, and lost trust. You need a deployment system that prevents mistakes, automates the heavy lifting, scales to multiple environments, and lets you sleep at night.
This is achievable with three pieces — GitHub Actions, Terraform, and AWS OIDC. It looks sophisticated. It runs on your laptop.
The Setup: GitHub Actions + Terraform + AWS OIDC
Why this stack?
GitHub Actions is free, built into your repo, and there’s no separate service to manage.
Terraform is infrastructure as code. You review changes before they deploy — perfect for solo operators who can’t afford mistakes.
AWS OIDC means no stored AWS keys. No credentials in environment variables that can leak. GitHub itself is the trusted identity.
The result: one person safely manages production infrastructure.
Step 1: AWS OIDC Setup
Create a trust relationship between GitHub and AWS so GitHub can authenticate directly without keys.
resource "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
tags = {
Name = "github-actions"
}
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-deployment"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Federated = aws_iam_openid_connect_provider.github.arn
}
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
StringEquals = {
"token.actions.githubusercontent.com:sub" = "repo:linuxlsr/myproject:ref:refs/heads/main"
}
}
}
]
})
}
Now GitHub can authenticate to AWS without any stored keys. Tighten the IAM policy to specific resource ARNs in production.
Step 2: Environment-Specific Terraform
Organize Terraform to support dev, staging, and production with shared modules.
terraform/
environments/
dev/
main.tf (dev Lambda config)
terraform.tfvars (dev settings)
staging/
main.tf (staging Lambda config)
terraform.tfvars (staging settings)
prod/
main.tf (prod Lambda config)
terraform.tfvars (prod settings)
modules/
lambda/
dynamodb/
api_gateway/
shared.tf (global resources)
Each environment inherits from shared modules, but with different parameters — dev uses cheaper, smaller settings; prod uses production-grade settings.
module "lambda" {
source = "../../modules/lambda"
function_name = "invoice-parser-prod"
handler = "index.handler"
memory_size = 1024 # Production: more memory for reliability
timeout = 30
reserved_concurrency = 100 # Prevent runaway costs
environment_variables = {
ENVIRONMENT = "prod"
LOG_LEVEL = "info"
}
}
module "dynamodb" {
source = "../../modules/dynamodb"
table_name = "invoices-prod"
billing_mode = "PAY_PER_REQUEST" # Auto-scale on demand for prod
}
Step 3: GitHub Actions CI/CD Pipeline
Build a workflow that runs tests on every commit, plans Terraform on PR, applies on merge to main, and includes an approval gate before production.
name: Deploy to AWS
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: "3.11"
- run: pip install -r requirements.txt pytest
- run: pytest tests/ -v
terraform_plan:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deployment
aws-region: us-east-1
- run: |
cd terraform/environments/${{ matrix.environment }}
terraform init
terraform plan -out=tfplan
terraform_apply:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: terraform_plan
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging]
max-parallel: 1 # Deploy environments sequentially
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deployment
aws-region: us-east-1
- run: |
cd terraform/environments/${{ matrix.environment }}
terraform init
terraform apply -auto-approve tfplan
terraform_apply_prod:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: terraform_apply
runs-on: ubuntu-latest
environment:
name: production # GitHub environment with required reviewers
steps:
- uses: actions/checkout@v3
- uses: aws-actions/configure-aws-credentials@v2
with:
role-to-assume: arn:aws:iam::ACCOUNT_ID:role/github-actions-deployment
aws-region: us-east-1
- run: |
cd terraform/environments/prod
terraform init
terraform apply -auto-approve tfplan
- run: pytest tests/integration/test_prod.py -v
Key features:
- Tests first — no deploy without passing tests.
- Plans before apply — you see what Terraform will change.
- PR comments — changes are visible in the pull request.
- Sequential environments — dev, then staging, then prod. Never deploy to prod in parallel with other changes.
- Production approval gate — GitHub’s built-in environment approvals add a manual checkpoint.
Step 4: Automatic Rollback
If post-deploy smoke tests fail, roll back to the last known-good code.
deploy_prod_with_rollback:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Capture current version
run: |
CURRENT_VERSION=$(aws lambda get-function \
--function-name invoice-parser-prod \
--query 'Configuration.LastModified' --output text)
echo "CURRENT_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
- name: Deploy to prod
run: terraform -chdir=terraform/environments/prod apply -auto-approve
- name: Run smoke tests
id: smoke_test
run: pytest tests/integration/smoke_test.py -v --timeout=60
continue-on-error: true
- name: Rollback if tests fail
if: ${{ steps.smoke_test.outcome }} == 'failure'
run: |
echo "Tests failed. Rolling back..."
aws lambda update-function-code \
--function-name invoice-parser-prod \
--s3-bucket lambda-versions \
--s3-key invoice-parser-${CURRENT_VERSION}.zip
exit 1
- name: Notify Slack
if: failure()
uses: slackapi/slack-github-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK }}
payload: '{"text":"Production deployment failed and rolled back"}'
The rollback only works if you’ve been versioning your Lambda code in S3. Build that into your deploy step from day one — cheap insurance.
Step 5: Branch Protection
Protect main to enforce the process — the rules that turn a workflow into a guardrail.
Main branch:
✓ Require status checks to pass (tests, terraform plan)
✓ Require branches to be up to date
✓ Require code review from 1 person (or yourself on small teams)
✓ Require commit signatures
✓ Restrict who can push (only CI/CD system)
The Result
Your deploy flow looks like this:
- Write code locally.
- Push to a feature branch.
- Open a PR — GitHub runs tests and Terraform plan.
- Review the plan. Looks good?
- Merge to
main — GitHub deploys to dev, then staging, then prod. - Monitor alerts. If prod fails the smoke tests, automatic rollback.
No manual SSH. No copy-pasted commands. No “oops, I deployed to the wrong region.”
One person. Safe deployments. Production-grade.
Get the free AI Readiness Checklist
15 questions to diagnose your team’s AI readiness, where you’ll see ROI fastest, and what to tackle first.
✓ Takes 5 minutes
✓ Actionable next steps
✓ No sales pitch
No spam. Unsubscribe anytime.
or
Ready to build AI that actually works?
Let’s talk about how SRE discipline transforms AI from a risky experiment into a reliable business system.
Book Your Free Discovery Call