Environment Management
Managing secrets, environment variables, deployment environments, and configuration across CI/CD pipelines
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 linesEnvironment 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:
- CI platform configuration — secrets and variables set in the CI platform (GitHub, GitLab, etc.)
- External secrets managers — HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault
- 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_KEYper 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-argembeds 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.examplefiles (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.
.envfiles accidentally committed to git; use.gitignoreand 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_ENVfor 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
Related Skills
Artifact Management
Build artifact handling, dependency caching, container image management, and artifact registry patterns for CI/CD
Buildkite
Buildkite pipelines including dynamic pipeline generation, agent targeting, plugins, and hybrid cloud CI/CD
Circleci
CircleCI configuration including orbs, workflows, executors, and pipeline parameterization for CI/CD
Deployment Strategies
Blue/green, canary, rolling, and feature-flag deployment strategies with platform-specific implementation patterns
Github Actions
GitHub Actions workflows for CI/CD automation including reusable workflows, matrix builds, and deployment pipelines
Gitlab CI
GitLab CI/CD pipelines including multi-stage builds, includes, rules, and Auto DevOps configuration