Skip to main content

Environment Management

Master environment management strategies to ensure consistency, reliability, and security across all deployment environments.

Environment Management Overview

Effective environment management ensures that applications run consistently across development, staging, and production environments.

Environment Strategy

Infrastructure as Code (IaC)

IaC enables you to define and provision infrastructure using code, ensuring reproducibility and version control.

Terraform Implementation

1. Terraform Project Structure

terraform/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── kubernetes/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── database/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── production/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── terraform.tfstate

2. AWS Infrastructure Module

# modules/networking/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true

tags = merge(
var.common_tags,
{
Name = "${var.environment}-vpc"
}
)
}

resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]

map_public_ip_on_launch = true

tags = merge(
var.common_tags,
{
Name = "${var.environment}-public-subnet-${count.index + 1}"
Type = "public"
}
)
}

resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + length(var.availability_zones))
availability_zone = var.availability_zones[count.index]

tags = merge(
var.common_tags,
{
Name = "${var.environment}-private-subnet-${count.index + 1}"
Type = "private"
}
)
}

resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id

tags = merge(
var.common_tags,
{
Name = "${var.environment}-igw"
}
)
}

resource "aws_eip" "nat" {
count = length(var.availability_zones)
domain = "vpc"

tags = merge(
var.common_tags,
{
Name = "${var.environment}-nat-eip-${count.index + 1}"
}
)

depends_on = [aws_internet_gateway.main]
}

resource "aws_nat_gateway" "main" {
count = length(var.availability_zones)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id

tags = merge(
var.common_tags,
{
Name = "${var.environment}-nat-${count.index + 1}"
}
)
}

# modules/networking/variables.tf
variable "environment" {
description = "Environment name"
type = string
}

variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
}

variable "availability_zones" {
description = "List of availability zones"
type = list(string)
}

variable "common_tags" {
description = "Common tags for all resources"
type = map(string)
default = {}
}

# modules/networking/outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}

output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}

output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}

3. Kubernetes Cluster Module

# modules/kubernetes/main.tf
resource "aws_eks_cluster" "main" {
name = "${var.environment}-cluster"
role_arn = aws_iam_role.cluster.arn
version = var.kubernetes_version

vpc_config {
subnet_ids = var.subnet_ids
endpoint_private_access = true
endpoint_public_access = true
public_access_cidrs = var.allowed_cidr_blocks
security_group_ids = [aws_security_group.cluster.id]
}

enabled_cluster_log_types = [
"api",
"audit",
"authenticator",
"controllerManager",
"scheduler"
]

tags = merge(
var.common_tags,
{
Name = "${var.environment}-eks-cluster"
}
)

depends_on = [
aws_iam_role_policy_attachment.cluster_policy,
aws_iam_role_policy_attachment.vpc_resource_controller
]
}

resource "aws_eks_node_group" "main" {
cluster_name = aws_eks_cluster.main.name
node_group_name = "${var.environment}-node-group"
node_role_arn = aws_iam_role.node.arn
subnet_ids = var.subnet_ids

instance_types = var.node_instance_types

scaling_config {
desired_size = var.desired_nodes
max_size = var.max_nodes
min_size = var.min_nodes
}

update_config {
max_unavailable = 1
}

labels = {
Environment = var.environment
}

tags = merge(
var.common_tags,
{
Name = "${var.environment}-node-group"
}
)

depends_on = [
aws_iam_role_policy_attachment.node_policy,
aws_iam_role_policy_attachment.cni_policy,
aws_iam_role_policy_attachment.container_registry
]
}

# IAM Role for EKS Cluster
resource "aws_iam_role" "cluster" {
name = "${var.environment}-eks-cluster-role"

assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "eks.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}

resource "aws_iam_role_policy_attachment" "cluster_policy" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
role = aws_iam_role.cluster.name
}

resource "aws_iam_role_policy_attachment" "vpc_resource_controller" {
policy_arn = "arn:aws:iam::aws:policy/AmazonEKSVPCResourceController"
role = aws_iam_role.cluster.name
}

# Security Group for EKS Cluster
resource "aws_security_group" "cluster" {
name = "${var.environment}-eks-cluster-sg"
description = "Security group for EKS cluster"
vpc_id = var.vpc_id

egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}

tags = merge(
var.common_tags,
{
Name = "${var.environment}-eks-cluster-sg"
}
)
}

4. Environment-Specific Configuration

