← 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:

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:

  1. Write code locally.
  2. Push to a feature branch.
  3. Open a PR — GitHub runs tests and Terraform plan.
  4. Review the plan. Looks good?
  5. Merge to main — GitHub deploys to dev, then staging, then prod.
  6. 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