Skip to content

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:

  1. A developer creates a PR with code that requires a new environment variable
  2. The PR is merged and deployed
  3. The application fails because the environment variable doesn't exist yet
  4. 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.example frequently 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.example stays 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:sync on 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.skip for local-only variables

Risks

  • False positives: Envy may flag variables that are intentionally not in .env.example. Mitigate with excludes configuration.
  • Incomplete coverage: env() calls in application code (outside config files) won't be detected. Mitigate with includes configuration for known cases.

Note: As a Laravel best practice, avoid calling env() outside config files—use config() 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.skip causes 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:

  1. Install and configure Envy in all Laravel applications (maybe start with Offer API or similar to test this and then replicate to others)
  2. Run initial envy:sync and envy:prune to establish baseline
  3. Review and resolve any discrepancies found
  4. Create .env.aws.skip with variables that don't require AWS verification
  5. Update GitHub Actions workflows to include Envy validation
  6. Create verification script for AWS environment checking (verify-env-vars.sh)
  7. Add startup validation to AppServiceProvider for critical variables
  8. 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

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