🚀 Executive Summary
TL;DR: The article addresses the paradox of perfect automation in CI/CD, where minor errors can lead to catastrophic production issues due to the lack of human oversight for critical steps. It offers three practical solutions, from a quick shell script to robust platform-native features and an ‘infrastructure lock,’ to safely integrate human checkpoints into automated workflows.
🎯 Key Takeaways
- A quick, platform-agnostic manual approval can be implemented using a shell script that pauses execution and waits for user input, though it relies on CI job timeouts.
- Robust and auditable manual checkpoints are best achieved through platform-native features like GitLab CI’s `when: manual` keyword or GitHub Actions Environments with required reviewers.
- For maximum security and out-of-band review, an ‘infrastructure lock’ can be implemented by requiring a privileged user to manually attach temporary IAM policies to grant deployment permissions.
Struggling to add a manual approval step to your fully automated CI/CD pipeline? Discover three practical solutions, from a quick script hack to robust platform-native features, to safely manage critical deployments without sacrificing velocity.
Let’s Talk About the ‘Big Red Button’: Adding Human Checkpoints to Automated Workflows
I remember it like it was yesterday. It was 2:45 AM, and my phone lit up with a PagerDuty alert that could melt steel. A junior engineer, eager to please, had pushed what they thought was a minor CSS fix. But our CI/CD pipeline, a beautiful, fully-automated beast we’d spent months perfecting, did its job flawlessly. It linted, tested, built, and deployed the change straight to production in under six minutes. The problem? A tiny typo in a webpack config file was also part of that commit, and it took down our entire customer-facing checkout service. The pipeline was perfect, but the process was flawed. We had built a high-speed train with no brakes.
The “Why”: The Paradox of Perfect Automation
We live and breathe automation. The goal is always to remove human error, increase speed, and make deployments a non-event. But there’s a paradox: the more powerful and seamless your automation becomes, the more catastrophic a small mistake can be. The root cause of this anxiety isn’t that automation is bad; it’s that some actions carry an irreversible, high-stakes business cost.
You don’t want a manual gate for every commit. But for things like:
- Deploying to the `production` environment for the first time in a week.
- Running a Terraform plan that wants to destroy `prod-db-01`.
- Pushing a major database schema migration.
…you want a human, a second set of eyes, to say “Yes, I’ve reviewed this. Proceed.” You need a thoughtfully placed emergency brake, not a hand crank on the engine.
The Fixes: From Duct Tape to Defensive Architecture
Look, I’ve seen this problem solved a dozen ways. Here are the three main patterns I’ve used in the trenches, ranging from a quick fix to get you through the night to a proper, robust solution.
Solution 1: The Quick & Dirty ‘Sleep-on-it’ Script
Let’s be honest, sometimes you just need a fix right now. You don’t have time to re-architect your whole pipeline. This is the duct tape solution, but it’s surprisingly effective. Most CI/CD runners (Jenkins, GitLab, GitHub Actions) are just executing shell scripts. So, you can make the shell wait.
Here’s a simple bash script you can drop into a job stage right before a risky step:
# In your .gitlab-ci.yml or Jenkinsfile shell step
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "!! MANUAL APPROVAL REQUIRED TO DEPLOY TO PRODUCTION !!"
echo "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo "You are about to deploy branch: $CI_COMMIT_BRANCH"
echo "To environment: production"
echo "Review the pipeline logs above. If ready, type 'proceed' and press [ENTER]"
echo "To cancel, press CTRL+C or type anything else."
read -p "Confirmation: " user_input
if [ "$user_input" != "proceed" ]; then
echo "Deployment cancelled by user."
exit 1
fi
echo "User approved. Proceeding with production deployment..."
# ... your kubectl apply, terraform apply, etc. command goes here
Heads Up: This is a hack! It works by keeping the runner job active while it waits for input. Most CI systems have job timeouts (e.g., 60 minutes). If no one types ‘proceed’ before the timeout, the job will fail. This is great for an emergency but isn’t a permanent solution.
Solution 2: The “Proper” Platform-Native Approach
Okay, the duct tape is holding. Now let’s fix it right. Your CI/CD platform of choice almost certainly has a built-in feature for this. This is the method you should be aiming for.
For GitLab CI, this is the `when: manual` keyword. It creates a “play” button in the UI for that job, which will only run when a user with appropriate permissions clicks it.
# .gitlab-ci.yml example
deploy_production:
stage: deploy
script:
- echo "Deploying to production environment..."
- ./deploy-script.sh --env=prod
rules:
- if: $CI_COMMIT_BRANCH == "main"
when: manual # This is the magic line
allow_failure: false
For GitHub Actions, you can achieve this using Environments. You can configure an environment (e.g., `production`) to require approval from specific people or teams before a workflow job that targets it can proceed.
# .github/workflows/deploy.yml example
jobs:
deploy-to-prod:
runs-on: ubuntu-latest
environment:
name: production # This name must match the one in your repo settings
url: https://my-app.com
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Deploy to production servers
run: ./deploy-script.sh
You then configure the `production` environment in your repository’s settings (Settings > Environments) and add the required reviewers. The workflow will pause automatically until one of them approves it in the GitHub UI.
Solution 3: The ‘Break Glass’ Infrastructure Lock
Sometimes, the risk is so high (think core billing data migration) that you don’t even trust the CI/CD UI. You want to make it physically impossible for the automation to proceed until an explicit, out-of-band action is taken. This is the “nuclear” option.
The strategy here is to have your CI/CD runner assume an IAM role with very specific, temporary credentials. By default, this role lacks permission to deploy to production. To “unlock” the deployment, a privileged user must manually attach a policy that grants `cloudformation:CreateStack` or `s3:PutObject` to that role for a limited time.
Here’s how it compares to the other methods:
| Method | Pros | Cons |
|---|---|---|
| Shell Script | Fast to implement, platform-agnostic. | Hacky, relies on job timeouts, not auditable. |
| Platform-Native | Clean, auditable (UI logs who approved), intended use. | Tied to your CI/CD platform (GitLab, GitHub, etc.). |
| Infrastructure Lock | Maximum security, forces out-of-band review, platform-agnostic. | Complex to set up, high operational friction, can be confusing. |
We only use this method during major infrastructure freezes or for changes so critical that we need sign-off from directors who don’t even have a GitLab account. It’s a massive, deliberate speed bump, and that’s the whole point.
Ultimately, the goal isn’t to slow down, it’s to add wisdom. Automation gets you speed; a well-placed manual checkpoint ensures you’re speeding in the right direction.
🤖 Frequently Asked Questions
âť“ Why are human checkpoints necessary in fully automated CI/CD pipelines?
Despite the goal of removing human error, highly automated pipelines can amplify the impact of small mistakes. Human checkpoints are crucial for high-stakes actions like production deployments or critical infrastructure changes to prevent catastrophic business costs by ensuring a second set of eyes reviews the action.
âť“ How do the different manual approval methods compare in terms of pros and cons?
The shell script method is fast and platform-agnostic but hacky, relies on job timeouts, and isn’t auditable. Platform-native solutions are clean, auditable, and intended for use but are tied to specific CI/CD platforms. The infrastructure lock offers maximum security and forces out-of-band review but is complex to set up and introduces high operational friction.
âť“ What is a common implementation pitfall when using the ‘sleep-on-it’ script method?
A common pitfall is that the script relies on the CI runner job staying active while waiting for input. Most CI systems have job timeouts (e.g., 60 minutes), so if no one types ‘proceed’ before the timeout, the job will fail, requiring a restart.
Leave a Reply