Cloud Linux Engineering
Senior infrastructure engineer designing and operating Linux systems specifically for cloud-native environments, focusing on ephemeral instance lifecycle, cloud-init bootstrapping, IMDSv2 security, and cloud-observable observability patterns.
TL;DR Checklist
- Write cloud-init as idempotent modules (yaml), not shell scripts — re-runnable on every boot
- Enforce IMDSv2 (token-based metadata) — never rely on IMDSv1 (no-token HTTP)
- Design all instances for replacement, not repair — include graceful degradation in shutdown handlers
- Use cloud provider-agnostic patterns where possible, fall back to provider-specific APIs when needed
- Configure cloud observability (logs, metrics, traces) at boot via cloud-init user-data
- Handle spot/preemptible signals with graceful checkpointing and drain logic
- Manage SSH keys via cloud-init, not manual key distribution
When to Use
Use this skill when:
- Designing cloud instance bootstrapping — You need to configure a cloud VM's initial state via cloud-init user-data
- Hardening cloud metadata access — You need to enforce IMDSv2 or restrict metadata API access on cloud instances
- Building for ephemeral infrastructure — You're designing instances that are meant to be replaced, not patched in-place
- Handling spot/preemptible instances — You need graceful shutdown, checkpointing, and drain logic for discounted cloud compute
- Integrating cloud observability — You need to ship logs, metrics, and traces to cloud-native monitoring at boot
- Managing SSH keys at scale — You need to distribute or rotate SSH keys across a fleet of cloud instances
When NOT to Use
Avoid this skill for:
- On-prem or bare-metal Linux setup — Use
hardware-provisioningandlinux-servicesinstead; cloud-init won't run - Configuring cloud VPC or networking at the infrastructure layer — Use
networkingfor bond/VLAN/firewall config; cloud networking is managed by the provider - Tuning kernel parameters — Use
kernel-tuningfor TCP, cgroups, or filesystem tuning after the instance is running - Application-level deployment — Use configuration management (Ansible, Salt, Chef) for application state; cloud-init handles only boot-time configuration
- Persistent VM management — If instances are long-lived and patched in-place, this skill's ephemeral patterns create unnecessary complexity
Core Workflow
1. Design Cloud-Init User-Data
Write cloud-init configurations as YAML modules, prioritizing idempotency and re-runnability.
# Cloud-init user-data: idempotent YAML modules (not shell scripts)
# This runs on every boot — it must be safe to re-execute
cat > /var/lib/cloud/scripts/per-boot/idempotent-setup.sh << 'USERDATA'
#!/bin/bash
# cloud-init per-boot script — runs every boot, must be idempotent
set -euo pipefail
readonly LOG_FILE="/var/log/cloud-init-setup.log"
exec >>"${LOG_FILE}" 2>&1
# Example: ensure application user exists (idempotent)
if ! id "appuser" &>/dev/null; then
useradd -r -s /usr/sbin/nologin -d /opt/app appuser
fi
# Example: ensure directory structure exists (idempotent)
mkdir -p /opt/app/{config,logs,data}
chown -R appuser:appuser /opt/app
chmod 750 /opt/app
# Example: deploy config only if it doesn't exist (idempotent)
if [[ ! -f /opt/app/config/settings.conf ]]; then
cp /opt/app/config/settings.conf.default /opt/app/config/settings.conf
fi
echo "$(date -Iseconds) Boot script completed successfully"
USERDATA
chmod +x /var/lib/cloud/scripts/per-boot/idempotent-setup.sh
Checkpoint: Cloud-init user-data is YAML-based where possible, shell scripts are idempotent (safe to run multiple times), and all operations have guard clauses for existing state.
2. Enforce IMDSv2 (Token-Based Metadata)
Configure all instances to use IMDSv2 and validate token-based metadata access.
#!/bin/bash
# Enforce IMDSv2 on cloud instances
# This works on AWS, GCP, and Azure with provider-specific commands
set -euo pipefail
# --- AWS: Require IMDSv2 at the instance level ---
require_imds_v2_aws() {
local instance_id
instance_id=$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $(curl -s -m 2 -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo "")
if [[ -z "$instance_id" ]]; then
echo "Not an AWS instance or IMDSv2 not enforced yet"
return 1
fi
# Require tokens for metadata access (rejects IMDSv1)
aws ec2 modify-instance-metadata-options \
--instance-id "$instance_id" \
--http-tokens required \
--http-endpoint enabled
echo "IMDSv2 enforced for instance: $instance_id"
}
# --- GCP: Metadata server already requires headers ---
require_imds_v2_gcp() {
local metadata
metadata=$(curl -s -m 2 \
-H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/name 2>/dev/null || echo "")
if [[ -z "$metadata" ]]; then
echo "Not a GCP instance"
return 1
fi
echo "GCP instance detected: $metadata (metadata server already requires headers)"
echo "GCP compute metadata API enforces Metadata-Flavor: Google header"
}
# --- Azure: Metadata requires header since API version 2019-02-01 ---
require_imds_v2_azure() {
local metadata
metadata=$(curl -s -m 2 \
-H "Metadata-Severity: Interactive" \
-H "Authorization: Bearer $(curl -s -m 2 -H 'Metadata-Severity: Interactive' 'http://169.254.169.254/metadata/instance?api-version=2021-02-01' | jq -r '.metadata' 2>/dev/null || echo 'token')" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01" 2>/dev/null | jq -r '.compute.name' 2>/dev/null || echo "")
if [[ -z "$metadata" ]]; then
echo "Not an Azure instance"
return 1
fi
echo "Azure instance detected: $metadata (metadata API requires header since 2019-02-01)"
}
# Detect provider and enforce
detect_provider() {
if curl -s -m 2 -H "X-aws-ec2-metadata-token: test" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null | grep -q '^i-'; then
echo "aws"
elif curl -s -m 2 -H "Metadata-Flavor: Google" http://metadata.google.internal/computeMetadata/v1/instance/name 2>/dev/null | grep -q '^'; then
echo "gcp"
elif curl -s -m 2 -H "Metadata-Severity: Interactive" http://169.254.169.254/metadata/instance?api-version=2021-02-01 2>/dev/null | grep -q '"compute"'; then
echo "azure"
else
echo "bare-metal"
fi
}
PROVIDER=$(detect_provider)
echo "Detected provider: $PROVIDER"
case "$PROVIDER" in
aws) require_imds_v2_aws ;;
gcp) require_imds_v2_gcp ;;
azure) require_imds_v2_azure ;;
bare-metal) echo "Bare metal — no cloud metadata API to enforce" ;;
esac
# --- Harden: Block IMDSv1 access at the host firewall level ---
# Only allow token-based metadata access (IMDSv2 requires X-aws-ec2-metadata-token header)
# Note: This is Linux-level; cloud provider also enforces at the hypervisor layer
sudo iptables -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 \
-m conntrack --ctstate NEW -m string --string "PUT" --algo bm -j ACCEPT
sudo iptables -A OUTPUT -d 169.254.169.254 -p tcp --dport 80 \
-m conntrack --ctstate NEW -j DROP
Checkpoint: IMDSv2 is enforced at the cloud provider level (metadata-options) AND Linux-level firewall blocks unauthenticated metadata requests. Instance metadata is only accessible with valid tokens.
3. Design for Ephemeral Instance Lifecycle
Implement shutdown handlers and graceful degradation for replaceable instances.
#!/bin/bash
# Graceful shutdown handler for ephemeral cloud instances
# Registered as a systemd service that runs on SIGTERM/shutdown
# Usage: cp shutdown-handler.service /etc/systemd/system/ && systemctl daemon-reload
set -euo pipefail
# This script runs on instance shutdown — must complete quickly (<30s)
SHUTDOWN_LOG="/var/log/instance-shutdown.log"
LOCK_FILE="/run/shutdown-handler.lock"
readonly SHUTDOWN_LOG LOCK_FILE
# Prevent concurrent shutdown runs
exec 200>"$LOCK_FILE"
flock -n 200 || { echo "Shutdown handler already running" >&2; exit 1; }
log() {
echo "$(date -Iseconds) $*" | tee -a "$SHUTDOWN_LOG"
}
# Phase 1: Stop accepting new work
log "Phase 1: Draining load balancer traffic"
# Remove from load balancer (AWS example — adapt for provider)
INSTANCE_ID=$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $(curl -s -m 2 -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo "unknown")
if [[ "$INSTANCE_ID" != "unknown" ]]; then
# Deregister from target group (non-blocking, fire-and-forget)
aws elbv2 deregister-targets \
--target-group-arn "arn:aws:elasticloadbalancing:$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $(curl -s -m 2 -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600') http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null || echo us-east-1)":targets" \
--targets "Id=$INSTANCE_ID" &
log "Deregistered instance $INSTANCE_ID from load balancer"
fi
# Phase 2: Flush connection state
log "Phase 2: Draining active connections"
# Wait for active connections to close (grace period)
sleep 5
# Phase 3: Persist critical state
log "Phase 3: Persisting critical state"
if [[ -d /opt/app/data ]]; then
# Ensure data is flushed to durable storage
sync
# Copy critical state to shared storage if available
if mountpoint -q /mnt/shared-state; then
cp -a /opt/app/data/*.state /mnt/shared-state/ 2>/dev/null || true
fi
fi
log "Shutdown complete. Instance may be terminated."
Checkpoint: Shutdown handler is registered as a systemd service with ExecStopPre and ExecStop, runs within the shutdown timeout, and persists critical state before instance termination.
4. Configure Cloud-Specific Filesystem Layouts
Use cloud-aware device paths and ephemeral storage patterns.
#!/bin/bash
# Cloud filesystem layout detection and configuration
# Cloud providers use predictable device naming via /dev/disk/cloud/*
set -euo pipefail
echo "=== Cloud Filesystem Layout Detection ==="
# --- AWS: EBS volumes and instance store ---
echo "--- EBS Volumes (persistent block storage) ---"
# EBS volumes appear as /dev/xvda, /dev/xvdb, etc.
# Cloud provider symlinks: /dev/disk/cloud/aws_ebs-*
lsblk -o NAME,SIZE,TYPE,MOUNTPOINT,MODEL | grep -E "disk|part" 2>/dev/null || echo "No block devices found"
# EBS-specific symlinks (persistent across reboots and instance stops)
if [[ -d /dev/disk/cloud ]]; then
echo "--- EBS Persistent Symlinks ---"
ls -la /dev/disk/cloud/
fi
# --- AWS: Instance Store (ephemeral NVMe) ---
echo ""
echo "--- Instance Store (ephemeral, lost on stop/terminate) ---"
# Instance store NVMe devices
lsblk -dno NAME,SIZE,TYPE,ROTA | grep -E "disk.*0$" || echo "No instance store devices found"
# Ephemeral storage mount points (AWS convention)
# /mnt — ephemeral storage (instance store)
# /dev/nvme1n1 — instance store (lost on stop)
# /dev/nvme0n1 — root EBS (persistent)
# --- Detect ephemeral vs persistent storage ---
detect_storage_type() {
local dev="$1"
# Check if device is behind EBS (persistent)
if [[ -L "/dev/disk/cloud/aws_ebs_${dev}" ]]; then
echo "persistent-ebs"
# Check if device is NVMe rotational (0 = SSD/NVMe)
elif [[ -f "/sys/block/${dev}/queue/rotational" ]]; then
local rota
rota=$(cat "/sys/block/${dev}/queue/rotational")
if [[ "$rota" == "0" ]]; then
echo "ssd-nvme"
else
echo "hdd"
fi
else
echo "unknown"
fi
}
# --- Mount ephemeral storage safely ---
mount_ephemeral_storage() {
local device="$1"
local mount_point="$2"
# Only mount ephemeral storage on NVMe device (not root)
if [[ "$(basename "$device")" == "nvme0n1" ]]; then
echo "Warning: $device appears to be root device — not mounting as ephemeral"
return 1
fi
mkdir -p "$mount_point"
# mkfs only if not already formatted (idempotent)
if ! blkid "$device" | grep -q 'TYPE='; then
echo "Formatting $device with ext4"
mkfs.ext4 -F "$device"
fi
# Mount with discard for TRIM on NVMe
if ! mountpoint -q "$mount_point"; then
mount -o discard,defaults "$device" "$mount_point"
echo "Mounted $device on $mount_point"
else
echo "$mount_point already mounted"
fi
}
Checkpoint: Ephemeral storage is correctly identified (NVMe vs EBS), mounted with appropriate options, and never used for persistent data. Root volume is always EBS-backed.
5. Configure Cloud Observability at Boot
Ship logs, metrics, and traces to cloud-native observability via cloud-init.
# Cloud-init YAML for observability agent installation
# This runs at first-boot to install and configure monitoring agents
# Install AWS CloudWatch Agent (provider-agnostic pattern: install agent, configure via env)
cat > /var/lib/cloud/scripts/per-boot/install-observability.sh << 'OBSERVABILITY'
#!/bin/bash
set -euo pipefail
# --- CloudWatch Agent (AWS) ---
install_cloudwatch_agent() {
local region
region=$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $(curl -s -m 2 -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600')" http://169.254.169.254/latest/meta-data/placement/region 2>/dev/null || echo "us-east-1")
if command -v amazon-cloudwatch-agent-config-wizard &>/dev/null; then
echo "CloudWatch agent already installed"
return 0
fi
echo "Installing CloudWatch agent for region: $region"
# Install CloudWatch Agent
wget "https://amazoncloudwatch-agent-${region}.s3.amazonaws.com/amazon_linux/amd64/latest/amazon-cloudwatch-agent.rpm" \
-O /tmp/cloudwatch-agent.rpm 2>/dev/null || true
if [[ -f /tmp/cloudwatch-agent.rpm ]]; then
rpm -U /tmp/cloudwatch-agent.rpm
rm /tmp/cloudwatch-agent.rpm
fi
}
# --- CloudWatch Agent Configuration ---
configure_cloudwatch_agent() {
cat > /opt/aws/amazon-cloudwatch-agent/etc/conf.d/cloudwatch_config.json << 'CW_CONFIG'
{
"agent": {
"metrics_collection_interval": 60,
"run_as_user": "root",
"debug": false
},
"metrics": {
"metrics_collected": {
"collectd": {
"metrics_aggregation_interval": 60
},
"cpu": {
"measurement": ["cpu_usage_idle", "cpu_usage_user", "cpu_usage_system"],
"metrics_collection_interval": 60,
"resources": ["*"]
},
"disk": {
"measurement": ["disk_used_percent", "disk_io_time"],
"metrics_collection_interval": 60,
"resources": ["*"]
},
"mem": {
"measurement": ["mem_used_percent", "mem_available"],
"metrics_collection_interval": 60
},
"net": {
"measurement": ["net_bytes_sent", "net_bytes_recv"],
"metrics_collection_interval": 60,
"resources": ["*"]
}
},
"append_dimensions": {
"InstanceId": "${aws:InstanceId}",
"ImageId": "${aws:ImageId}",
"AutoScalingGroupName": "${aws:AutoScalingGroupName}"
}
},
"logs": {
"logs_collected": {
"files": {
"collect_list": [
{
"log_group_name": "/var/log/myapp/{AutoScalingGroupName}",
"log_group_region": "{region}",
"file_path": "/var/log/myapp/*.log"
},
{
"log_group_name": "/var/log/syslog",
"log_group_region": "{region}",
"file_path": "/var/log/syslog"
}
]
}
}
}
}
CW_CONFIG
}
# --- CloudTrail integration (audit logging) ---
configure_cloudtrail_logging() {
# Ensure auditd is running for CloudTrail-compatible audit trails
if command -v auditctl &>/dev/null; then
# Log all privilege escalations
auditctl -a always,exit -F arch=b64 -S execve -k exec_audit
# Log all file access to sensitive paths
auditctl -w /etc/shadow -p rwxa -k shadow_changes
auditctl -w /etc/passwd -p wa -k passwd_changes
auditctl -w /etc/sudoers -p wa -k sudoers_changes
fi
}
install_cloudwatch_agent
configure_cloudwatch_agent
configure_cloudtrail_logging
echo "$(date -Iseconds) Observability setup complete"
OBSERVABILITY
chmod +x /var/lib/cloud/scripts/per-boot/install-observability.sh
Checkpoint: CloudWatch agent (or equivalent) is installed, configured with instance metadata for dimension enrichment, syslog and application logs are collected, and auditd captures security-relevant events.
6. Handle Spot/Preemptible Instance Signals
Implement graceful shutdown, checkpointing, and drain for spot instances.
#!/bin/bash
# Spot instance graceful shutdown handler
# AWS: EC2 Instance Stop/Retirement notifications via CloudWatch Events
# GCP: Preemption events via metadata
# Azure: Eviction/Deallocation notifications via metadata
set -euo pipefail
LOG_FILE="/var/log/spot-handler.log"
CHECKPOINT_DIR="/opt/app/checkpoints"
readonly LOG_FILE CHECKPOINT_DIR
log() {
echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"
}
# --- AWS Spot Instance Interruption Handling ---
# AWS sends 2-minute and 1-minute advance notices via CloudWatch Events
# This script is triggered by the SNS topic subscription on EC2 spot interruption
handle_aws_spot_interruption() {
local message="$1"
local instance_id
instance_id=$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $(curl -s -m 2 -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600')" http://169.254.169.254/latest/meta-data/instance-id 2>/dev/null || echo "unknown")
log "=== AWS Spot Interruption ==="
log "Instance: $instance_id"
log "Message: $message"
# Extract time remaining from message
local event_type
event_type=$(echo "$message" | jq -r '.event' 2>/dev/null || echo "spot-interruption")
local time_remaining
case "$event_type" in
*EC2_Instance-Stop*) time_remaining=120 ;; # Stop
*EC2_Instance-Retirement*) time_remaining=300 ;; # Retirement
*EC2_Spot_Instance-Interruption*) time_remaining=120 ;; # Spot
*) time_remaining=60 ;; # Default: 1 minute
esac
log "Time remaining: ${time_remaining}s"
# Phase 1: Flush application state (0-120s window)
log "Phase 1: Flushing application state to durable storage"
mkdir -p "$CHECKPOINT_DIR"
sync
# Phase 2: Gracefully stop accepting new connections
log "Phase 2: Stopping application"
systemctl stop myapp-service 2>/dev/null || true
# Phase 3: Persist checkpoints
log "Phase 3: Saving checkpoints"
if [[ -d /opt/app/data ]]; then
tar czf "${CHECKPOINT_DIR}/checkpoint-$(date +%Y%m%d-%H%M%S).tar.gz" -C /opt/app/data . 2>/dev/null || true
fi
# Phase 4: Deregister from load balancer
log "Phase 4: Deregistering from load balancer"
if [[ "$instance_id" != "unknown" ]]; then
aws elbv2 deregister-targets \
--target-group-arn "arn:aws:elasticloadbalancing:*:targets" \
--targets "Id=$instance_id" &>/dev/null || true
fi
# Phase 5: Exit — instance will be terminated
log "Spot interruption handled. Exiting gracefully."
exit 0
}
# --- GCP Preemption Handling ---
handle_gcp_preemption() {
log "=== GCP Preemption Detected ==="
# GCP sends SIGTERM with ~30s grace period
# Preemption notice via metadata: /metadata/preempted
local preempted
preempted=$(curl -s -m 2 -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/preempted 2>/dev/null || echo "false")
if [[ "$preempted" == "true" ]]; then
log "Instance is preempted — executing checkpoint and drain"
systemctl stop myapp-service 2>/dev/null || true
sync
exit 0
fi
}
# --- Azure Deallocation Handling ---
handle_azure_deallocation() {
log "=== Azure Deallocation Detected ==="
# Azure sends SIGTERM via custom handler
local deallocation
deallocation=$(curl -s -m 2 \
-H "Metadata-Severity: Interactive" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01" 2>/dev/null | \
jq -r '.compute.vmState' 2>/dev/null || echo "running")
if [[ "$deallocation" == "deallocated" ]]; then
log "VM is being deallocated — executing checkpoint and drain"
systemctl stop myapp-service 2>/dev/null || true
sync
exit 0
fi
}
# --- Generic: CloudWatch event trigger ---
# Set up CloudWatch Events rule to trigger this on spot interruption
# Usage: ./spot-handler.sh aws <sns-message-json>
# Usage: ./spot-handler.sh gcp
# Usage: ./spot-handler.sh azure
case "${1:-}" in
aws) handle_aws_spot_interruption "${2:-{}}" ;;
gcp) handle_gcp_preemption ;;
azure) handle_azure_deallocation ;;
*) echo "Usage: $0 {aws|gcp|azure} [args...]" ;;
esac
Checkpoint: Spot handler is registered as a systemd service triggered by CloudWatch Events (AWS) or metadata polling (GCP/Azure). Checkpoints are saved within the interruption window, and the load balancer is notified before termination.
7. Manage SSH Keys at Scale
Distribute and rotate SSH keys via cloud-init without manual key management.
# Cloud-init user-data: SSH key injection and rotation
# Keys are injected via EC2 User Data or AWS Systems Manager Parameter Store
cat > /var/lib/cloud/scripts/per-boot/manage-ssh-keys.sh << 'SSH_KEYS'
#!/bin/bash
set -euo pipefail
SSH_KEY_DIR="/home/appuser/.ssh"
SSH_KEY_FILE="${SSH_KEY_DIR}/authorized_keys"
# --- Option A: Inject keys from Systems Manager Parameter Store ---
fetch_keys_from_ssm() {
local param_path="$1"
local keys
keys=$(aws ssm get-parameter \
--name "$param_path" \
--with-decryption \
--query 'Parameter.Value' \
--output text 2>/dev/null) || {
echo "Failed to fetch keys from SSM: $param_path" >&2
return 1
}
echo "$keys" > "${SSH_KEY_FILE}.new"
}
# --- Option B: Inject keys from AWS Secrets Manager ---
fetch_keys_from_secrets() {
local secret_id="$1"
local keys
keys=$(aws secretsmanager get-secret-value \
--secret-id "$secret_id" \
--query 'SecretString' \
--output text 2>/dev/null) || {
echo "Failed to fetch keys from Secrets Manager: $secret_id" >&2
return 1
}
echo "$keys" > "${SSH_KEY_FILE}.new"
}
# --- Option C: Inject keys from cloud metadata (instance-specific) ---
fetch_keys_from_metadata() {
local public_key
public_key=$(curl -s -m 2 -H "X-aws-ec2-metadata-token: $(curl -s -m 2 -X PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600')" \
http://169.254.169.254/latest/meta-data/public-keys/0/openssh-key 2>/dev/null) || {
echo "No public key in metadata" >&2
return 1
}
echo "$public_key" > "${SSH_KEY_FILE}.new"
}
# --- Apply SSH keys (atomic, with backup) ---
apply_ssh_keys() {
local source="${1:-metadata}"
# Create SSH directory with strict permissions
mkdir -p "$SSH_KEY_DIR"
chmod 700 "$SSH_KEY_DIR"
case "$source" in
ssm)
fetch_keys_from_ssm "/secure/ssh-keys/fleet" || return 1
;;
secrets)
fetch_keys_from_secrets "ssh-keys/fleet" || return 1
;;
metadata)
fetch_keys_from_metadata || return 1
;;
*)
echo "Unknown source: $source" >&2
return 1
;;
esac
# Atomic swap: validate and replace
if grep -q '^ssh-rsa\|^ssh-ed25519\|^ecdsa-sha2-nistp' "${SSH_KEY_FILE}.new"; then
mv "${SSH_KEY_FILE}.new" "$SSH_KEY_FILE"
chown appuser:appuser "$SSH_KEY_FILE"
chmod 600 "$SSH_KEY_FILE"
echo "SSH keys updated successfully from: $source"
else
rm -f "${SSH_KEY_FILE}.new"
echo "Invalid key format — key not applied" >&2
return 1
fi
}
# --- Key rotation: remove keys not in current rotation ---
rotate_ssh_keys() {
local new_keys_file="$1"
local backup_dir="/etc/ssh-keys/backup"
local timestamp
timestamp=$(date +%Y%m%d-%H%M%S)
# Backup current keys
mkdir -p "$backup_dir"
if [[ -f "$SSH_KEY_FILE" ]]; then
cp "$SSH_KEY_FILE" "${backup_dir}/keys-${timestamp}"
fi
# Atomic replace
mv "$new_keys_file" "$SSH_KEY_FILE"
chown appuser:appuser "$SSH_KEY_FILE"
chmod 600 "$SSH_KEY_FILE"
# Rotate backups: keep last 10
ls -t "${backup_dir}"/keys-* 2>/dev/null | tail -n +11 | xargs rm -f 2>/dev/null || true
echo "SSH keys rotated at $timestamp"
}
# --- Usage: called from cloud-init or systemd timer for periodic rotation ---
apply_ssh_keys "${1:-metadata}"
SSH_KEYS
chmod +x /var/lib/cloud/scripts/per-boot/manage-ssh-keys.sh
# --- Cloud-init YAML for SSH key injection via SSM ---
cat > /etc/cloud/cloud.cfg.d/99-ssh-keys.cfg << 'SSH_CLOUDINIT'
# cloud-init config: SSH key management via SSM Parameter Store
ssh_authorized_keys: [] # Clear default keys — we manage via script
# Run key management at boot
runcmd:
- /var/lib/cloud/scripts/per-boot/manage-ssh-keys.sh ssm
SSH_CLOUDINIT
Checkpoint: SSH keys are managed centrally (SSM/Secrets Manager), injected atomically with proper permissions, rotated with backup retention, and never stored on-disk in plaintext outside the authorized_keys file.
Implementation Patterns
Pattern 1: Cloud-Init User-Data (Shell Script vs. YAML Modules)
# ❌ BAD: Using shell scripts for all cloud-init configuration
# Shell scripts run once and are hard to make idempotent
# They also bypass cloud-init's structured module system
#cloud-config
# This is still a shell script approach — fragile, not modular
runcmd:
- apt-get update && apt-get install -y nginx
- systemctl enable nginx
- echo "server_name myapp;" > /etc/nginx/sites-available/default
# Problems:
# - apt-get install runs on EVERY boot (cloud-init re-executes per-boot scripts)
# - No idempotency — apt-get update fails if network is flaky
# - No structured config management
# - Cannot be validated by cloud-init schema
# ✅ GOOD: Using cloud-init YAML modules (idempotent, structured, validated)
#cloud-config
# Package installation — idempotent, handles dependencies
package_update: true
package_upgrade: false
packages:
- nginx
# File resource — writes files atomically with ownership
write_files:
- path: /etc/nginx/sites-available/default
content: |
server {
listen 80;
server_name myapp;
location / {
proxy_pass http://localhost:8080;
}
}
owner: root:root
permissions: '0644'
# Cloud-config modules are idempotent — safe to re-run on every boot
# Package module: only installs if not already present
# Write_files module: only writes if content differs (uses hash comparison)
# Runcmd: runs only once on first boot (not per-reboot)
# For per-boot idempotent logic, use per-boot scripts with guard clauses
# (see Core Workflow Step 1)
Pattern 2: Provider-Agnostic Instance Detection
# ❌ BAD: Hardcoding AWS assumptions everywhere
INSTANCE_ID=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
REGION=$(aws ec2 describe-instance-attribute --instance-id $INSTANCE_ID --query Groups --output text)
# Problems:
# - No token for IMDSv1 (security risk)
# - AWS CLI not always available
# - Fails on GCP/Azure without modification
# - No error handling
# ✅ GOOD: Provider-agnostic detection with IMDSv2 enforcement
#!/bin/bash
set -euo pipefail
detect_cloud_provider() {
# IMDSv2 token — works for all providers that support token-based metadata
local token=""
# AWS IMDSv2 token
token=$(curl -s -m 2 -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 30" 2>/dev/null) || token=""
# GCP metadata flavor
if curl -s -m 2 -H "Metadata-Flavor: Google" \
http://metadata.google.internal/computeMetadata/v1/instance/name 2>/dev/null | grep -q '^'; then
echo "gcp"
return
fi
# Azure metadata
if curl -s -m 2 -H "Metadata-Severity: Interactive" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01" 2>/dev/null | grep -q '"compute"'; then
echo "azure"
return
fi
# AWS (fallback — if we get here, it's AWS)
if [[ -n "$token" ]]; then
echo "aws"
return
fi
echo "bare-metal"
}
# Usage: provider-agnostic instance attributes
get_instance_attribute() {
local provider
provider=$(detect_cloud_provider)
local attr="$1"
case "$provider" in
aws)
local token
token=$(curl -s -m 2 -X PUT "http://169.254.169.254/latest/api/token" \
-H "X-aws-ec2-metadata-token-ttl-seconds: 30" 2>/dev/null || echo "")
[[ -z "$token" ]] && return 1
curl -s -m 2 -H "X-aws-ec2-metadata-token: $token" \
"http://169.254.169.254/latest/meta-data/$attr"
;;
gcp)
curl -s -m 2 -H "Metadata-Flavor: Google" \
"http://metadata.google.internal/computeMetadata/v1/instance/$attr"
;;
azure)
local token
token=$(curl -s -m 2 -H "Metadata-Severity: Interactive" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01" 2>/dev/null | \
jq -r '.metadata' 2>/dev/null || echo "")
curl -s -m 2 -H "Metadata-Severity: Interactive" \
-H "Authorization: Bearer $token" \
"http://169.254.169.254/metadata/instance?api-version=2021-02-01" | \
jq -r ".compute.$attr" 2>/dev/null
;;
bare-metal)
echo "Not a cloud instance" >&2
return 1
;;
esac
}
# Examples:
get_instance_attribute "instance-id" # AWS: i-0abc123
get_instance_attribute "name" # GCP: my-instance-name
get_instance_attribute "placement/region" # AWS: us-east-1
Pattern 3: Cloud-Init Systemd Service for Observability
# /etc/systemd/system/cloud-observability.service
[Unit]
Description=Cloud Observability Agent Manager
After=network-online.target cloud-final.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/cloud-observability-bootstrap.sh
TimeoutStartSec=120
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
#!/bin/bash
# /usr/local/bin/cloud-observability-bootstrap.sh
# Bootstrap cloud observability agents — idempotent, provider-agnostic
set -euo pipefail
LOG="/var/log/cloud-observability-bootstrap.log"
exec >>"${LOG}" 2>&1
log() { echo "$(date -Iseconds) $*"; }
BOOTSTRAP_MARKER="/var/lib/cloud-observability/bootstrap-done"
# Skip if already bootstrapped (idempotent)
if [[ -f "$BOOTSTRAP_MARKER" ]]; then
log "Already bootstrapped — skipping"
exit 0
fi
log "Starting cloud observability bootstrap"
# Detect provider
PROVIDER=$(detect_cloud_provider 2>/dev/null || echo "unknown")
log "Detected provider: $PROVIDER"
case "$PROVIDER" in
aws)
# Install and configure CloudWatch agent
if ! systemctl is-active --quiet amazon-cloudwatch-agent.service 2>/dev/null; then
log "Installing CloudWatch agent"
install_cloudwatch_agent
configure_cloudwatch_agent
systemctl enable --now amazon-cloudwatch-agent
fi
# Configure auditd for CloudTrail-compatible audit trails
if systemctl is-active --quiet auditd; then
log "Auditd already running"
else
systemctl enable --now auditd
log "Started auditd"
fi
;;
gcp)
# Install Google Cloud Logging agent
if ! systemctl is-active --quiet google-fluentd 2>/dev/null; then
log "Installing Google Cloud Logging agent"
install_gcp_logging_agent
systemctl enable --now google-fluentd
fi
;;
azure)
# Install Azure Monitor agent
if ! systemctl is-active --quiet azuremonitoragent 2>/dev/null; then
log "Installing Azure Monitor agent"
install_azure_monitor_agent
systemctl enable --now azuremonitoragent
fi
;;
*)
log "No cloud provider detected — skipping cloud-specific agents"
;;
esac
# Create marker to prevent re-bootstrap
touch "$BOOTSTRAP_MARKER"
chmod 644 "$BOOTSTRAP_MARKER"
log "Bootstrap complete"
Checkpoint: Bootstrap runs once, is idempotent (checks for existing agent), provider-specific, and creates a marker file to prevent re-execution.
Constraints
MUST DO
- MUST write cloud-init user-data as YAML modules when possible — shell scripts in user-data should be used only for logic that cannot be expressed in YAML
- MUST enforce IMDSv2 on all cloud instances — never access metadata without a token; configure
--http-tokens requiredat the instance level - MUST design every instance as disposable — no in-place upgrades, no persistent state on local storage (except ephemeral caches with TTL)
- MUST ensure all cloud-init scripts are idempotent — they run on every boot, including reboots and recovery
- MUST configure cloud observability at boot time, not after instance is running — gaps in observability are gaps in incident detection
- MUST handle spot/preemptible signals gracefully — implement checkpointing, drain, and load balancer deregistration
- MUST manage SSH keys via centralized secret management (SSM, Secrets Manager, HashiCorp Vault) — never distribute keys manually
- MUST use
/dev/disk/cloud/*symlinks for cloud storage devices — these are persistent across instance stop/start cycles
MUST NOT DO
- MUST NOT use IMDSv1 (unauthenticated metadata endpoint
http://169.254.169.254/latest/meta-data/) — it is vulnerable to SSRF attacks (e.g., ROAST attack) - MUST NOT store persistent data on ephemeral instance store (
/mnt,/dev/nvme*instance store) — this data is lost on stop/terminate - MUST NOT use
runcmdfor package installation — use cloud-init'spackagesmodule instead;runcmdruns every boot and is not idempotent - MUST NOT hardcode cloud provider APIs in application code — use provider-agnostic patterns or abstraction layers
- MUST NOT bypass IMDSv2 enforcement even in development — the same security posture must apply to all environments
- MUST NOT rely on cron-based metadata polling for spot interruption — use CloudWatch Events (AWS), metadata preemption events (GCP), or custom handlers (Azure)
- MUST NOT store SSH private keys on cloud instances — only public keys should be injected; private keys must never leave the key owner's machine
- MUST NOT mount instance store without checking it's not the root device — mounting
/dev/nvme0n1(root) as ephemeral storage destroys the OS
Related Skills
| Skill | Purpose |
|---|---|
linux-services |
Manage systemd service lifecycle alongside cloud-init bootstrapping |
networking |
Configure VPC networking, security groups, and ENI-level networking at the Linux layer |
resource-management |
Configure cgroups and resource limits for cloud workloads |
linux-security |
Apply security hardening that complements IMDSv2 and cloud-specific security controls |
📖 skill(local cache): cloud-linux-engineering