Skip to content

AWS for DevOps: IAM, EC2, VPC, S3 & Deployment Patterns

The core AWS services a DevOps engineer touches daily, wired together into real deployment architectures with security baked in.

IAMEC2VPCS3RDSCloudWatchALB
RR

Ranjith R

Linux SysAdmin & Cloud Engineer

Published February 14, 2026Updated June 28, 2026
8 min readAdvanced#AWS#Cloud#IAM#EC2#Networking
Share
Section 01

Introduction

AWS is the default cloud substrate for most production workloads, but the surface area is enormous — over 200 services, most of which a DevOps engineer will never touch. In daily practice, the job narrows down to a small, well-defined core: IAM for identity and access, EC2 for compute, VPC for networking, S3 for object storage, RDS for managed databases, CloudWatch for observability, and an Application Load Balancer tying it all together in front of an Auto Scaling group.

This guide covers that core stack in the depth a working engineer actually needs — real IAM policy JSON, real VPC wiring, real CLI commands for every service, and the production patterns that connect them into a deployable architecture. It complements (rather than repeats) a static-hosting-specific S3 + CloudFront + ACM walkthrough elsewhere in this site — here the focus is the broader compute/networking/database stack behind an actual application.

Who this is for

  • DevOps/SRE engineers who need the AWS services touched daily, not the entire catalog.
  • Linux administrators moving workloads from on-prem or VPS hosting into AWS.
  • Anyone designing or reviewing a production 3-tier architecture on AWS for the first time.
Section 02

Prerequisites

You should have the following in place before working through this guide:

  • An AWS account with billing enabled and root-account MFA turned on.
  • An IAM user or role with administrative access for initial setup (never the root account day-to-day).
  • AWS CLI v2 installed locally, or access to AWS CloudShell in the console.
  • Basic comfort with JSON, since IAM policies and CLI --cli-input-json payloads are both JSON.
  • A registered domain if you intend to follow the Route 53 sections end to end.

Root account vs IAM

The root account should be used only to create your first IAM administrator and to set up billing alerts and MFA — then locked away. Everything in this guide is performed as an IAM identity, exactly as it would be in production.
Section 03

Theory

The Shared Responsibility Model

AWS secures the cloud — physical data centers, host hardware, hypervisor, and managed-service internals. You secure what you put in the cloud— IAM policies, security group rules, OS patching on EC2, data encryption, and application-level access control. Moving to a managed service (RDS vs. self-hosted MySQL on EC2) shifts more of that line to AWS, but it never disappears entirely — misconfigured IAM or an open security group is always the customer's responsibility.

Regions, Availability Zones, and edge locations

ConceptWhat it is
RegionAn independent geographic area (e.g. ap-south-1) containing multiple AZs; most resources are region-scoped.
Availability Zone (AZ)One or more discrete data centers within a region, with independent power/networking; the unit of fault-tolerance for Multi-AZ RDS and multi-AZ EC2/ALB deployments.
Edge locationA CloudFront/Route 53 point of presence, far more numerous than regions, used for caching and DNS resolution close to end users.

A production-grade design spreads compute and databases across at least two AZs in one region, so the loss of a single data center does not take the application down.

IAM fundamentals

  • Users— a persistent identity (human or service) with long-lived credentials; use sparingly and prefer roles for anything that isn't a human logging in.
  • Groups— a collection of users that share attached policies; there is no "group of groups" nesting.
  • Roles — an identity assumed temporarily (by an EC2 instance, a Lambda function, or another AWS account) via STS, producing short-lived credentials instead of a permanent access key.
  • Policies — JSON documents that grant or deny actions on resources; evaluated with an explicit-deny-wins, default-deny model — nothing is allowed unless a policy statement says so.

Prefer roles over access keys

An EC2 instance, ECS task, or Lambda function should assume an IAM role via an instance profile rather than have an IAM user's access keys baked into an environment variable or AMI. Roles issue temporary credentials that rotate automatically.
Section 04

Architecture

A standard production 3-tier layout: a public-facing Application Load Balancer in public subnets, application servers in private subnets across two Availability Zones, and a Multi-AZ RDS database in dedicated private database subnets. Outbound internet access for the private tier (OS updates, package installs) flows through a NAT Gateway in the public subnet; inbound internet access only ever reaches the ALB.

