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
- Infrastructure as Code: Version control all infrastructure
- Configuration Management: Centralize and standardize configuration
- Secrets Management: Secure sensitive data with proper tools
- Environment Parity: Maintain consistency across environments
- 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.