Skip to main content
Technology & EngineeringCicd Patterns382 lines

Environment Management

Managing secrets, environment variables, deployment environments, and configuration across CI/CD pipelines

Quick Summary18 lines
You are an expert in managing secrets, environments, and configuration across CI/CD pipelines.

## Key Points

1. **CI platform configuration** — secrets and variables set in the CI platform (GitHub, GitLab, etc.)
2. **External secrets managers** — HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
3. **Application configuration** — environment-specific config files, feature flags, service endpoints
- Required reviewers for production
- Wait timer (e.g., 15 minutes after staging deploy)
- Deployment branch restrictions (only `main`)
- Environment-scoped secrets (different `API_KEY` per environment)
- deploy-dev
- deploy-staging
- deploy-production
- name: Generate environment config
- name: Verify no secrets in git
skilldb get cicd-patterns-skills/Environment ManagementFull skill: 382 lines
Paste into your CLAUDE.md or agent config

Environment Management — CI/CD

You are an expert in managing secrets, environments, and configuration across CI/CD pipelines.

Overview

Environment management in CI/CD encompasses secrets handling (API keys, credentials, certificates), environment variables, deployment environment configuration (staging, production), and the promotion of releases across environments. Proper environment management ensures security (secrets are never exposed), consistency (environments are reproducible), and auditability (who deployed what, when, and where).

Setup & Configuration

Environment management spans three layers:

  1. CI platform configuration — secrets and variables set in the CI platform (GitHub, GitLab, etc.)
  2. External secrets managers — HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
  3. Application configuration — environment-specific config files, feature flags, service endpoints

GitHub Actions Environments

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v4
      - run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}

Configure in GitHub Settings > Environments:

  • Required reviewers for production
  • Wait timer (e.g., 15 minutes after staging deploy)
  • Deployment branch restrictions (only main)
  • Environment-scoped secrets (different API_KEY per environment)

Core Patterns

External Secrets with HashiCorp Vault

GitHub Actions with Vault:

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v4

      - name: Import secrets from Vault
        uses: hashicorp/vault-action@v3
        with:
          url: https://vault.example.com
          method: jwt
          role: github-actions
          jwtGithubAudience: https://vault.example.com
          secrets: |
            secret/data/production/db password | DB_PASSWORD ;
            secret/data/production/api key | API_KEY ;
            secret/data/production/tls cert | TLS_CERT

      - run: ./deploy.sh
        env:
          DB_PASSWORD: ${{ env.DB_PASSWORD }}
          API_KEY: ${{ env.API_KEY }}

AWS Secrets Manager Integration

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1

      - name: Get secrets
        uses: aws-actions/aws-secretsmanager-get-secrets@v2
        with:
          secret-ids: |
            prod/database
            prod/api-keys
          parse-json-secrets: true

      - run: ./deploy.sh
        # Secrets are available as environment variables:
        # PROD_DATABASE_PASSWORD, PROD_API_KEYS_STRIPE_KEY, etc.

Kubernetes Secrets with External Secrets Operator

# ExternalSecret pulls from AWS Secrets Manager into a K8s Secret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app-secrets
  namespace: production
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: app-secrets
    creationPolicy: Owner
  data:
    - secretKey: database-url
      remoteRef:
        key: prod/database
        property: url
    - secretKey: api-key
      remoteRef:
        key: prod/api-keys
        property: main
---
# Use in Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          envFrom:
            - secretRef:
                name: app-secrets

Environment Promotion Pipeline

# GitLab CI environment promotion
stages:
  - build
  - deploy-dev
  - deploy-staging
  - deploy-production

variables:
  IMAGE: registry.example.com/myapp

build:
  stage: build
  script:
    - docker build -t $IMAGE:$CI_COMMIT_SHA .
    - docker push $IMAGE:$CI_COMMIT_SHA

.deploy-template:
  script:
    - helm upgrade --install myapp ./chart
        --set image.tag=$CI_COMMIT_SHA
        --set environment=$DEPLOY_ENV
        --values ./chart/values-${DEPLOY_ENV}.yaml
        --namespace $DEPLOY_ENV
        --wait --timeout 300s

deploy-dev:
  extends: .deploy-template
  stage: deploy-dev
  variables:
    DEPLOY_ENV: dev
  environment:
    name: dev
    url: https://dev.example.com

deploy-staging:
  extends: .deploy-template
  stage: deploy-staging
  variables:
    DEPLOY_ENV: staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"

deploy-production:
  extends: .deploy-template
  stage: deploy-production
  variables:
    DEPLOY_ENV: production
  environment:
    name: production
    url: https://example.com
  rules:
    - if: $CI_COMMIT_BRANCH == "main"
      when: manual
      allow_failure: false

Helm Values Per Environment

# chart/values-dev.yaml
replicaCount: 1
resources:
  requests:
    cpu: 100m
    memory: 128Mi
ingress:
  host: dev.example.com
config:
  logLevel: debug
  featureFlags:
    newCheckout: true

# chart/values-production.yaml
replicaCount: 5
resources:
  requests:
    cpu: 500m
    memory: 512Mi
  limits:
    cpu: 1000m
    memory: 1Gi
ingress:
  host: example.com
config:
  logLevel: warn
  featureFlags:
    newCheckout: false

Dynamic Environments for Pull Requests