Client → Internet Gateway → ALB (public subnets) → EC2 (private subnets) → RDS Multi-AZ (private DB subnets), with a NAT Gateway for outbound-only traffic

Never put EC2 or RDS in a public subnet

Application and database instances should have no route to an Internet Gateway at all. The ALB is the only component with a public IP; everything behind it reaches the internet outbound-only via NAT, and receives inbound traffic only from the ALB's security group.
Section 05

Installation

Almost everything in this guide can be done from the AWS Console, but the CLI is what makes AWS work reproducible and scriptable — the same commands work in CI/CD pipelines.

terminal
# Download and install AWS CLI v2
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install

# Verify
aws --version

# Configure a named profile with an IAM user's access key
aws configure --profile deploy
# AWS Access Key ID: AKIA...
# AWS Secret Access Key: ********
# Default region name: ap-south-1
# Default output format: json

# Use the profile for any command
aws sts get-caller-identity --profile deploy

Named profiles over a single default

Use --profile with named profiles in ~/.aws/credentials for every account/role you touch, rather than overwriting the default profile — this avoids accidentally running a command against the wrong account.
~/.aws/config
[profile deploy]
region = ap-south-1
output = json

[profile prod-readonly]
role_arn = arn:aws:iam::123456789012:role/ReadOnlyAuditor
source_profile = deploy
region = ap-south-1
Section 06

Configuration

IAM — users, roles, and least privilege

Create IAM users only for humans; give services and EC2 instances roles instead. A least-privilege policy names specific actions and resources rather than "Action": "*":

ec2-s3-readonly-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadAppBucket",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::app-uploads-prod",
        "arn:aws:s3:::app-uploads-prod/*"
      ]
    },
    {
      "Sid": "AllowOwnLogGroup",
      "Effect": "Allow",
      "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
      "Resource": "arn:aws:logs:ap-south-1:123456789012:log-group:/app/prod:*"
    }
  ]
}

A role that EC2 instances assume needs a trust policy separate from its permissions policy, naming the EC2 service as the trusted principal:

ec2-trust-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": { "Service": "ec2.amazonaws.com" },
      "Action": "sts:AssumeRole"
    }
  ]
}
terminal
# Create the role, attach the trust policy, then attach the permissions policy
aws iam create-role \
  --role-name AppServerRole \
  --assume-role-policy-document file://ec2-trust-policy.json

aws iam put-role-policy \
  --role-name AppServerRole \
  --policy-name S3ReadOnlyAndLogs \
  --policy-document file://ec2-s3-readonly-policy.json

# Wrap the role in an instance profile so EC2 can use it
aws iam create-instance-profile --instance-profile-name AppServerProfile
aws iam add-role-to-instance-profile \
  --instance-profile-name AppServerProfile \
  --role-name AppServerRole

VPC — subnets, route tables, gateways

A production VPC splits into at least three subnet tiers per Availability Zone: public (ALB, NAT Gateway), private-app (EC2), and private-db (RDS) — each with its own route table.

terminal
# VPC and subnets
aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications \
  'ResourceType=vpc,Tags=[{Key=Name,Value=prod-vpc}]'

aws ec2 create-subnet --vpc-id vpc-0abc123 --cidr-block 10.0.0.0/24 \
  --availability-zone ap-south-1a --tag-specifications \
  'ResourceType=subnet,Tags=[{Key=Name,Value=public-1a}]'

aws ec2 create-subnet --vpc-id vpc-0abc123 --cidr-block 10.0.10.0/24 \
  --availability-zone ap-south-1a --tag-specifications \
  'ResourceType=subnet,Tags=[{Key=Name,Value=private-app-1a}]'

aws ec2 create-subnet --vpc-id vpc-0abc123 --cidr-block 10.0.20.0/24 \
  --availability-zone ap-south-1a --tag-specifications \
  'ResourceType=subnet,Tags=[{Key=Name,Value=private-db-1a}]'

# Internet Gateway for the public tier
aws ec2 create-internet-gateway
aws ec2 attach-internet-gateway --vpc-id vpc-0abc123 --internet-gateway-id igw-0def456

