DevOpsIndex

AWS Messaging, Serverless & Observability

SQS, SNS, EventBridge, Lambda, CloudWatch, and multi-account patterns.


SQS (Simple Queue Service)

Managed message queue — decouples producers from consumers. Messages are pulled by consumers.

Standard vs FIFO

Standard Queue FIFO Queue
Ordering Best-effort (not guaranteed) Strict FIFO per message group
Delivery At-least-once (duplicates possible) Exactly-once processing
Throughput Unlimited 300 msg/s (3000 with batching)
Use case High-throughput, order doesn't matter Financial transactions, order processing
Deduplication No (handle in consumer) Content-based or deduplication ID

Key Properties

Visibility timeout: When a consumer receives a message, it becomes invisible to other consumers for the timeout duration (default 30s). If not deleted before timeout expires, message re-appears. Set timeout > max processing time.

Dead Letter Queue (DLQ): After maxReceiveCount failed deliveries, move to DLQ for inspection. Essential for debugging poison-pill messages.

sequenceDiagram
    participant P as Producer
    participant Q as SQS Queue
    participant C as Consumer (Lambda/EC2)
    participant DLQ as Dead Letter Queue

    P->>Q: SendMessage
    C->>Q: ReceiveMessage (long poll)
    Q-->>C: Message (now invisible, 30s timeout)
    C->>C: Process message
    alt success
        C->>Q: DeleteMessage
    else failure (3 attempts)
        Q->>DLQ: Move to DLQ after maxReceiveCount
    end

Long polling: WaitTimeSeconds=20 — consumer waits up to 20s for a message instead of returning empty immediately. Reduces API calls and cost by ~95%.

Lambda + SQS (Event Source Mapping)

