AWS SAA-C03: Management, Operations, Observability & Cost — Study Guide
Part of the AWS SAA-C03 Complete Study Guide. Practice these concepts with verified answers in the AWS/Amazon exam hub, or take timed practice tests on ExamRoll.io.
CloudWatch: Metrics, Namespaces, Dashboards, and Alarms
CloudWatch is the default telemetry plane for AWS services, but its utility depends entirely on knowing which namespace a service publishes into. A namespace is a container for metrics that prevents collisions between services — AWS/Lambda holds Invocations, Errors, and Throttles for functions, while AWS/Events holds Invocations, FailedInvocations, TriggeredRules, and MatchedEvents for EventBridge rules. This distinction matters when diagnosing event-driven pipelines. If an EventBridge rule invokes a third-party API through an API destination and no traffic arrives downstream, the answer is not in AWS/Lambda; it is in AWS/Events. Checking TriggeredRules tells you whether the rule pattern actually matched, and Invocations/FailedInvocations tell you whether the target itself was called and whether the call succeeded. Searching Lambda metrics because Lambda is the more familiar service misses the fact that the rule may never have matched an incoming event.
Metric resolution is a per-resource setting with cost implications. Basic monitoring emits metrics every five minutes at no additional charge, which is sufficient for long-running steady-state workloads but far too coarse for autoscaling reactions, spike detection, or SLO calculations that require per-minute resolution. Detailed monitoring drops that to one minute (and to one second for custom high-resolution metrics), which is what you enable when scaling groups need to react quickly. It is a paid feature — which is why it is not on by default.
Every service emits a default set of metrics natively, but the hypervisor cannot see inside the guest OS. CPUUtilization, NetworkIn, and DiskReadOps are visible for EC2 without effort; memory utilization and filesystem-used percentages require the CloudWatch agent, which is the only way to obtain them. Custom application metrics also arrive via the agent or via PutMetricData.
Alarms should distinguish signal from noise. A single alarm on CPU > 50% fires constantly during normal bursts and trains operators to ignore it. Composite alarms combine child alarm states with a Boolean rule expression so operators are paged only when a genuinely actionable condition holds. For a workload where transient CPU spikes are benign but sustained CPU pressure combined with elevated disk read IOPS indicates a real issue:
HighCPUAlarm:
MetricName: CPUUtilization
Threshold: 50
EvaluationPeriods: 3
Period: 60
ComparisonOperator: GreaterThanThreshold
HighDiskReadAlarm:
MetricName: DiskReadOps
Threshold: 1000
EvaluationPeriods: 3
Period: 60
CompositeAlarm:
AlarmRule: >
ALARM("HighCPUAlarm") AND ALARM("HighDiskReadAlarm")
AlarmActions:
- arn:aws:sns:us-east-1:111122223333:ops-pager
Child alarms may still transition to ALARM state internally, but only the composite triggers SNS. Two independent alarms both paging on-call recreates the noise problem; a single alarm at a higher threshold misses the correlation.
Dashboards aggregate metric widgets, log widgets, and text. Two sharing patterns exist and confusing them is a common trap. If a viewer already has an AWS account, grant them an IAM identity (or a role via cross-account observability) with cloudwatch:GetDashboard and cloudwatch:GetMetricData. If the viewer has no AWS account — a product manager, a customer stakeholder — use the built-in dashboard sharing feature, which produces a shareable link protected by a single email/password, a Cognito user pool, or a public URL (rarely appropriate). Emailing screenshots is not observability, and creating an IAM user with console access for a non-AWS viewer is not least-privilege.
Cross-Account Observability with OAM
Hopping between dozens of accounts to view metrics is untenable. CloudWatch cross-account observability designates a monitoring account as a central sink and any number of source accounts that share metrics, logs, and traces with it via sink and link resources. The fastest rollout at scale is to open the CloudWatch console in the monitoring account, create a sink, and deploy the generated CloudFormation StackSet template across the organization to create AWS::Oam::Link resources in each source account:
Resources:
ObservabilityLink:
Type: AWS::Oam::Link
Properties:
LabelTemplate: "$AccountName"
ResourceTypes:
- AWS::CloudWatch::Metric
- AWS::Logs::LogGroup
- AWS::XRay::Trace
SinkIdentifier: arn:aws:oam:us-east-1:111122223333:sink/abc-123
Solving this with cross-account IAM roles and manual queries is technically possible but does not give you unified dashboards, cross-account Metrics Insights queries, or the automatic account-name label enrichment that OAM links provide.
Container Insights and Distributed Tracing
For containerized workloads, Container Insights is the canonical CloudWatch feature. On EKS it deploys the CloudWatch agent (or the ADOT collector) as a DaemonSet plus Fluent Bit for log forwarding. The agent scrapes cAdvisor and kubelet metrics and emits them into the ECS/ContainerInsights and ContainerInsights namespaces (CPU/memory per pod, node, namespace, and cluster), while Fluent Bit ships stdout/stderr into log groups such as /aws/containerinsights/<cluster>/application, /dataplane, and /host. Rolling your own Prometheus/Grafana stack is possible; the managed pattern is Container Insights plus Logs Insights:
fields @timestamp, kubernetes.pod_name, log
| filter kubernetes.namespace_name = "payments"
| filter log like /ERROR/
| stats count() by kubernetes.pod_name
Metrics tell you that something is slow; traces tell you where. X-Ray instruments each service in a request path, propagating a trace ID via the X-Amzn-Trace-Id header and emitting segments and subsegments to the X-Ray daemon or ADOT collector. The service map visualizes nodes and edges with latency, error, and fault percentages. Without X-Ray, a p99 spike surfaces as a CloudWatch metric with no attribution.
from aws_xray_sdk.core import xray_recorder, patch_all
patch_all() # instruments boto3, requests, sqlalchemy, etc.
@xray_recorder.capture('checkout')
def checkout(order_id): ...
Container Insights plus X-Ray plus CloudWatch Logs is the three-legged stool for microservice observability.
The Three Log Streams: CloudTrail, VPC Flow Logs, CloudWatch Logs
Any AWS observability strategy separates three distinct log streams: management-plane API activity (CloudTrail), data-plane network activity (VPC Flow Logs), and application/OS output (CloudWatch Logs). Each answers a different forensic question, and confusing them is a common design error.
CloudTrail records every AWS API call — who invoked it (userIdentity), from which IP, against which resource, with what parameters, and whether it succeeded. It is the only service that reliably ties a mutation back to a specific IAM principal. A common misconception is that CloudWatch Logs is the right place to look for API activity; CloudWatch Logs captures application/system output, not principal-level audit data. CloudTrail can deliver its events to CloudWatch Logs for real-time metric filtering, but the underlying audit record originates in CloudTrail.
In an AWS Organizations environment, the correct pattern is an organization trail created from the management (or delegated administrator) account, which automatically enrolls new member accounts and streams events into a single S3 bucket in a dedicated log-archive account:
CentralTrail:
Type: AWS::CloudTrail::Trail
Properties:
IsOrganizationTrail: true
IsMultiRegionTrail: true
IncludeGlobalServiceEvents: true
EnableLogFileValidation: true # SHA-256 digest chain
S3BucketName: org-cloudtrail-logs
KMSKeyId: !Ref TrailKmsKey
The destination bucket needs versioning, a bucket policy restricting s3:PutObject to the CloudTrail service principal, SSE-KMS encryption, cross-account read only for auditors, and ideally S3 Object Lock in compliance mode for WORM guarantees. Log file validation produces signed digest files so tampering is detectable. Management events are on by default for 90 days; retaining beyond that requires a trail. Data events (S3 object-level, Lambda invoke, DynamoDB item-level) are opt-in due to volume and cost. For ad-hoc historical queries, CloudTrail Lake or Athena against the trail bucket lets you run SQL:
SELECT userIdentity.arn, eventTime, requestParameters
FROM cloudtrail_logs
WHERE eventName = 'AuthorizeSecurityGroupIngress'
AND eventTime BETWEEN '2024-01-08' AND '2024-01-12';
VPC Flow Logs capture metadata about IP traffic — the 5-tuple, bytes, packets, action (ACCEPT/REJECT), and log status — for a VPC, subnet, or ENI. They do not capture payloads. Delivery targets are CloudWatch Logs, S3, or Kinesis Data Firehose. Choose S3 when archiving; choose CloudWatch Logs when you need metric filters to fire alarms on suspicious traffic patterns; choose Firehose when the requirement is near-real-time analytics. The canonical near-real-time pipeline for a VPC with NLBs, ASGs, and databases:
ENIs → VPC Flow Logs (delivery: Kinesis Data Firehose)
→ Firehose delivery stream (optional Lambda transform)
→ Amazon OpenSearch Service (index: vpc-flow-*)
→ OpenSearch Dashboards
The alternative CloudWatch Logs → subscription filter → Firehose → OpenSearch path works but adds a hop and cost. S3 is wrong when the requirement is “near real time.” Not enabling Flow Logs at all is the most damaging trap: when a security incident occurs there is no record of source/destination/port and no ACCEPT vs REJECT visibility. The same reasoning applies to ALB access logs — opt-in, delivered to S3, and without them there is no request-level record of client IPs, response codes, target latencies, or user agents. Together, Flow Logs (L3/L4) and ALB access logs (L7) constitute the traffic forensics baseline.
CloudWatch Logs receives application, Lambda, and OS logs via the CloudWatch agent. Its power comes from metric filters: pattern expressions that scan incoming events and increment a custom metric on match, which then drives an alarm:
# Metric filter detecting inbound SSH sessions from Flow Logs
[version, account, eni, source, dest, srcport, destport=22,
protocol=6, packets, bytes, start, end, action=ACCEPT, status]
A parallel filter on 3389 covers RDP. Log ingestion without alerting produces post-incident forensics, not prevention.
For large fleets the standard is to fan out logs into a processing pipeline via a subscription filter on a log group, streaming matching events to a Kinesis Data Stream, Firehose, or Lambda in near real time:
{
"filterPattern": "",
"destinationArn": "arn:aws:firehose:us-east-1:111122223333:deliverystream/logs-to-os"
}
The trap is sending raw logs to storage without a processing pipeline: dumping to S3 with no Glue catalog, no OpenSearch index, and no Athena workgroup means logs exist but cannot be actioned during an incident. Observability requires a query surface, not just durable bytes. Log groups also need explicit retention policies — the default is “never expire,” which quietly burns money — and can be exported to S3 for long-term archival under lifecycle rules.
API-Level Event Detection with Least Overhead
For high-value control-plane events — CreateImage, AuthorizeSecurityGroupIngress, StopLogging, non-MFA ConsoleLogin — the lowest-overhead pattern is CloudTrail → EventBridge rule → SNS. EventBridge natively receives every CloudTrail management event, and a rule with an event pattern needs no Lambda glue:
{
"source": ["aws.ec2"],
"detail-type": ["AWS API Call via CloudTrail"],
"detail": {
"eventSource": ["ec2.amazonaws.com"],
"eventName": ["CreateImage"]
}
}
Sending CloudTrail to CloudWatch Logs and applying a metric filter also works but adds a log group, filter, alarm, and cost, so it loses on operational overhead when the requirement is simply “alert on API X.”
AWS Config: Continuous Configuration and Drift
Config and CloudTrail are often conflated but answer fundamentally different questions. CloudTrail records who called what; Config records what the resource looks like now and how it changed over time. Expecting Config to log API calls is a classic wrong answer — Config does not know that a user invoked PutBucketAcl; it knows that at 14:03:22 the bucket’s ACL changed from state A to state B. To identify the principal, correlate the Config change record with the CloudTrail event at the same timestamp (Config deep-links to it in the console).
Config rules evaluate resources against desired state. Managed rules cover common checks (restricted-ssh, s3-bucket-public-read-prohibited, required-tags, ec2-instance-no-public-ip, s3-bucket-versioning-enabled, desired-instance-type), and custom rules run as Lambda functions or use CloudFormation Guard. Rules evaluate on configuration change and on a schedule, mark resources COMPLIANT or NON_COMPLIANT, and can trigger automatic remediation via SSM Automation documents. This is the low-operational-overhead answer to “detect open SSH” or “detect oversized instance types” — the rules already exist and integrate natively with SNS and Security Hub. Proposing periodic manual audits, scheduled scans, or hand-rolled scanners requires you to build and maintain code that Config already ships.
Config publishes evaluation results to SNS. To automate remediation, wire an EventBridge rule to the compliance-change event and invoke an SSM Automation document or Lambda:
{
"source": ["aws.config"],
"detail-type": ["Config Rules Compliance Change"],
"detail": {
"configRuleName": ["restricted-ssh"],
"newEvaluationResult": { "complianceType": ["NON_COMPLIANT"] }
}
}
Aggregators consolidate compliance across an organization; conformance packs bundle rules for frameworks like PCI-DSS or HIPAA. Workload Discovery on AWS (formerly AWS Perspective) is a solution built on Config that visualizes resource relationships and generates architecture diagrams — the canonical answer when a question asks for an inventory tool that can diagram an existing environment. Config is a prerequisite.
| Need | Service |
|---|---|
| Who made an API call | CloudTrail |
| Did a resource drift from its baseline | AWS Config |
| Draw me the architecture | Workload Discovery on AWS |
| Is there PII in this bucket | Macie |
| CVEs in EC2/ECR/Lambda | Amazon Inspector |
Security Hub, GuardDuty, and Control Tower
Security Hub is the aggregation plane for security findings. It ingests from GuardDuty (threat detection based on VPC Flow Logs, DNS, and CloudTrail), Inspector (vulnerability findings), Macie, IAM Access Analyzer, and Config, then normalizes everything into the AWS Security Finding Format (ASFF). Enabling Security Hub also enables security standards, most notably the AWS Foundational Security Best Practices (FSBP) standard — dozens of automatic checks (root MFA, public S3, unencrypted EBS, multi-region CloudTrail) mapped to Config rules under the covers.
At organization scale, designate a delegated administrator account for Security Hub (and for GuardDuty and Config), enable the service and FSBP standard for the entire organization with the “auto-enable new accounts” toggle, and route findings via EventBridge to SNS by matching Security Hub Findings - Imported filtered on the FSBP standard ARN with Compliance.Status = FAILED. Rolling your own CloudTrail-parsing Lambda or per-account dashboards adds operational burden that Security Hub already absorbs.
A crucial conceptual split: Security Hub is detective and aggregative, not preventive. Preventing drift is the job of AWS Control Tower, which orchestrates a well-architected multi-account landing zone. Account Factory provisions new accounts with baseline networking, logging, and IAM. Governance is expressed through guardrails of three flavors:
| Guardrail Type | Mechanism | When It Acts |
|---|---|---|
| Preventive | Service Control Policies (SCPs) | Blocks the API call outright |
| Proactive | CloudFormation Hooks | Blocks non-compliant resources at deploy time, before creation |
| Detective | AWS Config rules | Reports drift after the fact |
Proactive controls evaluate CloudFormation templates before a stack deploys and refuse to create, for example, an unencrypted RDS instance. Security Hub would only tell you the unencrypted instance exists after it is running. If a requirement says “prevent,” think Control Tower. If it says “detect, notify, or aggregate,” think Security Hub or Config.
Macie: Sensitive Data Discovery
Macie uses ML and pattern matching to identify sensitive data — PII, financial data, credentials — inside S3 objects. The critical trap is assuming Macie protects data. It does not. Macie discovers and reports. Correct usage:
- Enable Macie in the target account/region.
- Configure a sensitive data discovery job, targeting buckets/prefixes/tags on a schedule.
- Route findings out via EventBridge (
source: aws.macie) to SNS for notifications, Lambda for remediation (quarantine, bucket-policy tightening), or Security Hub.
{
"source": ["aws.macie"],
"detail-type": ["Macie Finding"],
"detail": { "severity": { "description": ["High"] } }
}
Without the discovery job, Macie produces nothing. Without the EventBridge → SNS wiring, findings sit inside the Macie console unnoticed.
Organizations, SCPs, and Tag Policies
AWS Organizations exposes three policy types relevant here. Tag policies define allowed tag keys, casing, and values — they report noncompliance and, combined with aws:ResourceTag/aws:RequestTag conditions, can be enforced. Service Control Policies (SCPs) define the maximum permissions available to member principals. Backup policies and AI opt-out policies round out the set.
A crucial mental model: SCPs never grant permissions. They are a filter over what IAM (identity policies, resource policies, permissions boundaries) can otherwise allow. A principal must have an IAM Allow and the SCP must not Deny (or must include the action in its Allow list). “Just attach an SCP” is never a complete answer to a permissions question — without an IAM Allow, the principal is denied by default regardless of SCP contents.
To require tags at creation, combine tag policies with an SCP such as:
{
"Effect": "Deny",
"Action": ["ec2:RunInstances", "rds:CreateDBInstance"],
"Resource": "*",
"Condition": {
"Null": { "aws:RequestTag/CostCenter": "true" }
}
}
The tag policy declares the schema; the SCP denies creation without the tag; the IAM policy grants the create action. All three are needed. AWS Config’s required-tags rule detects and remediates existing non-compliant resources.
Systems Manager Automation and Patching
Systems Manager (SSM) is the operational spine for lifecycle activities across fleets of EC2 and hybrid nodes. Two constructs matter most for patching: Automation documents (declarative YAML/JSON runbooks calling AWS APIs or scripts) and Maintenance Windows (which schedule those runbooks against tag-based targets or resource groups within a change window with concurrency and error thresholds).
The canonical patching flow is AWS-RunPatchBaseline (a Command document) executing against instances selected by a Patch Group tag, using a Patch Baseline that defines approval rules per OS. For instances behind load balancers, running AWS-RunPatchBaseline directly breaks connections mid-patch because instances remain InService in the target group. The correct pattern is AWSEC2-PatchLoadBalancerInstance, which:
- Deregisters the instance from its CLB or ALB target group.
- Waits for connection draining / deregistration delay.
- Invokes the patch baseline scan and install steps.
- Reboots if the baseline requires it.
- Re-registers the instance and waits for target health to return
healthy.
Two prerequisites cause the “produces errors” failure mode. First, the IAM role passed as AutomationAssumeRole must include elasticloadbalancing:DeregisterTargets, RegisterTargets, and DescribeTargetHealth in addition to standard SSM patching permissions. Second, the instance must be a managed node — SSM Agent running and the instance profile including AmazonSSMManagedInstanceCore. Without these, deregistration calls fail or SSM cannot see the instance.
schemaVersion: '0.3'
description: Patch instance behind ALB
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
InstanceId: { type: String }
TargetGroupArn: { type: String }
AutomationAssumeRole: { type: String }
mainSteps:
- name: deregister
action: aws:executeAwsApi
inputs:
Service: elbv2
Api: DeregisterTargets
TargetGroupArn: '{{ TargetGroupArn }}'
---
---
← Previous: [Security](/posts/saa-c03-security-iam/) · [All domains](/posts/aws-saa-c03-study-guide/) · Next: [High Availability](/posts/saa-c03-ha-dr/) →
**[Practice Management questions →](/kb/amazon/)** · **[Timed practice on ExamRoll.io →](https://www.examroll.io/?utm_source=guide&utm_medium=referral&utm_campaign=saa-c03)**
Pass the whole exam — not just this question
You found this answer. Get every verified question and explanation in one place, and save hours of prep. Free to start.
Pass your exam →