# NAT Gateway (needs an Elastic IP) in the public subnet, for private-tier egress
aws ec2 allocate-address --domain vpc
aws ec2 create-nat-gateway --subnet-id subnet-public1a --allocation-id eipalloc-0aaa111

# Route tables
aws ec2 create-route-table --vpc-id vpc-0abc123
aws ec2 create-route --route-table-id rtb-public \
  --destination-cidr-block 0.0.0.0/0 --gateway-id igw-0def456
aws ec2 create-route --route-table-id rtb-private \
  --destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-0bbb222
aws ec2 associate-route-table --subnet-id subnet-public1a --route-table-id rtb-public
aws ec2 associate-route-table --subnet-id subnet-private-app1a --route-table-id rtb-private

EC2 and Security Groups

Security groups are stateful virtual firewalls attached to instances or ENIs — a response to allowed inbound traffic is automatically allowed out, and vice versa. Design them narrowly: reference other security groups as the source instead of CIDR blocks wherever possible.

terminal
# ALB security group: public HTTPS/HTTP only
aws ec2 create-security-group --group-name alb-sg \
  --description "ALB ingress from internet" --vpc-id vpc-0abc123
aws ec2 authorize-security-group-ingress --group-id sg-alb \
  --protocol tcp --port 443 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id sg-alb \
  --protocol tcp --port 80 --cidr 0.0.0.0/0

# App security group: only from the ALB's security group, never the open internet
aws ec2 create-security-group --group-name app-sg \
  --description "App servers, ingress from ALB only" --vpc-id vpc-0abc123
aws ec2 authorize-security-group-ingress --group-id sg-app \
  --protocol tcp --port 8080 --source-group sg-alb

# DB security group: only from the app tier, on the DB port
aws ec2 create-security-group --group-name db-sg \
  --description "RDS ingress from app tier only" --vpc-id vpc-0abc123
aws ec2 authorize-security-group-ingress --group-id sg-db \
  --protocol tcp --port 5432 --source-group sg-app

# Launch an EC2 instance with an instance profile, key pair, and user-data bootstrap
aws ec2 run-instances \
  --image-id ami-0abcdef1234567890 \
  --instance-type t3.small \
  --key-name prod-keypair \
  --subnet-id subnet-private-app1a \
  --security-group-ids sg-app \
  --iam-instance-profile Name=AppServerProfile \
  --user-data file://bootstrap.sh \
  --block-device-mappings 'DeviceName=/dev/xvda,Ebs={VolumeSize=30,VolumeType=gp3}' \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=app-server-1a}]'

AMIs, instance types, and user-data

An AMI is the base image (OS + pre-baked software); an instance type (e.g. t3.small, m6i.large) sets vCPU/RAM/network allowance; user-data is a boot-time script for one-time bootstrap (installing an agent, pulling the latest app release) rather than baking every change into a new AMI.
Section 07

Commands

EC2

terminal
aws ec2 describe-instances --filters "Name=tag:Name,Values=app-server-*"
aws ec2 start-instances --instance-ids i-0abc123
aws ec2 stop-instances --instance-ids i-0abc123
aws ec2 reboot-instances --instance-ids i-0abc123
aws ec2 terminate-instances --instance-ids i-0abc123
aws ec2 create-snapshot --volume-id vol-0abc123 --description "pre-upgrade snapshot"
aws ec2 describe-security-groups --group-ids sg-app

S3

terminal
aws s3 mb s3://app-uploads-prod
aws s3 cp ./report.csv s3://app-uploads-prod/reports/
aws s3 sync ./build s3://app-uploads-prod --delete
aws s3 ls s3://app-uploads-prod --recursive --human-readable
aws s3api put-bucket-versioning --bucket app-uploads-prod \
  --versioning-configuration Status=Enabled
aws s3api put-bucket-lifecycle-configuration --bucket app-uploads-prod \
  --lifecycle-configuration file://lifecycle.json

RDS