Lambda polls SQS automatically (you don't write polling code). Lambda scales based on queue depth — up to 1000 concurrent Lambda invocations per queue.

Queue depth: 1000 msgs → Lambda scales to process in parallel batches
Batch size: 1-10000 messages per invocation

If Lambda throws, batch goes back to queue → retried → eventually hits DLQ.


SNS (Simple Notification Service)

Managed pub/sub — one message fans out to many subscribers. Push model (vs SQS pull).

graph TD
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef orange fill:#e67e22,stroke:#d35400,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef teal fill:#1abc9c,stroke:#16a085,color:#fff

    PUBLISHER["Publisher (app / CloudWatch alarm)"]:::blue
    TOPIC["SNS Topic"]:::orange
    SQS1["SQS Queue: order-processing"]:::teal
    SQS2["SQS Queue: analytics"]:::teal
    LAMBDA["Lambda: send email"]:::green
    HTTP["HTTP endpoint: webhook"]:::blue
    EMAIL["Email"]:::blue

    PUBLISHER --> TOPIC
    TOPIC --> SQS1
    TOPIC --> SQS2
    TOPIC --> LAMBDA
    TOPIC --> HTTP
    TOPIC --> EMAIL

SNS + SQS fan-out pattern: SNS topic → multiple SQS queues. Each queue has its own consumer processing the same event independently. This is the standard pattern for event-driven architectures.

Message filtering: Subscribers can filter messages by attributes, so each subscriber only receives relevant events:

// SQS subscription filter: only receive order events for region "us-east"
{
  "region": ["us-east"],
  "eventType": ["order.placed", "order.cancelled"]
}

EventBridge

Serverless event bus — more powerful than SNS for routing. Native integration with 200+ AWS services as sources.

graph LR
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef orange fill:#e67e22,stroke:#d35400,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef teal fill:#1abc9c,stroke:#16a085,color:#fff

    S3["S3 event"]:::blue
    EC2["EC2 state change"]:::orange
    CUSTOM["Custom app event"]:::teal
    SCHEDULED["Scheduled rule (cron)"]:::blue

    BUS["EventBridge Event Bus"]:::orange

    LAMBDA["Lambda"]:::green
    SQS["SQS"]:::blue
    SF["Step Functions"]:::teal
    API["API Gateway"]:::blue
    CROSS["Cross-account/region bus"]:::orange

    S3 --> BUS
    EC2 --> BUS
    CUSTOM --> BUS
    SCHEDULED --> BUS

    BUS -->|"content-based filter rule"| LAMBDA
    BUS --> SQS
    BUS --> SF
    BUS --> API
    BUS --> CROSS

SNS vs SQS vs EventBridge

SQS SNS EventBridge
Model Queue (pull) Pub/Sub (push) Event bus (push)
Ordering FIFO option No No
Persistence Yes (up to 14 days) No (fire and forget) No
Filtering No Attribute filter Rich content-based filter
Sources Application Application 200+ AWS services + custom
Replay No (archive manually) No Yes (archive + replay)
Cross-account With resource policy Yes Yes (event bus)
Use case Work queues, job buffering Fan-out notifications Event-driven automation, service integration

Rule of thumb:

  • Need to buffer / retry / process asynchronously → SQS
  • Need fan-out to multiple consumers → SNS (or SNS→SQS)
  • Reacting to AWS service events or complex routing → EventBridge

Lambda

Serverless compute — run code in response to events, pay per 100ms of execution.

Execution Model

sequenceDiagram
    participant T as Trigger (API GW / SQS / S3)
    participant LM as Lambda Service
    participant ENV as Execution Environment

    T->>LM: invoke
    alt warm start (env reused)
        LM->>ENV: reuse existing environment
        ENV-->>LM: handler runs ~ms
    else cold start (new env)
        LM->>ENV: provision micro-VM (Firecracker)
        ENV->>ENV: download code, init runtime, run init code
        ENV-->>LM: handler runs (~100ms-2s cold start)
    end
    LM-->>T: response

Cold start optimization:

  • Use Provisioned Concurrency (keeps N envs warm) for latency-sensitive paths
  • Minimize package size (smaller = faster cold start)
  • Avoid VPC attachment unless necessary (VPC adds ~1-2s historically, now much faster with hyperplane)
  • Move initialization outside the handler function (DB connections, SDK clients)
// WRONG: new DB connection every invocation
func Handler(ctx context.Context, event events.APIGatewayProxyRequest) {
    db := sql.Open("postgres", os.Getenv("DB_URL"))  // cold start every time
    defer db.Close()
    ...
}

// RIGHT: initialize once, reuse across warm invocations
var db *sql.DB
func init() {
    db, _ = sql.Open("postgres", os.Getenv("DB_URL"))
}
func Handler(ctx context.Context, event events.APIGatewayProxyRequest) {
    // db is reused across warm invocations
}

Lambda Concurrency

Account limit: 1000 concurrent (soft, can raise)
Reserved concurrency: guarantee N for a function, cap its max
Provisioned concurrency: pre-warmed environments (avoids cold start)

Burst limit: 3000 initial burst, then +500/minute

Throttling: When concurrency limit hit, Lambda returns 429. SQS event source mapping retries; API Gateway returns 429 to caller.

Lambda Destinations

On async invocations, route successes/failures to SQS, SNS, EventBridge, or another Lambda:

Lambda async invoke
  → success → SNS topic: order-success
  → failure → SQS DLQ: order-failures

Cleaner than wrapping everything in try/catch for async flows.

Lambda in VPC

Lambda can access private resources (RDS, ElastiCache) inside a VPC via ENI injection:

Lambda function (invocation)
  → attaches to VPC subnet via Hyperplane ENI
  → private IP in your subnet
  → can reach RDS on 10.0.10.50:5432
  → outbound internet: via NAT Gateway (needs private subnet + NAT)

Gotcha: Lambda in VPC with no VPC endpoint for S3/DynamoDB will route through NAT GW — add VPC endpoints to avoid NAT costs.


CloudWatch

Metrics, Logs, Alarms

graph TD
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef orange fill:#e67e22,stroke:#d35400,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
    classDef red fill:#e74c3c,stroke:#c0392b,color:#fff

    EC2["EC2 / Lambda / RDS / EKS"]:::blue
    METRICS["CW Metrics"]:::orange
    LOGS["CW Logs"]:::teal
    INSIGHTS["Logs Insights"]:::blue
    ALARM["CW Alarm"]:::red
    DASH["Dashboard"]:::green
    SNS2["SNS --> PagerDuty / Lambda"]:::orange

    EC2 -->|"PutMetricData"| METRICS
    EC2 -->|"log agent / SDK"| LOGS
    METRICS --> ALARM
    METRICS --> DASH
    LOGS --> INSIGHTS
    ALARM -->|"ALARM state"| SNS2

Log groups and retention: By default, logs never expire (expensive). Always set retention:

aws logs put-retention-policy \
  --log-group-name /aws/lambda/my-function \
  --retention-in-days 30

CloudWatch Logs Insights Query

# Error rate in last 1 hour
fields @timestamp, @message
| filter @message like /ERROR/
| stats count(*) as errors by bin(5m)
| sort @timestamp asc

Custom Metrics

// Emit custom metric from application
svc := cloudwatch.NewFromConfig(cfg)
svc.PutMetricData(ctx, &cloudwatch.PutMetricDataInput{
    Namespace: aws.String("MyApp/Orders"),
    MetricData: []types.MetricDatum{{
        MetricName: aws.String("OrderProcessingTime"),
        Value:      aws.Float64(float64(duration.Milliseconds())),
        Unit:       types.StandardUnitMilliseconds,
        Dimensions: []types.Dimension{{
            Name: aws.String("Environment"), Value: aws.String("prod"),
        }},
    }},
})

Container Insights (EKS)

Deploys a CloudWatch agent DaemonSet to EKS — ships pod/node CPU, memory, network, disk metrics and container logs automatically.

aws eks create-addon \
  --cluster-name my-cluster \
  --addon-name amazon-cloudwatch-observability

Secrets Manager vs Parameter Store

Secrets Manager SSM Parameter Store
Purpose Secrets with rotation Config + secrets (tiered)
Automatic rotation Yes (Lambda-based, built-in for RDS, Redshift) No
Versioning Yes Yes
Cost $0.40/secret/month + $0.05/10k API calls Free (Standard tier), $0.05/advanced param/month
Max size 64 KB 4 KB (standard), 8 KB (advanced)
KMS encryption Always encrypted Optional (SecureString type)
Cross-account Yes (resource policy) No
Use case DB passwords, API keys needing rotation App config, feature flags, non-rotating secrets

In EKS: Use External Secrets Operator or Secrets Store CSI Driver to mount Secrets Manager / Parameter Store values as Kubernetes secrets or volume files.


Multi-Account Strategy

Single AWS account for everything is an anti-pattern at scale. AWS Organizations provides a hierarchy for governance.

graph TD
    classDef blue fill:#3498db,stroke:#2980b9,color:#fff
    classDef orange fill:#e67e22,stroke:#d35400,color:#fff
    classDef green fill:#2ecc71,stroke:#27ae60,color:#fff
    classDef teal fill:#1abc9c,stroke:#16a085,color:#fff
    classDef red fill:#e74c3c,stroke:#c0392b,color:#fff
    classDef dark fill:#2c3e50,stroke:#1a252f,color:#fff

    ROOT["Management Account<br/>Organizations · billing · SCPs"]:::dark

    subgraph Security["Security OU"]
        SEC["Security Tooling<br/>GuardDuty · Security Hub"]:::red
        LOG["Log Archive<br/>CloudTrail · Flow Logs"]:::orange
    end

    subgraph Infra["Infrastructure OU"]
        NET["Network Account<br/>TGW · shared VPCs"]:::blue
        TOOLS["Shared Services<br/>CI/CD · artifact registry"]:::teal
    end

    subgraph Workloads["Workloads OU"]
        PROD["Prod Account"]:::green
        STAGING["Staging Account"]:::blue
        DEV["Dev Account(s)"]:::teal
    end

    ROOT --> Security
    ROOT --> Infra
    ROOT --> Workloads

Service Control Policies (SCPs)

SCPs are IAM policies attached to OUs/accounts in Organizations. They restrict the maximum permissions any IAM entity in that account can have — even the root user.

// SCP: prevent anyone from disabling GuardDuty
{
  "Statement": [{
    "Effect": "Deny",
    "Action": [
      "guardduty:DeleteDetector",
      "guardduty:DisassociateFromMasterAccount"
    ],
    "Resource": "*"
  }]
}

SCPs do NOT grant permissions — they only set the ceiling. An IAM policy must still exist to actually allow access.

Cross-Account Role Assumption Pattern

sequenceDiagram
    participant CI as CI/CD (Tools Account)
    participant STS as AWS STS
    participant PROD as Prod Account Role

    CI->>STS: AssumeRole arn:aws:iam::PROD:role/deploy-role
    STS->>STS: verify CI role has sts:AssumeRole permission
    STS->>STS: verify deploy-role trust policy allows CI account
    STS-->>CI: Temporary credentials (15min - 12hr)
    CI->>PROD: API calls with temp credentials

The trust policy on the target role is the door; the identity policy on the caller is the key. Both must match.


Cost Optimization Patterns

Compute

Strategy Savings
Savings Plans (1 or 3 yr, flexible) Up to 66% vs On-Demand
Reserved Instances (1 or 3 yr, specific type) Up to 72%
Spot Instances (interruptible, 2-min notice) Up to 90%
Graviton (ARM) instances 20% better price/perf than x86
Right-sizing (use Compute Optimizer) 20-30% typical savings

EKS + Spot: Run stateless workloads on Spot node groups with Karpenter. Karpenter selects cheapest available instance type/AZ dynamically.

Data Transfer (biggest surprise bill)

Intra-AZ:       Free
Inter-AZ:       $0.01/GB each direction  ← biggest hidden cost
Internet out:   $0.09/GB (first 10 TB)
S3/DynamoDB via VPC endpoint: Free (saves NAT GW charges)
CloudFront → origin: cheaper than direct internet

Minimize inter-AZ traffic: Keep pod-to-pod communication within the same AZ using topology-aware routing in K8s (topologySpreadConstraints, trafficPolicy: Local).

Tagging strategy for cost allocation

aws:createdBy: terraform
team: payments
env: prod
service: order-api
cost-center: engineering-platform

Use AWS Cost Explorer + tag-based cost allocation reports. Enforce tags with SCPs or AWS Config rules.