# environments/production/main.tf
terraform {
required_version = ">= 1.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}

backend "s3" {
bucket = "myapp-terraform-state"
key = "production/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}

provider "aws" {
region = var.aws_region

default_tags {
tags = {
Environment = "production"
ManagedBy = "Terraform"
Project = "myapp"
}
}
}

module "networking" {
source = "../../modules/networking"

environment = "production"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]

common_tags = {
Environment = "production"
Project = "myapp"
}
}

module "kubernetes" {
source = "../../modules/kubernetes"

environment = "production"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
kubernetes_version = "1.28"
node_instance_types = ["t3.large"]
desired_nodes = 3
min_nodes = 3
max_nodes = 10

common_tags = {
Environment = "production"
Project = "myapp"
}
}

module "database" {
source = "../../modules/database"

environment = "production"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
instance_class = "db.r5.xlarge"
allocated_storage = 100
multi_az = true
backup_retention = 30

common_tags = {
Environment = "production"
Project = "myapp"
}
}

# environments/production/terraform.tfvars
aws_region = "us-west-2"

# Network configuration
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"]

# Kubernetes configuration
kubernetes_version = "1.28"
node_instance_types = ["t3.large"]
desired_nodes = 3
min_nodes = 3
max_nodes = 10

# Database configuration
db_instance_class = "db.r5.xlarge"
db_allocated_storage = 100
db_multi_az = true
db_backup_retention_period = 30

Terraform CI/CD Pipeline

pipeline {
agent any

environment {
AWS_CREDENTIALS = credentials('aws-credentials')
ENVIRONMENT = "${params.ENVIRONMENT}"
}

parameters {
choice(
name: 'ENVIRONMENT',
choices: ['dev', 'staging', 'production'],
description: 'Target environment'
)
choice(
name: 'ACTION',
choices: ['plan', 'apply', 'destroy'],
description: 'Terraform action'
)
}

stages {
stage('Terraform Init') {
steps {
dir("terraform/environments/${ENVIRONMENT}") {
sh '''
terraform init \
-backend-config="bucket=myapp-terraform-state" \
-backend-config="key=${ENVIRONMENT}/terraform.tfstate"
'''
}
}
}

stage('Terraform Validate') {
steps {
dir("terraform/environments/${ENVIRONMENT}") {
sh 'terraform validate'
}
}
}

stage('Terraform Plan') {
steps {
dir("terraform/environments/${ENVIRONMENT}") {
sh '''
terraform plan \
-out=tfplan \
-var-file=terraform.tfvars
'''
}
}
}

stage('Approval') {
when {
expression { params.ACTION == 'apply' || params.ACTION == 'destroy' }
}
steps {
script {
def plan = readFile("terraform/environments/${ENVIRONMENT}/tfplan")
input message: "Review Terraform plan for ${ENVIRONMENT}. Proceed?",
parameters: [text(name: 'PLAN', description: 'Terraform Plan', defaultValue: plan)]
}
}
}

stage('Terraform Apply') {
when {
expression { params.ACTION == 'apply' }
}
steps {
dir("terraform/environments/${ENVIRONMENT}") {
sh 'terraform apply -auto-approve tfplan'
}
}
}

stage('Terraform Destroy') {
when {
expression { params.ACTION == 'destroy' }
}
steps {
dir("terraform/environments/${ENVIRONMENT}") {
sh 'terraform destroy -auto-approve -var-file=terraform.tfvars'
}
}
}
}

post {
always {
archiveArtifacts artifacts: 'terraform/environments/**/*.tfplan', allowEmptyArchive: true
}
success {
slackSend(
channel: '#infrastructure',
color: 'good',
message: "✅ Terraform ${params.ACTION} completed for ${ENVIRONMENT}"
)
}
failure {
slackSend(
channel: '#infrastructure',
color: 'danger',
message: "❌ Terraform ${params.ACTION} failed for ${ENVIRONMENT}"
)
}
}
}

Configuration Management

Configuration management ensures consistent configuration across environments while allowing environment-specific overrides.

Environment Configuration Strategy

1. Configuration Hierarchy

# config/default.yml
application:
name: myapp
version: 1.0.0

server:
port: 8080
timeout: 30000

database:
pool:
min: 2
max: 10
timeout: 5000

logging:
level: info
format: json

features:
new_ui: false
beta_features: false
# config/production.yml
server:
port: 80
timeout: 60000
workers: 4

database:
pool:
min: 10
max: 50
ssl: true
replication:
enabled: true
read_replicas: 3

logging:
level: warn

features:
new_ui: true
beta_features: false

monitoring:
enabled: true
sampling_rate: 0.1

2. Configuration Loading

// config-loader.js
const fs = require('fs');
const yaml = require('js-yaml');
const _ = require('lodash');

class ConfigLoader {
constructor() {
this.config = {};
this.environment = process.env.NODE_ENV || 'development';
}

load() {
// Load default configuration
const defaultConfig = this.loadFile('config/default.yml');

// Load environment-specific configuration
const envConfig = this.loadFile(`config/${this.environment}.yml`);

// Merge configurations (environment overrides default)
this.config = _.merge({}, defaultConfig, envConfig);

// Override with environment variables
this.applyEnvironmentVariables();

// Validate configuration
this.validate();

return this.config;
}

loadFile(filepath) {
try {
const content = fs.readFileSync(filepath, 'utf8');
return yaml.load(content);
} catch (error) {
console.error(`Failed to load config file ${filepath}:`, error);
return {};
}
}

applyEnvironmentVariables() {
// Override specific values with environment variables
const envMappings = {
'DATABASE_URL': 'database.url',
'DATABASE_PASSWORD': 'database.password',
'REDIS_URL': 'cache.url',
'LOG_LEVEL': 'logging.level',
'FEATURE_NEW_UI': 'features.new_ui'
};

Object.entries(envMappings).forEach(([envVar, configPath]) => {
if (process.env[envVar]) {
_.set(this.config, configPath, this.parseValue(process.env[envVar]));
}
});
}

parseValue(value) {
// Parse boolean strings
if (value === 'true') return true;
if (value === 'false') return false;

// Parse numbers
if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d+\.\d+$/.test(value)) return parseFloat(value);

return value;
}

validate() {
const required = [
'application.name',
'server.port',
'database.url'
];

const missing = required.filter(path => !_.has(this.config, path));

if (missing.length > 0) {
throw new Error(`Missing required configuration: ${missing.join(', ')}`);
}
}

get(path, defaultValue = null) {
return _.get(this.config, path, defaultValue);
}
}