terminal
aws rds create-db-instance \
  --db-instance-identifier app-prod-db \
  --db-instance-class db.t3.medium \
  --engine postgres \
  --engine-version 16.3 \
  --master-username appadmin \
  --master-user-password 'ChangeMe!ReplaceWithSecretsManager' \
  --allocated-storage 100 \
  --multi-az \
  --db-subnet-group-name prod-db-subnet-group \
  --vpc-security-group-ids sg-db \
  --backup-retention-period 7 \
  --no-publicly-accessible

aws rds create-db-instance-read-replica \
  --db-instance-identifier app-prod-db-replica-1 \
  --source-db-instance-identifier app-prod-db

aws rds describe-db-instances --db-instance-identifier app-prod-db
aws rds create-db-snapshot --db-instance-identifier app-prod-db \
  --db-snapshot-identifier app-prod-db-manual-2026-07-02
aws rds modify-db-parameter-group --db-parameter-group-name app-pg16 \
  --parameters "ParameterName=max_connections,ParameterValue=200,ApplyMethod=pending-reboot"

CloudWatch

terminal
aws cloudwatch put-metric-alarm \
  --alarm-name high-cpu-app-tier \
  --namespace AWS/EC2 --metric-name CPUUtilization \
  --dimensions Name=AutoScalingGroupName,Value=app-asg \
  --statistic Average --period 300 --evaluation-periods 2 \
  --threshold 75 --comparison-operator GreaterThanThreshold \
  --alarm-actions arn:aws:sns:ap-south-1:123456789012:ops-alerts

aws logs create-log-group --log-group-name /app/prod
aws logs tail /app/prod --follow
aws cloudwatch get-metric-statistics \
  --namespace AWS/RDS --metric-name FreeStorageSpace \
  --dimensions Name=DBInstanceIdentifier,Value=app-prod-db \
  --start-time 2026-07-01T00:00:00Z --end-time 2026-07-02T00:00:00Z \
  --period 3600 --statistics Average

Route 53

terminal
aws route53 create-hosted-zone --name app.example.com --caller-reference $(date +%s)
aws route53 list-resource-record-sets --hosted-zone-id Z1ABCDEF23456
aws route53 change-resource-record-sets --hosted-zone-id Z1ABCDEF23456 \
  --change-batch file://upsert-alb-alias.json
aws route53 create-health-check --caller-reference $(date +%s) \
  --health-check-config file://health-check.json

CloudFront & ACM

terminal
# ACM certificate must be requested in us-east-1 for CloudFront use
aws acm request-certificate --domain-name app.example.com \
  --validation-method DNS --region us-east-1

aws cloudfront list-distributions
aws cloudfront create-invalidation --distribution-id EDFDVBD6EXAMPLE --paths "/*"

ALB / Elastic Load Balancing

terminal
aws elbv2 create-load-balancer \
  --name app-alb --type application --scheme internet-facing \
  --subnets subnet-public1a subnet-public1b --security-groups sg-alb

aws elbv2 create-target-group \
  --name app-tg --protocol HTTP --port 8080 --vpc-id vpc-0abc123 \
  --target-type instance --health-check-path /healthz \
  --health-check-interval-seconds 15 --healthy-threshold-count 2 \
  --unhealthy-threshold-count 3

aws elbv2 create-listener \
  --load-balancer-arn arn:aws:elasticloadbalancing:...:loadbalancer/app/app-alb/abc \
  --protocol HTTPS --port 443 \
  --certificates CertificateArn=arn:aws:acm:...:certificate/xyz \
  --default-actions Type=forward,TargetGroupArn=arn:aws:elasticloadbalancing:...:targetgroup/app-tg/def

aws elbv2 describe-target-health --target-group-arn arn:aws:elasticloadbalancing:...:targetgroup/app-tg/def
Section 08

Examples

