This page is for developers, DevOps and cloud engineers facing an AWS round, from a first cloud job to a senior platform role. Most AWS interviews start with EC2 and S3, then test IAM and least privilege, VPC networking, load balancers and Auto Scaling, and the choice between RDS and DynamoDB. Later rounds add Lambda limits, CloudWatch, CloudFormation, surviving the loss of an Availability Zone and a small design on the whiteboard. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap in your own stories.
Search all questions by round, difficulty and level, or save the ones you want to practise.
Families: general purpose, compute optimized, memory optimized, storage optimized and accelerated with GPUs.
Measure: find the bottleneck first: CPU, memory, disk or network.
Right-size: start small, watch the metrics under real load, then move up or down.
"I start with what the app is short of. EC2 groups instances into families: general purpose for balanced web and app servers, compute optimized for CPU-heavy work like encoding or batch jobs, memory optimized for caches and in-memory databases, storage optimized for very fast local disk, and accelerated instances with GPUs for machine learning. Inside a family, the size just scales CPU and memory together. Burstable types are fine for small services that idle most of the day but can run out of CPU credits under steady load. I'd also check whether the code runs on ARM, because the Graviton types usually give better value. Then I don't guess for long: I pick a sensible size, load test it, watch CPU and memory in CloudWatch, and right-size from there."
Picking an instance by name or habit without saying what resource the workload actually needs.
On-Demand: pay by the second or hour, no commitment; for new, spiky or short-lived work.
Commitments: Savings Plans or Reserved Instances for the steady baseline you know you'll run for one or three years.
Spot: spare capacity at a deep discount that can be taken back with a two-minute warning; only for work that tolerates interruption.
Dedicated: Dedicated Hosts or Instances for licensing or compliance needs.
"There are four main ways to pay. On-Demand has no commitment, so it's right for new workloads, spiky traffic, or anything I don't understand yet. Once I know the steady baseline a system always needs, I cover that with a Savings Plan or Reserved Instances, committing for one or three years in exchange for a much lower rate. Savings Plans are the more flexible choice, since a compute plan follows you across instance families and regions. Spot is spare capacity sold cheaply, but AWS can reclaim it with a two-minute notice, so I only use it for stateless or restartable work like batch jobs, CI runners or extra web nodes behind a load balancer. Dedicated Hosts are for when a software licence or a compliance rule needs physical isolation. In practice a good setup mixes them: commitments for the floor, On-Demand and Spot above it."
Putting a stateful database or a single critical server on Spot, or committing to a long reservation before knowing the real baseline.
Frequent: Standard for data read often; Intelligent-Tiering when the access pattern is unknown.
Infrequent: Standard-IA and One Zone-IA, cheaper to store but with retrieval fees and a minimum storage duration.
Archive: the Glacier classes, from instant retrieval down to Deep Archive that takes hours.
Lifecycle: rules that transition or expire objects by age or prefix.
"Standard is the default for data that's read often. If I honestly don't know how often something will be read, Intelligent-Tiering moves each object between tiers for me based on access, for a small monitoring charge. For data that's read rarely but must come back fast, like old reports, there's Standard-IA, and One Zone-IA if the data can be recreated, since it lives in one Availability Zone only. Those charge per retrieval and have a minimum storage duration, so they're wrong for short-lived files. Then there are the Glacier classes for archives: Instant Retrieval, Flexible Retrieval that takes minutes to hours, and Deep Archive, the cheapest, where a restore takes hours. To move data automatically I set a lifecycle rule, say logs go to Standard-IA after a month, to Glacier after a quarter, and get deleted after a year."
Saying cheaper classes are always better, ignoring retrieval fees, minimum durations and restore times.
Today: S3 gives strong read-after-write consistency for new objects, overwrites, deletes and listings.
History: overwrites and deletes used to be eventually consistent, so older code and advice may still work around it.
Limits: it is not locking; two writers to the same key means the last write wins unless you use conditional writes.
"Yes. S3 has had strong read-after-write consistency since late 2020, so once a PUT succeeds, any read after it gets the new data, and that covers overwrites, deletes and list operations too. Before that, overwrites and deletes were only eventually consistent, which is why you still see old code with retries or extra databases just to track what's really in a bucket. What strong consistency does not give me is locking. If two processes write the same key at almost the same time, the one that lands last wins and the other write is simply gone. So if I need coordination, like only one worker processing a file, I use a conditional write, which S3 itself now supports with an if-none-match or if-match check, or a lock record in DynamoDB. Turning on versioning also means an overwrite never destroys the older copy."
Saying S3 is still eventually consistent for overwrites, or assuming strong consistency means S3 handles concurrent writers safely.
No public path: Block Public Access on, ACLs disabled, no wildcard principals in the bucket policy.
Who gets in: IAM roles with narrow policies, and a bucket policy that enforces HTTPS.
Encryption: encrypted at rest by default; SSE-KMS when you need key control and an audit of key use.
Recovery and audit: versioning, maybe Object Lock, plus CloudTrail data events.
"I'd go layer by layer. First, make sure nothing is public. New buckets now start with Block Public Access on and ACLs disabled, but I'd confirm both, turn Block Public Access on for the whole account too, and make sure no bucket policy has a wildcard principal. Second, access goes through IAM roles with the smallest set of actions and prefixes each service needs, and I add a bucket policy that denies any request not using HTTPS. Third, encryption. New objects are encrypted at rest by default now, but for customer documents I'd use a KMS key, so key use is logged and I can control who may decrypt. Fourth, protection from mistakes: versioning so an overwrite or delete can be undone, and Object Lock if there's a retention rule. Finally, logging, with CloudTrail data events so I can see who read what. Customers get files through short-lived presigned URLs, never a public link."
Relying only on the object's name being hard to guess, or making the bucket public so the website can show files.
Flow: the client asks your API for permission; the API checks the user and returns a presigned PUT URL.
Scope: the URL is tied to one bucket, one key and a short expiry; the key is chosen by the server.
Upload: the browser PUTs straight to S3; an S3 event or a callback tells the backend it arrived.
"I'd use a presigned URL. The browser calls my API and says it wants to upload a file. The API checks the user is logged in and allowed to, then picks the object key itself, something like the user's ID plus a random name, so nobody can overwrite someone else's file. It signs a PUT URL for that exact key with a short expiry, a few minutes, using the backend's own role. The browser uploads straight to S3 with that URL, so the heavy traffic never touches my servers and no AWS credentials ever reach the client. For very large files I'd use multipart upload with a presigned URL per part. Then I need to know it landed, so either an S3 event triggers processing, or the client tells the API and the API checks the object exists. The bucket also needs a CORS rule that allows the PUT from my site."
import uuid
import boto3
s3 = boto3.client("s3")
def upload_url(user_id: str) -> dict:
key = f"uploads/{user_id}/{uuid.uuid4()}.pdf"
url = s3.generate_presigned_url(
"put_object",
Params={"Bucket": "customer-docs", "Key": key,
"ContentType": "application/pdf"},
ExpiresIn=300, # seconds
)
return {"url": url, "key": key}
Letting the client choose any key, or sending long-lived access keys to the browser.
Policy: a JSON document saying which actions are allowed or denied on which resources.
User and group: a user is a long-term identity; a group bundles users to share policies.
Role: an identity with no password or keys of its own that someone or something assumes for temporary credentials.
"A policy is the rulebook: a JSON document that allows or denies actions, like reading objects in one bucket. On its own it does nothing until it's attached to an identity or a resource. A user is a long-term identity, a person or an old-style application, with a console password or access keys. A group is just a way to attach the same policies to many users, like a developers group. A role is different: it has no permanent credentials. Someone or something assumes it and gets temporary credentials that expire, maybe an EC2 instance, a Lambda function, a user from another account, or a person signing in through single sign-on. These days I'd push almost everything towards roles, because temporary credentials expire on their own, so a leaked one isn't useful for months the way a forgotten access key is."
Describing a role as just a group of permissions, or creating an IAM user with access keys for every application.
Role: create an IAM role with read access to that bucket and attach it through an instance profile.
Credentials: the SDK fetches temporary credentials from the instance metadata service and refreshes them.
Hardening: require IMDSv2 and keep the policy narrow.
"It should never have access keys in the code, a config file or environment variables. I'd create an IAM role whose policy allows reading only the bucket, or the prefix, that the app needs, and attach it to the instance through an instance profile. Then the AWS SDK finds the credentials by itself: it asks the instance metadata service, gets temporary credentials, and refreshes them before they expire, so there's nothing to rotate or leak into a repository. I'd also require IMDSv2 on the instance, because it uses a session token and makes it much harder for a bug like server-side request forgery to steal those credentials. The same idea applies everywhere: Lambda functions get an execution role, and containers get a task role."
Putting an access key and secret into the application's config, even in a private repository.
Default: everything is denied unless something allows it.
Explicit deny: a deny in any applicable policy beats every allow.
Ceilings: organization SCPs and RCPs, permission boundaries and session policies only limit; they never grant on their own.
Cross-account: both the caller's identity policy and the resource's policy must allow the action.
"I think of it as a funnel. It starts from an implicit deny: if nothing allows the action, it fails. Then AWS gathers every policy that applies, meaning identity policies, resource policies like a bucket policy, organization service and resource control policies, permission boundaries and session policies. If any of them has an explicit deny that matches, the answer is deny, full stop, whatever else says allow. The organization policies, boundaries and session policies act as ceilings: they don't grant anything, but the action has to be inside them. Then there must be an allow from an identity or resource policy. Across accounts it's stricter: the caller's own policy and the resource policy on the other side must both allow it. So when I debug an access denied, I check for a deny first, then the ceilings, then whether an allow exists at all."
Saying the most permissive policy wins, or that an allow can override an explicit deny.
Two resources: ListBucket applies to the bucket ARN; GetObject applies to object ARNs.
Prefix: limit listing with a condition on the s3:prefix key.
Nothing extra: no wildcards on actions or buckets.
"The trap here is that S3 actions work at two levels. Listing is an action on the bucket itself, so s3:ListBucket goes on the bucket ARN, and I restrict it with a condition so the caller can only list keys starting with the reports folder. Reading is an action on objects, so s3:GetObject goes on the object ARN with the folder prefix and a wildcard after it. If I put GetObject on the bucket ARN, or ListBucket on the object path, the policy looks right but doesn't work. I keep the actions explicit, no s3 star, and no other buckets. If the objects were encrypted with a customer-managed KMS key, I'd also need kms:Decrypt on that key, which people often forget."
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::team-data",
"Condition": { "StringLike": { "s3:prefix": "reports/*" } }
},
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::team-data/reports/*"
}
]
}
Granting s3:* on every resource and calling it temporary.
Contain: deactivate the key immediately; assume it was already used.
Investigate: CloudTrail for every call made with that key ID, across all regions.
Clean up: remove anything the attacker created, rotate what the key could reach, replace the key with a role.
Prevent: secret scanning before code is pushed, and fewer long-lived keys.
"I'd assume it's already been found, because automated scanners pick up public keys within minutes. So step one is deactivating the key right away, even if something in production uses it. A short outage is better than an attacker with our credentials. Then I'd investigate with CloudTrail, searching every region for calls made with that access key ID, because attackers often launch instances in regions we never use. I'd look for new IAM users, keys or roles they may have created to keep access, and remove them, along with any instances or other resources they started. If the key could read secrets or data, I'd treat those as exposed and rotate them. Deleting the commit isn't enough, since it's already been copied. Afterwards the app gets a role instead of a key, and we add secret scanning so this can't reach a repository again."
Starting by deleting the commit from the repository and leaving the key active.
Public: its route table sends internet traffic to an internet gateway, and instances have public IPs.
Private: no route to the internet gateway; outbound internet, if any, goes through a NAT gateway.
Placement: load balancers in public subnets; app servers and databases in private ones.
"There's no public checkbox on a subnet. What makes it public is its route table: if it has a route for all internet traffic, 0.0.0.0/0, pointing at an internet gateway, it's public. An instance in it also needs a public or Elastic IP to be reachable. A private subnet has no route to the internet gateway, so nothing on the internet can start a connection to it. If the instances there need to download updates, their route table sends outbound traffic to a NAT gateway sitting in a public subnet. The usual layout is a public and a private subnet in each Availability Zone: the load balancer lives in the public ones, and the app servers and databases live in the private ones, reachable only from the load balancer and each other."
Saying a subnet is public because of its name, or putting the database in a public subnet for convenience.
Security group: attached to an instance or network interface, stateful, allow rules only.
NACL: attached to a subnet, stateless, allow and deny rules checked in number order.
Use: security groups for almost everything; NACLs as a coarse extra layer, like blocking an address range.
"A security group sits on the network interface of an instance, a load balancer or a database. It's stateful, so if I allow inbound traffic on port 443, the reply goes back out automatically. It only has allow rules, and a nice feature is that a rule can point at another security group, like the database accepting traffic only from the app servers' group. A network ACL sits at the subnet boundary. It's stateless, so I have to allow the return traffic myself, which usually means opening the ephemeral port range outbound, and that's where people break things. It has both allow and deny rules, evaluated in order by rule number, first match wins. In practice I do almost all my filtering with security groups and use NACLs rarely, for a blunt subnet-wide rule like blocking a known bad range."
Saying both are stateful, or trying to add a deny rule to a security group.
NAT gateway: lets private instances start outbound connections while blocking inbound ones.
Resilience: a standard NAT gateway lives in one zone, so run one per Availability Zone, or use a regional NAT gateway that spans zones.
Endpoints: gateway endpoints for S3 and DynamoDB, interface endpoints for other services, so that traffic stays private.
"Servers in a private subnet still need to reach out now and then, for package updates or a third-party API. A NAT gateway sits in a public subnet and lets them start outbound connections, while nothing from the internet can start a connection back in. A standard NAT gateway lives in one zone, so it's a single point of failure for the others. For production I put one in each Availability Zone with each private route table pointing at its own zone's gateway, or I use the newer regional NAT gateway, which spreads across zones by itself. When the traffic is to AWS services themselves, I'd rather use VPC endpoints. A gateway endpoint for S3 or DynamoDB is just a route table entry and costs nothing extra, and interface endpoints cover services like Secrets Manager. That keeps the traffic off the internet path and cuts NAT data charges."
Saying a NAT gateway lets the internet reach private servers, or routing heavy S3 traffic through NAT without a second thought.
Push back: a public database is exposed to the whole internet's scanning and password guessing.
Better options: a site-to-site VPN, PrivateLink, a read replica, or a narrow API or data export.
Least access: read-only user, only the tables they need, logged, with an end date.
"I'd say no to the public subnet, but not stop there, because the vendor's need is real. Putting the database in a public subnet exposes it to constant scanning and password guessing, and one weak setting means our customer data is out. Then I'd ask what they actually need. If they need a live connection, a site-to-site VPN or PrivateLink keeps traffic private, and they'd connect to a read replica, never the primary, so their queries can't slow down production. If they only need certain data, an export to a bucket they can read, or a small API, is even safer. Either way they get a read-only database user limited to the tables they need, access is logged, and it has an end date. If time is short, I'd propose the smallest safe version first rather than the quick unsafe one."
Agreeing to open the database to the internet with only a strong password as protection.
ALB: layer 7, understands HTTP; routes by path, host or header; ends TLS; good for web apps and APIs.
NLB: layer 4, TCP, UDP and TLS; very high throughput, low latency, static IP per zone.
Shared: both spread targets across zones and run health checks.
"An Application Load Balancer works at layer 7, so it reads the HTTP request. That lets me route by path or host name, like sending /api to one target group and /images to another, send redirects, terminate TLS with a certificate from Certificate Manager, and put a web application firewall in front. It's my default for websites and HTTP APIs. A Network Load Balancer works at layer 4, so it just forwards TCP or UDP connections without looking inside. I'd pick it for non-HTTP protocols, for extreme throughput with very low latency, when a client needs a fixed IP address per zone to put on an allow list, or when the backend has to see the client's original IP. Both do health checks and spread traffic across Availability Zones, so the choice is really about which layer you need to work at."
Saying an NLB can route by URL path, or that the two are interchangeable.
Pieces: a launch template, minimum, desired and maximum size, and subnets across zones.
Healing: health checks, ideally from the load balancer, replace broken instances.
Policies: target tracking for most cases; step, scheduled and predictive for special ones.
Tuning: instance warm-up and cooldowns so it doesn't flap, and a stateless app so any instance can go.
"An Auto Scaling group keeps a fleet of identical instances built from a launch template, between a minimum and a maximum, spread across subnets in several Availability Zones. It does two jobs. One is healing: if an instance fails its health check, it's terminated and replaced, and I use the load balancer's health check so an instance that's running but serving errors gets replaced too. The other is elasticity. My default policy is target tracking, where I pick a metric and a target, like average CPU around the middle or a number of requests per instance, and it adds or removes instances to hold it. Scheduled scaling helps when I know the pattern, like a daily morning peak. I also set a warm-up time so a new instance isn't counted before it's ready, and make sure the app is stateless, because any instance can disappear on scale-in."
Keeping user sessions or uploaded files on the instance's local disk inside an Auto Scaling group.
Front: a load balancer across at least two zones, plus a CDN for static content.
App tier: stateless instances in an Auto Scaling group spread over the same zones, with headroom.
Data: a Multi-AZ database, sessions in a shared store, files in S3.
Hidden single points: a NAT gateway per zone (or a regional one), no singleton servers, test by failing a zone.
"A region has several Availability Zones, each made of one or more separate data centres, so the goal is that no piece lives in only one. At the front, an Application Load Balancer enabled in at least two zones, with CloudFront for static files. Behind it, stateless app servers in an Auto Scaling group spread across those zones, sized so the remaining zones can carry the full load if one disappears. That headroom is what people forget. State moves out of the servers: sessions in ElastiCache or DynamoDB, uploads in S3, and the database on RDS Multi-AZ so a standby in another zone takes over automatically. Then I hunt hidden single points, like one NAT gateway, one cron server or one bastion host, and fix each. Finally I'd test it by taking a zone's instances out of service in staging and watching what breaks."
Running everything in two zones but with a single NAT gateway, a single database instance or no spare capacity.
RDS: managed relational engines; joins, flexible queries, transactions across tables.
DynamoDB: serverless key-value and document store; consistent speed at any scale, but you design around known queries.
Question to ask: do I know my access patterns up front, and do I need ad hoc queries?
"RDS gives me a managed relational database, like PostgreSQL or MySQL, where AWS handles backups, patching and failover. I'd pick it when the data is relational, I need joins and transactions across tables, and I expect to ask new questions of the data later, like a billing system or an admin dashboard. DynamoDB is a serverless key-value and document store. It gives steady, fast responses at almost any scale with no servers to size, which suits things like user sessions, carts or event data with huge traffic. The catch is I have to design the table around my access patterns up front, because querying by something that isn't a key is awkward or expensive. So my first question is: do I know exactly how I'll read this data? If yes and scale matters, DynamoDB. If the queries will keep changing, RDS."
Choosing DynamoDB for a data model full of joins and ad hoc reports, or saying NoSQL is always faster.
Multi-AZ: a synchronous standby in another zone for automatic failover; in the classic setup it serves no reads.
Read replica: an asynchronous copy that serves reads; it can lag and must be promoted by hand.
Together: production often uses both, for different reasons.
"Multi-AZ is about availability. RDS keeps a standby copy in another Availability Zone and writes to it synchronously, so no committed data is lost. If the primary fails, or during some maintenance, RDS fails over to the standby and the endpoint name stays the same, so the app just reconnects. In the classic Multi-AZ setup, the standby doesn't serve any traffic. The newer Multi-AZ cluster option does have readable standbys, but that's a different deployment. A read replica is about scale. It's an asynchronous copy that I can point read-heavy work at, like reports or search pages, and it can even live in another region. Because it's asynchronous it can lag behind, so I wouldn't read from it right after a write that the user expects to see. And it doesn't fail over by itself; I'd have to promote it. For production I usually want Multi-AZ for safety and replicas only if reads are the bottleneck."
Saying read replicas give you automatic failover, or that the classic Multi-AZ standby helps with read load.
Access first: list the queries, then design keys that answer them directly.
Spread: a partition key with many distinct values that are used evenly; sort key for ranges within a partition.
Hot keys: add a suffix to split heavy keys, or cache reads.
Indexes: a GSI for a different access pattern; an LSI only at table creation for another sort order.
"I start from the queries, not the entities. DynamoDB hashes the partition key to decide where an item lives, and each partition has a throughput limit, so I want a key with many distinct values that traffic hits evenly, like a user ID or an order ID. The sort key then lets me fetch ranges inside one partition, like all orders for a customer newest first. A hot partition happens when one key gets far more traffic than the rest, like a date as the key where every write lands on today. Adaptive capacity helps a bit, but the real fix is design: spread writes by adding a random or calculated suffix to the key and reading across the suffixes, or put a cache in front of heavy reads. For a second access pattern, like finding orders by status, I'd add a global secondary index with its own keys."
Using a low-variety value like status or today's date as the partition key, or relying on Scan with filters for normal queries.
Hard limits: 15 minutes maximum per invocation for a standard function; memory up to about 10 GB, and CPU grows with memory.
Concurrency: a regional concurrency quota shared by every function in the account.
Behaviour: cold starts, no state between runs, and connection storms against databases.
Wrong fit: long jobs, steady heavy traffic, or anything needing a persistent connection.
"The first hard limit is time: a standard function invocation can run for at most 15 minutes. Memory goes up to about 10 GB, and CPU is allocated in proportion, so raising memory is also how you get more CPU. Concurrency is capped by a regional quota that all functions in the account share, so one runaway function can starve the others unless I set reserved concurrency. Then there are the practical limits. A new execution environment has a cold start, which adds latency, especially for heavy runtimes. Nothing survives between runs except by luck, so state goes to S3 or a database. And thousands of concurrent functions can flood a relational database with connections, which is what RDS Proxy is for. So Lambda is wrong for jobs longer than 15 minutes, for steady, heavy traffic that's cheaper on containers, or for anything holding a long-lived connection."
Saying Lambda scales infinitely with no limits, or planning a long batch job around one invocation.
Event: loop over Records; each holds the bucket name and the object key.
Encoding: keys arrive URL-encoded, so decode them before use.
Safety: create the client outside the handler, write output elsewhere, make processing safe to repeat.
"The S3 event can carry more than one record, so I loop over Records and pull out the bucket name and the key. The trap is that the key arrives URL-encoded, so a file called my report.csv shows up with a plus sign instead of the space, and I have to decode it or the GetObject call fails. I create the S3 client outside the handler, so a warm environment reuses it. Two more things matter in production. First, I never write the output back into the same bucket and prefix that triggers the function, or it triggers itself in a loop. Second, delivery is at least once, so the same event can arrive twice, and processing should be safe to repeat. If a record keeps failing, I'd send it to a dead-letter queue rather than lose it."
import urllib.parse
import boto3
s3 = boto3.client("s3") # reused across warm invocations
def handler(event, context):
for record in event["Records"]:
bucket = record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
body = s3.get_object(Bucket=bucket, Key=key)["Body"].read()
result = body.decode("utf-8").upper()
s3.put_object(Bucket="processed-output", Key=key, Body=result)
return {"processed": len(event["Records"])}
Writing results into the same bucket and prefix that triggers the function, creating an endless loop.
Metrics: CPU, network and status checks come built in; memory and disk need the CloudWatch agent.
Logs: ship app logs to CloudWatch Logs and search them with Logs Insights.
Alarms: alarms on user-facing signals, like errors and latency, that notify through SNS or trigger scaling.
"CloudWatch has three parts I'd use together. Metrics: EC2 sends CPU, network and status checks on its own, but not memory or disk usage, because AWS can't see inside the operating system. For those I install the CloudWatch agent, which can also ship log files. Logs: the app's logs go to CloudWatch Logs, and Logs Insights lets me query them when something breaks. Alarms: I'd alarm on what users feel first, like the load balancer's 5xx errors and response time, then on causes like high CPU, a full disk or a failed status check. Alarms send to an SNS topic that pages the on-call person or posts to the team chat, and some can trigger actions like scaling or recovering an instance. A small dashboard with those same numbers helps anyone see health at a glance."
Assuming memory and disk metrics exist by default, or alerting only on CPU.
What: a YAML or JSON template describes resources; CloudFormation builds them as one stack.
Safe changes: review a change set first; it shows what will be updated, replaced or deleted.
Protection: DeletionPolicy and UpdateReplacePolicy set to Retain on data, termination protection, drift detection and automatic rollback.
"CloudFormation is AWS's infrastructure as code. I describe resources in a YAML template, and CloudFormation creates them as a stack, working out the order from the dependencies. Because the template lives in version control, changes get reviewed like application code, and I can build the same environment again for staging. For production, I never apply a change blind. I create a change set, which shows every resource that will be added, modified or deleted, and most importantly anything that will be replaced, because a replaced database or bucket can mean lost data. On stateful resources I set both the deletion policy and the update-replace policy to Retain, so neither deleting nor replacing throws the data away, and I turn on termination protection for the stack. If an update fails halfway, CloudFormation rolls back to the last working state. And I run drift detection now and then to catch anyone changing things by hand in the console."
AWSTemplateFormatVersion: "2010-09-09"
Resources:
ReportsBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Retain
UpdateReplacePolicy: Retain
Properties:
VersioningConfiguration:
Status: Enabled
Outputs:
BucketName:
Value: !Ref ReportsBucket
Updating a production stack without looking at what the change will replace or delete.
Upload: the API hands out presigned URLs; files go straight into an uploads bucket.
Process: an S3 event, ideally through a queue, triggers a Lambda that makes the sizes and writes to a second bucket.
Serve and track: CloudFront in front of the output bucket; metadata in DynamoDB.
Operate: retries and a dead-letter queue, alarms, lifecycle rules, and all of it in a template.
"I'd keep it serverless because the load is bursty. The client asks an API, API Gateway plus a small Lambda, for an upload URL. That checks the user and returns a presigned PUT for a key it chooses in an uploads bucket. When the file lands, S3 sends an event into an SQS queue, and a Lambda reads from the queue, makes the thumbnail and the medium size, and writes them to a separate output bucket, so it can never trigger itself. The queue gives me retries and a dead-letter queue for images that keep failing. Photo records, like owner, status and sizes, go in DynamoDB keyed by user so the app can list a user's photos fast. Output is served through CloudFront with the bucket kept private. Lifecycle rules clear abandoned uploads, alarms watch the dead-letter queue, and the whole thing is defined in CloudFormation."
Routing every upload through a single web server's disk, or making the bucket public so photos can be shown.
Narrow down: Cost Explorer by service, then region, usage type and tags, day by day.
Usual suspects: runaway Lambda or scaling, NAT and data transfer, log volume, forgotten resources, or a compromise.
Guardrails: budgets with alerts, anomaly detection, and tags that name an owner.
"I'd open Cost Explorer with daily granularity and group by service to see what jumped, then drill into region and usage type. That usually points straight at something. A service in a region we don't use, especially lots of big instances, makes me think of a compromised account first, so I'd check CloudTrail straight away. Other common causes I'd look for: a Lambda function triggering itself in a loop, an Auto Scaling group stuck at its maximum, a NAT gateway suddenly carrying heavy traffic because something started pulling data from S3 over the internet path, or debug logging left on and flooding CloudWatch Logs. Tags tell me which team owns it, so I can fix it with them rather than guess. Afterwards I'd set up AWS Budgets alerts and Cost Anomaly Detection, so next time we hear about it in hours, not at the end of the month."
Waiting for the end-of-month invoice, or shutting random resources down without finding the cause.
Situation: the symptom users saw and how you found out.
Evidence: the metrics, logs or traces you checked and what ruled things out.
Fix and follow-up: the immediate fix, the root cause, and the alarm or change that prevents a repeat.
"At my last company our API started timing out every evening for about twenty minutes. CloudWatch showed the app servers' CPU was fine, but the load balancer's response time spiked at the same moment every day. I pulled the logs for that window in Logs Insights and saw the slow requests were all waiting on database connections. The RDS metrics showed connections hitting the limit exactly when a nightly report job started, because the job opened a fresh connection per row. As a quick fix we moved the job to a read replica, which cleared the timeouts the same day. The real fix was rewriting the job to use a small connection pool. I also added an alarm on database connections so we'd see it coming, and wrote a short note in our runbook. The lesson I took was to line up metrics from each layer on one timeline."
A story where the fix was restarting servers until it went away, with no root cause and no follow-up.
Finding: what was wrong and how you discovered it.
Evidence: how you learned what the access was really used for before changing it.
Rollout: tighten in steps, with a way back, and prevent it returning.
"When I joined my last team, the main application ran with an IAM user whose access keys were years old and had full admin rights. Several services and a couple of scripts shared those keys, and nobody was sure which. Revoking them in one go would have taken production down. So I first looked at CloudTrail to see which actions and resources those keys actually used over the past few months, and IAM Access Advisor to see which services were touched. That gave me a real list. I created a separate role for each service with only what it used, moved the services one at a time to their roles, and watched for access denied errors after each move. Once CloudTrail showed no calls on the old keys for two weeks, I deactivated them, waited a bit more, then deleted them. We also added a rule to flag any new long-lived keys."
Deleting the keys straight away and breaking production, or leaving them alone because changing them felt risky.
Why: the problem hand-built setup was causing.
How: inventory, template, import existing resources rather than recreate them, verify no changes.
Result: what got easier, and the rule that kept it that way.
"At my last company the team had a staging environment someone had clicked together in the console two years earlier. When we needed a second copy for a new client, nobody could remember every setting, and the first attempt was missing a security group rule that took a day to find. I offered to put it into CloudFormation. I listed every resource by hand, wrote the template, and then used resource import so the stack adopted the existing VPC, instances and database instead of rebuilding them. Before calling it done, I ran drift detection and a change set to confirm the template matched reality with nothing to change. After that, building the new client's environment took an afternoon from the same template with different parameters. We also agreed that any change goes through the template, and drift detection runs weekly to catch anyone who forgets."
Deleting and recreating live resources just to get them into a template.
ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.