0043: Environment Variable Validation in CI/CD Pipeline for Laravel Applications
STATUS
Accepted
CONTEXT
Following Incident IR-235, where support cases could not be created due to missing environment variables, we identified a gap in our deployment process. Code changes that depend on new or modified environment variables can be deployed before the infrastructure changes that provision those variables, leading to runtime failures.
Currently, our CI/CD pipeline (GitHub Actions, as established in ADR 0036) does not validate that required environment variables exist in the target environment before deploying application code. This creates a problem where:
- A developer creates a PR with code that requires a new environment variable
- The PR is merged and deployed
- The application fails because the environment variable doesn't exist yet
- Infrastructure changes to add the variable are deployed afterward (or forgotten entirely)
This problem is compounded by:
- Separate repositories for application code and infrastructure (CDK)
- No automated way to detect new environment variable dependencies
- No enforcement of deployment ordering between infrastructure and application changes
Laravel applications typically use the env() helper function throughout config files to reference environment variables, with .env.example serving as documentation for required variables. However, .env.example files often become stale as developers add new env() calls without updating the example file.
Considered Options
Option 1: Manual .env.example Maintenance with CI Validation
Require developers to manually keep .env.example synchronized with actual env() usage in config files. Add CI steps to compare .env.example against production/staging configurations.
Pros:
- Uses Laravel's existing convention
- No additional dependencies
Cons:
- Relies on developer discipline
.env.examplefrequently drifts from actual requirements- No automated detection of new
env()calls
Option 2: Worksome Envy for Automated .env.example Synchronization
Use the Worksome Envy package to automatically detect env() calls in config files and keep .env.example synchronized. Integrate Envy's dry-run mode into CI to fail builds when environment variables are out of sync.
Pros:
- Automatically scans config files for
env()calls - Detects missing or extraneous environment variables
- Dry-run mode for CI validation without side effects
- Preserves comments and can auto-generate location references
- Handles default values from
env('VAR', 'default')calls - Well-maintained, Laravel-specific solution
Cons:
- Adds a dev dependency
- Can make the CI/CD pipeline slower
Option 3: Laravel Configuration Caching with Strict Mode
Use php artisan config:cache in CI and configure Laravel to fail on missing environment variables by removing default values from env() calls.
Pros:
- Uses built-in Laravel functionality
- Forces explicit environment variable definitions
Cons:
- Only catches issues at config cache time
- Requires removing useful default values
- Doesn't prevent deployment of code with missing variables
Option 4: Pre-Deployment Verification Against AWS
Add a verification step that queries AWS SSM Parameter Store / Secrets Manager / Elastic Beanstalk to confirm all variables in .env.example exist before deployment.
Pros:
- Verifies against actual environment configuration
- Blocks deployment if variables are missing
Cons:
- Requires IAM permissions to read environment configuration
- Depends on accurate
.env.example(ties back to Option 1) - Adds complexity to deployment workflow
DECISION
We recommend a layered approach combining Options 2 and 4, using Worksome Envy as the primary mechanism:
Phase 1: Envy Integration for .env.example Synchronization
Install Envy in all Laravel applications:
composer require worksome/envy --dev
php artisan envy:install
Configure Envy (config/envy.php) to match project conventions:
return [
'environment_files' => [
base_path('.env.example'),
],
'config_files' => [
config_path(),
],
'display' => [
'comments' => true, // Preserve config file comments
'location' => true, // Show where each variable is referenced
'default' => true, // Include default values
],
'excludes' => [
// Variables managed outside config files
'APP_KEY',
'DEBUGBAR_*',
],
'includes' => [
// Force inclusion of variables used outside config
],
];
Run initial synchronization to establish baseline:
php artisan envy:sync
php artisan envy:prune
Phase 2: CI/CD Validation
PR Validation Checks
Add validation job to GitHub Actions workflow that runs on all PRs. Example implementation:
validate-environment:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Install Dependencies
run: composer install --no-interaction --prefer-dist
- name: Validate Environment Variables (Sync)
run: php artisan envy:sync --dry
- name: Validate Environment Variables (Prune)
run: php artisan envy:prune --dry
The --dry flag causes Envy to exit with a non-zero status if there are any synchronization issues, failing the CI build without making changes.
Define which variables require AWS verification
Not all variables in .env.example need to be provisioned through AWS infrastructure. Some variables have safe defaults, are local-only, or are set through other mechanisms (e.g., Elastic Beanstalk platform settings).
We use an exclusion list approach (.env.aws.skip) where all variables from .env.example are verified against AWS except those explicitly excluded. This ensures new environment variables are automatically verified by default, catching missing infrastructure configuration early rather than silently skipping verification.
Example file:
# .env.aws.skip - Variables that do NOT require AWS verification
# All other variables in .env.example will be verified against AWS SSM/Secrets Manager
# Local development settings (have safe defaults)
APP_DEBUG
APP_ENV
LOG_LEVEL
LOG_CHANNEL
# Set by platform/runtime
AWS_REGION
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
# Optional features (application handles missing values)
DEBUGBAR_ENABLED
TELESCOPE_ENABLED
AWS Enforcement Step
Add enforcement step before deployment that verifies variables exist in target environment. Example implementation:
verify-environment:
runs-on: ubuntu-latest
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Verify Environment Variables in AWS
run: ./scripts/verify-env-vars.sh ${{ env.ENVIRONMENT }}
env:
AWS_REGION: ${{ vars.AWS_REGION }}
Phase 3: Application Startup Validation
As a final safety net, add validation at application boot in AppServiceProvider. Example implementation:
public function boot(): void
{
$this->validateRequiredEnvironmentVariables();
}
private function validateRequiredEnvironmentVariables(): void
{
$required = [
'APP_KEY',
'DB_CONNECTION',
'DB_HOST',
'DB_DATABASE',
// Add critical variables here
];
$missing = array_filter($required, fn ($var) => empty(env($var)));
if (!empty($missing)) {
throw new RuntimeException(
'Missing required environment variables: ' . implode(', ', $missing)
);
}
}
CONSEQUENCES
Positive Outcomes
- Automated detection of new
env()calls ensures.env.examplestays current - CI fails fast when environment variables are added without updating documentation
- Prevents deployment of code that will fail due to missing environment variables
- Clear documentation of environment variable requirements through
.env.example - Comments and location tracking improve developer understanding
- Reduces incident frequency related to misconfigured environments
Negative Outcomes
- Adds Envy as a dev dependency to all Laravel applications
- Requires initial effort to run
envy:syncon existing applications - May surface many existing discrepancies that need resolution
- Variables used outside config files require manual inclusion configuration
- Requires maintaining
.env.example(via Envy) plus.env.aws.skipfor local-only variables
Risks
- False positives: Envy may flag variables that are intentionally not in
.env.example. Mitigate withexcludesconfiguration. - Incomplete coverage:
env()calls in application code (outside config files) won't be detected. Mitigate withincludesconfiguration for known cases.
Note: As a Laravel best practice, avoid calling
env()outside config files—useconfig()helpers instead. This ensures proper caching and makes Envy's detection comprehensive. - Skip list drift: Forgetting to add a local-only variable to.env.aws.skipcauses false deployment failures. Mitigate by ensuring error messages guide developers to add the variable to the skip list. - CI overhead: Adds time to CI pipeline. Mitigate by running Envy validation in parallel with other checks.
NOTES
Implementation Tasks
After ADR approval, the following subtasks should be created on the parent story:
- Install and configure Envy in all Laravel applications (maybe start with Offer API or similar to test this and then replicate to others)
- Run initial
envy:syncandenvy:pruneto establish baseline - Review and resolve any discrepancies found
- Create
.env.aws.skipwith variables that don't require AWS verification - Update GitHub Actions workflows to include Envy validation
- Create verification script for AWS environment checking (
verify-env-vars.sh) - Add startup validation to
AppServiceProviderfor critical variables - Update PR templates with environment variable checklist
Envy Commands Reference
| Command | Description |
|---|---|
php artisan envy:sync |
Syncs .env.example with env() calls in config |
php artisan envy:sync --dry |
Check only, exit non-zero if out of sync |
php artisan envy:sync --force |
Apply changes without prompts |
php artisan envy:prune |
Remove unused variables from .env.example |
php artisan envy:prune --dry |
Check only, exit non-zero if prunable |
References
- Incident IR-235 Postmortem
- ADR 0036: Optimal CI/CD Pipeline
- Worksome Envy - GitHub
- PR #98: feat(AGPI-1622): create adr for env validation
- PR #127: docs: backfill PR reference links for existing ADRs
Original Author
Luiz Bueno
Approval date
Approved by
Appendix
Example Verification Script
#!/bin/bash
# verify-env-vars.sh - Verify required environment variables exist in target environment
# Verifies all .env.example variables except those listed in .env.aws.skip
set -e
ENVIRONMENT=$1
ENV_EXAMPLE=".env.example"
ENV_AWS_SKIP=".env.aws.skip"
if [ ! -f "$ENV_EXAMPLE" ]; then
echo "No .env.example file found, skipping AWS verification"
exit 0
fi
if [ ! -f "$ENV_AWS_SKIP" ]; then
echo "No .env.aws.skip file found, skipping AWS verification"
echo "Create .env.aws.skip to enable pre-deployment verification."
exit 0
fi
# Get all variables from .env.example
ALL_VARS=$(grep -E '^[A-Z][A-Z0-9_]*=' "$ENV_EXAMPLE" | cut -d'=' -f1)
# Get variables to skip
SKIP_VARS=$(grep -E '^[A-Z][A-Z0-9_]*$' "$ENV_AWS_SKIP" || true)
# Filter out skipped variables
REQUIRED_VARS=""
for VAR in $ALL_VARS; do
if ! echo "$SKIP_VARS" | grep -qx "$VAR"; then
REQUIRED_VARS="$REQUIRED_VARS $VAR"
fi
done
if [ -z "$REQUIRED_VARS" ]; then
echo "No variables to verify"
exit 0
fi
echo "Verifying AWS environment variables for $ENVIRONMENT..."
echo ""
MISSING_VARS=()
FOUND_COUNT=0
TOTAL_COUNT=0
for VAR in $REQUIRED_VARS; do
((TOTAL_COUNT++))
# Check SSM Parameter Store
if aws ssm get-parameter --name "/$ENVIRONMENT/$VAR" --query 'Parameter.Value' --output text 2>/dev/null; then
((FOUND_COUNT++))
echo " ✓ $VAR (SSM)"
# Check Secrets Manager
elif aws secretsmanager get-secret-value --secret-id "$ENVIRONMENT/$VAR" --query 'SecretString' --output text 2>/dev/null; then
((FOUND_COUNT++))
echo " ✓ $VAR (Secrets Manager)"
else
MISSING_VARS+=("$VAR")
echo " ✗ $VAR (NOT FOUND)"
fi
done
echo ""
echo "Verified $FOUND_COUNT/$TOTAL_COUNT variables"
if [ ${#MISSING_VARS[@]} -gt 0 ]; then
echo ""
echo "ERROR: Missing required environment variables in AWS:"
for VAR in "${MISSING_VARS[@]}"; do
echo " - $VAR"
done
echo ""
echo "Options to resolve:"
echo " 1. Add the variable to AWS SSM (/$ENVIRONMENT/VAR_NAME) or Secrets Manager"
echo " 2. Add the variable to .env.aws.skip if it doesn't require AWS provisioning"
echo ""
echo "See: https://docs.internal/env-var-guidelines"
exit 1
fi
echo ""
echo "All required AWS environment variables verified successfully."
Example PR Template Addition
## Environment Variable Checklist
If this PR adds or modifies environment variables:
- [ ] Added new `env()` call to appropriate config file (not directly in application code)
- [ ] Ran `php artisan envy:sync` to update `.env.example`
- [ ] Added local-only variables to `.env.aws.skip` if they don't require AWS verification
- [ ] Infrastructure PR created/merged (if applicable): [link]
- [ ] Verified variables exist in all applicable environments
Example .env.example with Envy Comments
When configured with display.comments and display.location enabled, Envy generates helpful documentation:
# Application
# Used in: config/app.php
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
# Database
# Used in: config/database.php
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
# BigDataCloud API for geocoding fallback
# Used in: config/services.php
# Added: 2026-01
API_KEY_BIGDATACLOUD=
Example .env.aws.skip File
# .env.aws.skip - Variables that do NOT require AWS verification
#
# All variables in .env.example EXCEPT those listed here will be verified
# against AWS SSM Parameter Store or Secrets Manager before deployment.
#
# Add variables here if they:
# - Have safe defaults in config files
# - Are local development only
# - Are set by the platform/runtime (not infrastructure)
# - Are optional features the app handles gracefully when missing
# Local development settings
APP_DEBUG
APP_ENV
LOG_LEVEL
LOG_CHANNEL
LOG_STACK
# Platform-provided (Elastic Beanstalk, ECS, etc.)
AWS_REGION
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
# Optional development tools
DEBUGBAR_ENABLED
TELESCOPE_ENABLED
IGNITION_ENABLED
# Have safe defaults in config
CACHE_DRIVER
SESSION_DRIVER
QUEUE_CONNECTION
BROADCAST_DRIVER