A complete deployment of a load-balanced, auto-scaled application with a managed database backend, built up in order:

  1. Provision networking

    Create the VPC, public/private-app/private-db subnets across two AZs, an Internet Gateway, a NAT Gateway, and route tables as shown in Configuration.
  2. Create the launch template

    Define an EC2 launch template with the AMI, instance type, security group, instance profile, and user-data bootstrap script the app servers will run.
  3. Create the target group and ALB

    aws elbv2 create-target-group with a /healthz health check, then create-load-balancer and create-listener on port 443 with the ACM certificate attached.
  4. Create the Auto Scaling Group

    Attach the launch template to an ASG spanning both private-app subnets, wired to the target group, with min/desired/max capacity and a target-tracking scaling policy on CPU utilization.
  5. Launch RDS Multi-AZ

    Create the DB subnet group from the two private-db subnets, then create-db-instance --multi-azwith the app tier's security group as the only allowed ingress source.
  6. Point DNS at the ALB

    Create a Route 53 alias A record for app.example.comtargeting the ALB's DNS name and hosted zone ID, then confirm end-to-end with curl -I https://app.example.com.
Section 09

Screenshots

EC2 console — target group showing all instances in the 'healthy' state behind the ALB

Screenshot placeholder

CloudWatch dashboard — CPUUtilization and RDS FreeableMemory graphed alongside the ALB's HealthyHostCount alarm

Screenshot placeholder

Section 10

Real World Example

Scenario: launch a production order-processing API for a mid-traffic e-commerce backend, with zero single points of failure and full observability from day one.

  1. IAM foundation

    Create an AppServerRolewith least-privilege access to the app's S3 bucket and its own CloudWatch log group only, wrapped in an instance profile — no long-lived access keys anywhere in the fleet.
  2. Network and compute

    Stand up the 3-tier VPC, then an Auto Scaling Group of t3.small app servers across two AZs behind an ALB, with security groups chained ALB → app → db and no direct internet route to either private tier.
  3. Database

    Launch a Multi-AZ RDS PostgreSQL instance with automated backups, a read replica for reporting queries, and a custom parameter group tuned for connection limits.
  4. Observability

    Ship application logs to a CloudWatch log group, add alarms on ALB 5xx rate, target group unhealthy host count, and RDS FreeStorageSpace, each notifying an SNS topic that pages on-call.
  5. DNS and cutover

    Create the Route 53 alias record pointing at the ALB, verify health checks are green, then cut production traffic over during a low-traffic window and watch the CloudWatch dashboard for the first hour.
Troubleshooting

Common Issues

Security group misconfiguration

A connection that times out (rather than an immediate refusal) almost always points to a security group or NACL blocking the port, not the application. Check the security group's inbound rules first — remember they are stateful, so only inbound rules typically need adjusting for a client-initiated connection.

IAM AccessDenied errors

Read the exact Action and Resource named in the error message — IAM denials are usually a missing statement, not a broken one. Use aws iam simulate-principal-policyto test a specific action/resource pair against the caller's actual attached policies before guessing.

ALB target group showing unhealthy

Confirm the health check path returns a 200 on the exact port configured on the target group (not the listener port), and that the app security group allows inbound traffic from the ALB's security group on that port — a common cause is the health check path requiring auth or hitting a route that doesn't exist.

RDS connection timeout from the app tier

Verify the app server sits in a subnet whose route table can reach the DB subnet (both inside the same VPC), and that the RDS security group's inbound rule sources the app tier's security group, not a stale CIDR block or the wrong SG entirely.

ACM certificate region mismatch for CloudFront

CloudFront only accepts ACM certificates issued in us-east-1, regardless of which region the distribution or its origin lives in. A certificate requested in any other region simply won't appear in the CloudFront certificate picker.
Best Practices

Best Practices

  • Use infrastructure as code (Terraform/CloudFormation) instead of manually clicking through the console for anything beyond a quick experiment.
  • Tag every resource consistently (Environment, Owner, Project) for cost allocation and cleanup.
  • Prefer managed services (RDS over self-hosted MySQL on EC2) whenever operational overhead outweighs the control you'd gain.
  • Spread stateful and stateless resources across at least two Availability Zones by default, not as an afterthought.
  • Set a billing alarm and AWS Budgets alert before any account does meaningful work.
  • Rotate IAM access keys and prefer roles/OIDC federation for CI/CD instead of long-lived credentials.
Security