# GitHub Actions: Deploy preview environment per PR
deploy-preview:
  if: github.event_name == 'pull_request'
  runs-on: ubuntu-latest
  environment:
    name: pr-${{ github.event.pull_request.number }}
    url: https://pr-${{ github.event.pull_request.number }}.preview.example.com
  steps:
    - uses: actions/checkout@v4
    - name: Deploy preview
      run: |
        kubectl create namespace pr-${{ github.event.pull_request.number }} --dry-run=client -o yaml | kubectl apply -f -
        helm upgrade --install myapp-pr-${{ github.event.pull_request.number }} ./chart \
          --namespace pr-${{ github.event.pull_request.number }} \
          --set image.tag=${{ github.sha }} \
          --set ingress.host=pr-${{ github.event.pull_request.number }}.preview.example.com

# Cleanup when PR is closed
cleanup-preview:
  if: github.event.action == 'closed'
  runs-on: ubuntu-latest
  steps:
    - run: |
        kubectl delete namespace pr-${{ github.event.pull_request.number }} --ignore-not-found

OIDC Federation (No Long-Lived Credentials)

# GitHub Actions OIDC with multiple cloud providers
jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read
    steps:
      # AWS
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1

      # GCP
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: projects/123456/locations/global/workloadIdentityPools/github/providers/github
          service_account: deploy@project.iam.gserviceaccount.com

      # Azure
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

.env File Management

# Generate .env from CI secrets at deploy time
- name: Generate environment config
  run: |
    cat <<EOF > .env.production
    NODE_ENV=production
    DATABASE_URL=${{ secrets.DATABASE_URL }}
    REDIS_URL=${{ secrets.REDIS_URL }}
    API_KEY=${{ secrets.API_KEY }}
    SENTRY_DSN=${{ secrets.SENTRY_DSN }}
    EOF

- name: Verify no secrets in git
  run: |
    if git ls-files --cached | grep -E '\.env($|\.)'; then
      echo "ERROR: .env file is tracked by git"
      exit 1
    fi

Core Philosophy

Environment management is fundamentally about two things: ensuring secrets never leak and ensuring environments are reproducible. These goals are in tension — secrets need to be available to the right processes at the right time, but restricting access too aggressively breaks pipelines and slows development. The resolution is layered access control: OIDC federation for ephemeral credentials, external secrets managers for centralized governance, environment-scoped variables for isolation, and required reviewers for production gates. Each layer adds a defense that compensates for failures in the others.

The most important shift in modern environment management is from static credentials to ephemeral, identity-based access. A long-lived AWS access key stored in a CI variable is a ticking time bomb: it never expires, it is shared across every workflow run, and if leaked it grants access until someone notices and rotates it. OIDC federation replaces this with short-lived tokens issued per workflow run, scoped to the specific permissions needed, and automatically expired. This is not just a security improvement — it eliminates an entire category of operational toil (credential rotation) and incident response (leaked key remediation).

Environment parity is the principle that staging should be as close to production as possible in configuration, data shape, and infrastructure. Every difference between environments is a place where bugs can hide. Using Helm values files, Kustomize overlays, or environment-specific config generates environment differences declaratively, making them auditable and reviewable. When a deployment fails in staging but works in dev, the first question should be "what differs between these environments?" — and the answer should be findable in version control, not buried in a CI platform's settings page.

Anti-Patterns

  • Long-lived credentials in CI variables. Storing static AWS access keys or service account JSON files as CI secrets that are never rotated creates a growing attack surface. Use OIDC federation for cloud providers and short-lived tokens for everything else.

  • Shared credentials across environments. Using the same database password for staging and production means a staging breach compromises production data. Scope every secret to its environment so credential isolation matches environment isolation.

  • Secrets in Docker build args. Passing secrets via --build-arg embeds them in the image layer history, visible to anyone who pulls the image. Use Docker BuildKit secret mounts (--mount=type=secret) which are never persisted in the image.

  • Orphaned preview environments. Creating dynamic per-PR environments without automated cleanup means every abandoned PR leaves running infrastructure. Always pair environment creation with a cleanup trigger on PR close or merge.

  • Manual environment configuration. Setting environment variables through a CI platform's web UI means configuration is not version-controlled, not reviewable, and not reproducible. Define environment configuration in code (Helm values, Terraform, CI config files) so changes go through the same review process as application code.

Best Practices

  • Use OIDC federation instead of long-lived credentials wherever possible (GitHub Actions, GitLab, CircleCI all support it).
  • Store secrets in external secrets managers (Vault, AWS Secrets Manager), not in CI platform variables for production.
  • Use environment-scoped secrets so staging and production have different credentials with the same variable names.
  • Implement required reviewers and deployment protection rules on production environments.
  • Never echo, log, or print secrets; CI platforms mask known secrets but cannot mask derived values.
  • Use .env.example files (without real values) committed to git as documentation for required variables.
  • Rotate secrets regularly and automate rotation through your secrets manager.
  • Create dynamic preview environments per pull request for testing; clean them up on PR close.
  • Use Helm values files or Kustomize overlays to manage per-environment configuration declaratively.
  • Audit secret access; use CI platform audit logs and secrets manager access logs to track who accessed what.

Common Pitfalls

  • Secrets printed to CI logs via echo, env, or debug output are permanently exposed in build logs.
  • Using the same credentials across all environments means a staging breach compromises production.
  • .env files accidentally committed to git; use .gitignore and pre-commit hooks to prevent this.
  • Long-lived service account keys stored in CI variables that are never rotated become a security liability.
  • Dynamic environments (per-PR) that are never cleaned up waste resources and leave orphaned infrastructure.
  • Secrets in Docker build args are visible in image layer history; use BuildKit secret mounts instead.
  • Environment variables set at the step level in GitHub Actions are not available in other steps; use $GITHUB_ENV for cross-step variables.
  • Vault tokens or cloud credentials with overly broad permissions; follow least-privilege principles.

Install this skill directly: skilldb add cicd-patterns-skills

Get CLI access →