module.exports = new ConfigLoader();

Secrets Management

1. HashiCorp Vault Integration

// vault-client.js
const vault = require('node-vault');

class VaultClient {
constructor() {
this.client = vault({
apiVersion: 'v1',
endpoint: process.env.VAULT_ADDR,
token: process.env.VAULT_TOKEN
});
}

async getSecret(path) {
try {
const response = await this.client.read(path);
return response.data;
} catch (error) {
console.error(`Failed to read secret from ${path}:`, error);
throw error;
}
}

async getDatabaseCredentials(environment) {
const path = `secret/data/${environment}/database`;
const secret = await this.getSecret(path);

return {
host: secret.host,
port: secret.port,
username: secret.username,
password: secret.password,
database: secret.database
};
}

async getAPIKeys(environment) {
const path = `secret/data/${environment}/api-keys`;
return await this.getSecret(path);
}

async rotateSecret(path, newValue) {
try {
await this.client.write(path, { data: newValue });
console.log(`Secret rotated successfully at ${path}`);
} catch (error) {
console.error(`Failed to rotate secret at ${path}:`, error);
throw error;
}
}
}

module.exports = new VaultClient();

2. AWS Secrets Manager Integration

// aws-secrets-manager.js
const AWS = require('aws-sdk');

class SecretsManager {
constructor() {
this.client = new AWS.SecretsManager({
region: process.env.AWS_REGION || 'us-west-2'
});
}

async getSecret(secretName) {
try {
const response = await this.client.getSecretValue({
SecretId: secretName
}).promise();

if ('SecretString' in response) {
return JSON.parse(response.SecretString);
} else {
const buff = Buffer.from(response.SecretBinary, 'base64');
return buff.toString('ascii');
}
} catch (error) {
console.error(`Failed to retrieve secret ${secretName}:`, error);
throw error;
}
}

async createSecret(secretName, secretValue) {
try {
await this.client.createSecret({
Name: secretName,
SecretString: JSON.stringify(secretValue)
}).promise();

console.log(`Secret ${secretName} created successfully`);
} catch (error) {
console.error(`Failed to create secret ${secretName}:`, error);
throw error;
}
}

async updateSecret(secretName, newValue) {
try {
await this.client.updateSecret({
SecretId: secretName,
SecretString: JSON.stringify(newValue)
}).promise();

console.log(`Secret ${secretName} updated successfully`);
} catch (error) {
console.error(`Failed to update secret ${secretName}:`, error);
throw error;
}
}

async rotateSecret(secretName) {
try {
await this.client.rotateSecret({
SecretId: secretName,
RotationLambdaARN: process.env.ROTATION_LAMBDA_ARN
}).promise();

console.log(`Secret rotation initiated for ${secretName}`);
} catch (error) {
console.error(`Failed to rotate secret ${secretName}:`, error);
throw error;
}
}
}