Security Hardening

  • Least privilege IAM — scope every policy to specific actions and resource ARNs; avoid wildcard Action: * or Resource: * outside of break-glass roles.
  • MFA everywhere — enforce MFA on the root account and on every IAM user with console access, and require it in policy conditions for destructive actions.
  • Security groups as default-deny — start from no inbound rules and add only what's required, sourced from other security groups rather than broad CIDR ranges wherever possible.
  • Encryption in transit and at rest — TLS on every ALB listener and RDS connection; enable EBS and RDS storage encryption with KMS by default.
  • CloudTrail — enable an organization-wide trail logging to a locked-down S3 bucket so every API call is auditable after the fact.
  • No public subnets for app or DB tiers — only load balancers and NAT gateways should have a route to an Internet Gateway.
Interview Questions

Interview Questions

A user is a persistent identity with its own credentials. A group is a collection of users sharing attached policies — it has no credentials of its own. A role has no long-term credentials at all; it's assumed temporarily (via STS) by a user, service, or another AWS account, producing short-lived credentials that expire automatically.
Cheat Sheet

Cheat Sheet

aws-cheatsheet.sh
# --- Identity ---
aws sts get-caller-identity
aws configure --profile <name>

# --- IAM ---
aws iam create-role --role-name <r> --assume-role-policy-document file://trust.json
aws iam put-role-policy --role-name <r> --policy-name <p> --policy-document file://policy.json
aws iam simulate-principal-policy --policy-source-arn <arn> --action-names <action>

# --- EC2 ---
aws ec2 run-instances --image-id <ami> --instance-type t3.small --key-name <kp> \
  --subnet-id <subnet> --security-group-ids <sg> --iam-instance-profile Name=<profile>
aws ec2 describe-instances --filters "Name=tag:Name,Values=<name>"
aws ec2 create-snapshot --volume-id <vol>

# --- VPC ---
aws ec2 create-vpc --cidr-block 10.0.0.0/16
aws ec2 create-subnet --vpc-id <vpc> --cidr-block <cidr> --availability-zone <az>
aws ec2 create-nat-gateway --subnet-id <subnet> --allocation-id <eip>
aws ec2 authorize-security-group-ingress --group-id <sg> --protocol tcp --port <p> --source-group <sg2>

# --- S3 ---
aws s3 sync ./build s3://<bucket> --delete
aws s3api put-bucket-versioning --bucket <bucket> --versioning-configuration Status=Enabled

# --- RDS ---
aws rds create-db-instance --db-instance-identifier <id> --engine postgres --multi-az ...
aws rds create-db-snapshot --db-instance-identifier <id> --db-snapshot-identifier <snap>
aws rds create-db-instance-read-replica --db-instance-identifier <replica> --source-db-instance-identifier <id>

# --- CloudWatch ---
aws cloudwatch put-metric-alarm --alarm-name <name> --namespace AWS/EC2 --metric-name CPUUtilization ...
aws logs tail /app/prod --follow

# --- ALB ---
aws elbv2 create-target-group --name <tg> --protocol HTTP --port 8080 --vpc-id <vpc>
aws elbv2 create-listener --load-balancer-arn <arn> --protocol HTTPS --port 443 --certificates CertificateArn=<arn>
aws elbv2 describe-target-health --target-group-arn <arn>

# --- Route53 / CloudFront / ACM ---
aws route53 change-resource-record-sets --hosted-zone-id <zone> --change-batch file://upsert.json
aws acm request-certificate --domain-name <domain> --validation-method DNS --region us-east-1
aws cloudfront create-invalidation --distribution-id <id> --paths "/*"
Summary

Summary

The core AWS stack a DevOps engineer runs daily is smaller than the service catalog suggests: IAM identities and least-privilege policies, a VPC with clean public/private tiering, EC2 behind an Auto Scaling Group and ALB, S3 for durable object storage, RDS for a managed relational database with Multi-AZ failover, and CloudWatch tying it all together with metrics, logs, and alarms. Master these seven services and their interactions — not every AWS product — and you can design, deploy, and troubleshoot the overwhelming majority of production architectures.

Resources

Resources

  • AWS official documentation — docs.aws.amazon.com
  • AWS Well-Architected Framework — aws.amazon.com/architecture/well-architected
  • AWS CLI command reference — awscli.amazonaws.com
  • AWS Architecture Center (reference architectures) — aws.amazon.com/architecture
  • AWS Skill Builder (free official training) — skillbuilder.aws