module.exports = new SecretsManager();

Kubernetes ConfigMaps and Secrets

1. ConfigMap Management

# configmap-dev.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: myapp-config
namespace: dev
data:
app.config: |
{
"environment": "dev",
"logLevel": "debug",
"features": {
"newUI": true,
"betaFeatures": true
},
"monitoring": {
"enabled": true,
"samplingRate": 1.0
}
}
nginx.conf: |
server {
listen 80;
server_name localhost;

location / {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}

location /health {
access_log off;
return 200 "healthy\n";
}
}
# secret-dev.yaml
apiVersion: v1
kind: Secret
metadata:
name: myapp-secrets
namespace: dev
type: Opaque
stringData:
database-url: "postgresql://user:password@postgres:5432/myapp_dev"
redis-url: "redis://redis:6379"
api-key: "dev-api-key-12345"
jwt-secret: "dev-jwt-secret-67890"

2. Using ConfigMaps and Secrets in Deployments

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
namespace: dev
spec:
replicas: 2
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: app
image: myapp:latest
ports:
- containerPort: 8080

# Environment variables from ConfigMap
env:
- name: NODE_ENV
value: "dev"
- name: LOG_LEVEL
valueFrom:
configMapKeyRef:
name: myapp-config
key: logLevel

# Environment variables from Secrets
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: myapp-secrets
key: database-url
- name: API_KEY
valueFrom:
secretKeyRef:
name: myapp-secrets
key: api-key

# Mount ConfigMap as volume
volumeMounts:
- name: config
mountPath: /etc/config
readOnly: true
- name: nginx-config
mountPath: /etc/nginx
readOnly: true

volumes:
- name: config
configMap:
name: myapp-config
items:
- key: app.config
path: config.json
- name: nginx-config
configMap:
name: myapp-config
items:
- key: nginx.conf
path: nginx.conf

Environment Consistency

Docker Multi-Stage Builds

# Dockerfile with consistent environments
# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM node:18-alpine

WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001

# Copy built artifacts
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./

# Set user
USER nodejs

# Expose port
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js

# Start application
CMD ["node", "dist/index.js"]

Environment Parity Checklist

# environment-parity-checklist.yml
development:
infrastructure:
- Docker Compose for local services
- Minikube for Kubernetes testing
- LocalStack for AWS services

configuration:
- Same configuration structure as production
- Development-specific values
- Debug logging enabled

dependencies:
- Same versions as production
- Additional development tools

data:
- Anonymized production data
- Seed data for testing

staging:
infrastructure:
- Mirror of production infrastructure
- Scaled down resources
- Separate network/VPC

configuration:
- Production-like configuration
- Staging-specific endpoints
- Enhanced logging

dependencies:
- Exact production versions
- No development dependencies

data:
- Sanitized production data
- Recent data snapshots

production:
infrastructure:
- Full production setup
- High availability configuration
- Auto-scaling enabled

configuration:
- Production configuration
- Optimized settings
- Error logging only

dependencies:
- Locked versions
- Security-hardened

data:
- Live production data
- Regular backups

Key Takeaways

Environment Management Best Practices

  1. Infrastructure as Code: Version control all infrastructure
  2. Configuration Management: Centralize and standardize configuration
  3. Secrets Management: Secure sensitive data with proper tools
  4. Environment Parity: Maintain consistency across environments
  5. Automation: Automate environment provisioning and updates

Implementation Strategy

  • Start with IaC: Define infrastructure as code first
  • Centralize Configuration: Use configuration management tools
  • Secure Secrets: Implement proper secrets management
  • Test Environments: Validate changes in lower environments
  • Document Everything: Maintain clear documentation

Common Patterns

  • Environment-Specific Overrides: Base config + environment overrides
  • Secrets Rotation: Regular rotation of sensitive credentials
  • Infrastructure Testing: Test infrastructure changes before applying
  • Environment Isolation: Strong network and access isolation
  • Configuration Validation: Validate configs before deployment

Next Steps: Ready to master release management? Continue to Section 5.3: Release Management to learn about version control, feature flags, and release coordination.


Effective environment management ensures consistency, security, and reliability across all stages of the software delivery pipeline.