AWS, GCP, Azure & serverless — 8 comprehensive lessons covering infrastructure, containers, IaC, and cloud security with real CLI examples.
Cloud services are categorized into three tiers based on the level of abstraction from raw infrastructure:
IaaS — You manage: OS, runtime, apps, data
Provider manages: Virtualization, servers, storage, networking
Example: AWS EC2, Azure VMs, GCP Compute Engine
PaaS — You manage: Applications, data
Provider manages: OS, runtime, middleware, servers
Example: AWS Elastic Beanstalk, Heroku, Google App Engine
SaaS — You manage: Nothing (just use it)
Provider manages: Everything
Example: Gmail, Salesforce, Slack
A region is a geographic area. An Availability Zone (AZ) is one or more isolated data centers within a region, connected by low-latency links.
us-east-1 (N. Virginia)
├── us-east-1a (AZ 1)
├── us-east-1b (AZ 2)
├── us-east-1c (AZ 3)
├── us-east-1d (AZ 4)
└── us-east-1f (AZ 6)
us-west-2 (Oregon)
├── us-west-2a
├── us-west-2b
└── us-west-2c
On-Demand — Pay per second/minute/hour, no commitment
Best for: dev/test, unpredictable workloads
Reserved — 1-3 year commitment, 30-75% discount
Best for: steady-state, predictable usage
Spot — Bid for unused capacity, up to 90% discount
Best for: batch jobs, fault-tolerant workloads
Risk: can be reclaimed with 2-min notice
Dedicated — Single-tenant physical server
Best for: compliance, licensing requirements
IaaS PaaS SaaS
────── ────── ──────
Your Data ✅ ✅ ✅
Your Apps ✅ ✅ ❌
Your Runtime ✅ ❌ ❌
Your OS ✅ ❌ ❌
Your Network ✅ ❌ ❌
Virtualization ❌ ❌ ❌
Physical Hosts ❌ ❌ ❌
Physical Network ❌ ❌ ❌
Physical Storage ❌ ❌ ❌
AWS — Largest market share, broadest service catalog
Billing: per-second (EC2), per-hour (others)
33 regions, 105 AZs
GCP — Strong in data/analytics/AI, Kubernetes-native
Billing: per-second (VMs), sustained-use discounts
40 regions, 121 zones
Azure — Best enterprise integration (Active Directory, Office 365)
Billing: per-minute (VMs), reserved instances
60+ regions, 190+ DCs
Virtual servers in the cloud. You pick the instance type (CPU, RAM, storage, network) and pay by the second.
# Create a key pair
aws ec2 create-key-pair \
--key-name my-key \
--query 'KeyMaterial' \
--output text > my-key.pem
# Launch a t3.micro instance (2 vCPU, 1 GB RAM)
aws ec2 run-instances \
--image-id ami-0c55b159cbfafe1f0 \
--instance-type t3.micro \
--key-name my-key \
--security-group-ids sg-0abc1234def567890 \
--subnet-id subnet-0abc1234 \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=web-server}]'
# List instances
aws ec2 describe-instances \
--query 'Reservations[*].Instances[*].[InstanceId,State.Name,PublicIpAddress]' \
--output table
# Stop / Start / Terminate
aws ec2 stop-instances --instance-ids i-0abc1234def567890
aws ec2 start-instances --instance-ids i-0abc1234def567890
aws ec2 terminate-instances --instance-ids i-0abc1234def567890
Object storage with 11 nines of durability. Unlimited storage, pay per GB.
# Create a bucket
aws s3 mb s3://my-unique-bucket-name
# Upload a file
aws s3 cp ./index.html s3://my-bucket/index.html
# Sync a directory
aws s3 sync ./dist s3://my-bucket/static/ --delete
# List bucket contents
aws s3 ls s3://my-bucket/
# Set bucket policy (public read)
aws s3api put-bucket-policy \
--bucket my-bucket \
--policy '{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}'
# Enable versioning
aws s3api put-bucket-versioning \
--bucket my-bucket \
--versioning-configuration Status=Enabled
# Create an IAM user
aws iam create-user --user-name deploy-bot
# Create access key for programmatic access
aws iam create-access-key --user-name deploy-bot
# Attach a managed policy
aws iam attach-user-policy \
--user-name deploy-bot \
--policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
# Create a custom policy (least privilege)
aws iam put-user-policy \
--user-name deploy-bot \
--policy-name S3BucketAccess \
--policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::my-bucket/*"
}]
}'
# Create an IAM role for EC2
aws iam create-role \
--role-name EC2S3Access \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "ec2.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}'
# Create a VPC
aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=my-vpc}]'
# Create a subnet
aws ec2 create-subnet \
--vpc-id vpc-0abc1234 \
--cidr-block 10.0.1.0/24 \
--availability-zone us-east-1a
# Create an Internet Gateway and attach
aws ec2 create-internet-gateway
aws ec2 attach-internet-gateway \
--internet-gateway-id igw-0abc1234 \
--vpc-id vpc-0abc1234
# Create a Security Group (web server rules)
aws ec2 create-security-group \
--group-name web-sg \
--description "Web server SG" \
--vpc-id vpc-0abc1234
# Allow HTTP
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc1234 \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
# Allow SSH (restrict to your IP)
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc1234 \
--protocol tcp \
--port 22 \
--cidr 203.0.113.50/32
# Allow HTTPS
aws ec2 authorize-security-group-ingress \
--group-id sg-0abc1234 \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
--query 'KeyMaterial' --output text when creating key pairs — AWS only shows the private key once. Store it in ~/.ssh/ and set permissions to chmod 400.
Run code without provisioning servers. Pay only for compute time used (rounded to the nearest millisecond).
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('Visitors')
def lambda_handler(event, context):
# Parse request
body = json.loads(event.get('body', '{}'))
path = event.get('path', '/')
if event['httpMethod'] == 'GET' and path == '/count':
response = table.get_item(Key={'id': 'counter'})
count = response.get('Item', {}).get('count', 0)
return {
'statusCode': 200,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'count': count})
}
if event['httpMethod'] == 'POST' and path == '/increment':
response = table.update_item(
Key={'id': 'counter'},
UpdateExpression='SET #c = if_not_exists(#c, :zero) + :inc',
ExpressionAttributeNames={'#c': 'count'},
ExpressionAttributeValues={':inc': 1, ':zero': 0},
ReturnValues='ALL_NEW'
)
return {
'statusCode': 200,
'body': json.dumps({'count': response['Attributes']['count']})
}
return {'statusCode': 404, 'body': json.dumps({'error': 'Not found'})}
# Create the function
aws lambda create-function \
--function-name visitor-counter \
--runtime python3.12 \
--role arn:aws:iam::123456789012:role/LambdaDynamoRole \
--handler index.lambda_handler \
--zip-file fileb://function.zip \
--timeout 10 \
--memory-size 128
# Update function code
aws lambda update-function-code \
--function-name visitor-counter \
--zip-file fileb://function.zip
# Invoke locally for testing
aws lambda invoke \
--function-name visitor-counter \
--payload '{"httpMethod":"GET","path":"/count"}' \
output.json
cat output.json
# Create a REST API
aws apigateway create-rest-api \
--name 'VisitorAPI' \
--description 'Simple visitor counter API'
# Create resource (/count)
aws apigateway create-resource \
--rest-api-id abc123 \
--parent-id \
--path-part 'count'
# Create GET method
aws apigateway put-method \
--rest-api-id abc123 \
--resource-id def456 \
--http-method GET \
--authorization-type NONE
# Set Lambda integration
aws apigateway put-integration \
--rest-api-id abc123 \
--resource-id def456 \
--http-method GET \
--type AWS_PROXY \
--integration-http-method POST \
--uri arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:visitor-counter/invocations
# Deploy to stage
aws apigateway create-deployment \
--rest-api-id abc123 \
--stage-name prod
# Grant API Gateway permission to invoke Lambda
aws lambda add-permission \
--function-name visitor-counter \
--statement-id apigateway-invoke \
--action lambda:InvokeFunction \
--principal apigateway.amazonaws.com \
--source-arn "arn:aws:execute-api:us-east-1:123456789012:abc123/*"
# Create a table
aws dynamodb create-table \
--table-name Visitors \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST \
--table-class STANDARD
# Put an item
aws dynamodb put-item \
--table-name Visitors \
--item '{"id": {"S": "counter"}, "count": {"N": "0"}}'
# Get an item
aws dynamodb get-item \
--table-name Visitors \
--key '{"id": {"S": "counter"}}'
# Query with conditions
aws dynamodb query \
--table-name Visitors \
--key-condition-expression "id = :id" \
--expression-attribute-values '{":id": {"S": "counter"}}'
# Scan all items
aws dynamodb scan --table-name Visitors
# S3 bucket notification triggers Lambda on upload
aws s3api put-bucket-notification-configuration \
--bucket my-upload-bucket \
--notification-configuration '{
"LambdaFunctionConfigurations": [{
"Events": ["s3:ObjectCreated:*"],
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:process-upload"
}]
}'
# The Lambda function processes the S3 event:
# {
# "Records": [{
# "eventSource": "aws:s3",
# "s3": {
# "bucket": {"name": "my-upload-bucket"},
# "object": {"key": "uploads/data.csv", "size": 1024}
# }
# }]
# }
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]
# Login to ECR
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com
# Create repository
aws ecr create-repository --repository-name my-app
# Build, tag, push
docker build -t my-app .
docker tag my-app:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
# Create ECS cluster
aws ecs create-cluster --cluster-name my-cluster
# Register task definition (Fargate)
aws ecs register-task-definition \
--family my-app-task \
--network-mode awsvpc \
--requires-compatibilities FARGATE \
--cpu '256' \
--memory '512' \
--execution-role-arn arn:aws:iam::123456789012:role/ecsTaskExecutionRole \
--container-definitions '[
{
"name": "my-app",
"image": "123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest",
"portMappings": [{"containerPort": 3000, "protocol": "tcp"}],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs"
}
}
}
]'
# Run the service
aws ecs run-task \
--cluster my-cluster \
--task-definition my-app-task \
--launch-type FARGATE \
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-0abc1234"],
"securityGroups": ["sg-0abc1234"],
"assignPublicIp": "ENABLED"
}
}'
# Create a service (keeps desired count running)
aws ecs create-service \
--cluster my-cluster \
--service-name my-app-service \
--task-definition my-app-task \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration '{
"awsvpcConfiguration": {
"subnets": ["subnet-0abc1234"],
"securityGroups": ["sg-0abc1234"],
"assignPublicIp": "ENABLED"
}
}'
# Build and deploy directly from source
gcloud run deploy my-app \
--source . \
--platform managed \
--region us-central1 \
--allow-unauthenticated \
--memory 512Mi \
--cpu 1 \
--min-instances 0 \
--max-instances 10
# Deploy a pre-built container image
gcloud run deploy my-app \
--image gcr.io/my-project/my-app:latest \
--platform managed \
--region us-central1 \
--port 3000
# Set environment variables
gcloud run services update my-app \
--set-env-vars="DATABASE_URL=postgres://...,NODE_ENV=production"
# View logs
gcloud run services logs read my-app --limit 50
ECS Fargate:
min/max per service
Target tracking: CPU > 70% → scale up
Scale-out: ~60-90 seconds
Scale-in: ~5-15 minutes (cooldown)
Cloud Run:
min-instances: 0 (scale to zero!)
max-instances: 1000
Scale-out: ~100ms per instance
Concurrency: 1-1000 requests per instance
EKS (Kubernetes HPA):
minReplicas / maxReplicas
Metrics: CPU, memory, custom (Prometheus)
Scale-out: ~30 seconds (depends on pods pending)
# Create a VM
gcloud compute instances create my-vm \
--zone=us-central1-a \
--machine-type=e2-micro \
--image-family=debian-12 \
--image-project=debian-cloud \
--boot-disk-size=20GB \
--tags=http-server,https-server
# SSH into the VM
gcloud compute ssh my-vm --zone=us-central1-a
# List all instances
gcloud compute instances list
# Stop / Start / Delete
gcloud compute instances stop my-vm --zone=us-central1-a
gcloud compute instances start my-vm --zone=us-central1-a
gcloud compute instances delete my-vm --zone=us-central1-a
# Create a disk and attach
gcloud compute disks create data-disk \
--size=100GB \
--type=pd-balanced \
--zone=us-central1-a
gcloud compute instances attach-disk my-vm \
--disk=data-disk \
--zone=us-central1-a
# Create a bucket (globally unique name)
gsutil mb -l us-central1 gs://my-unique-bucket-name
# Upload files
gsutil cp ./index.html gs://my-bucket/
gsutil -m cp -r ./static/* gs://my-bucket/static/
# Make publicly readable
gsutil iam ch allUsers:objectViewer gs://my-bucket
# Set lifecycle rule (delete after 90 days)
gsutil lifecycle set lifecycle.json gs://my-bucket
# lifecycle.json content:
# {
# "rule": [{
# "action": {"type": "Delete"},
# "condition": {"age": 90}
# }]
# }
# Generate a signed URL (temporary access)
gsutil signurl -d 1h service-account-key.json gs://my-bucket/file.pdf
# Deploy HTTP-triggered function (Gen 2)
gcloud functions deploy hello-http \
--gen2 \
--runtime=nodejs20 \
--region=us-central1 \
--source=./function-source \
--entry-point=helloHttp \
--trigger-http \
--allow-unauthenticated \
--memory=256MB
# Deploy event-triggered function
gcloud functions deploy process-upload \
--gen2 \
--runtime=nodejs20 \
--region=us-central1 \
--source=./function-source \
--entry-point=processUpload \
--trigger-event-filters="type=google.cloud.storage.object.v1.finalized" \
--trigger-event-filters="bucket=my-upload-bucket"
# View logs
gcloud functions logs read hello-http --limit 20
# Query a public dataset
bq query --use_legacy_sql=false '
SELECT
name,
SUM(number) AS total,
MIN(year) AS first_appeared,
MAX(year) AS last_appeared
FROM `bigquery-public-data.usa_names.usa_1910_current`
WHERE state = "TX"
GROUP BY name
ORDER BY total DESC
LIMIT 10
'
# Create a dataset
bq mk --dataset my-project:analytics
# Load CSV into a table
bq load \
--source_format=CSV \
--autodetect \
analytics.sales_data \
./sales.csv
# Export to GCS
bq extract \
'my-project:analytics.sales_data' \
'gs://my-bucket/exports/sales_*.csv'
# Allow HTTP traffic
gcloud compute firewall-rules create allow-http \
--allow=tcp:80 \
--source-ranges=0.0.0.0/0 \
--target-tags=http-server
# Allow SSH from your IP
gcloud compute firewall-rules create allow-ssh \
--allow=tcp:22 \
--source-ranges=203.0.113.50/32 \
--target-tags=ssh-allowed
# List firewall rules
gcloud compute firewall-rules list
# Create a resource group
az group create --name myResourceGroup --location eastus
# Create a Linux VM
az vm create \
--resource-group myResourceGroup \
--name myVM \
--image Ubuntu2204 \
--size Standard_B2s \
--admin-username azureuser \
--ssh-key-value ~/.ssh/id_rsa.pub \
--public-ip-sku Standard
# List VMs
az vm list --output table
# Start / Stop / Restart
az vm start --resource-group myResourceGroup --name myVM
az vm stop --resource-group myResourceGroup --name myVM
az vm restart --resource-group myResourceGroup --name myVM
# Delete the VM
az vm delete --resource-group myResourceGroup --name myVM --yes
# Create a data disk
az vm disk attach \
--resource-group myResourceGroup \
--vm-name myVM \
--name data-disk \
--size-gb 100 \
--new
# Create a storage account
az storage account create \
--name mystorageaccount123 \
--resource-group myResourceGroup \
--location eastus \
--sku Standard_LRS
# Create a container
az storage container create \
--name mycontainer \
--account-name mystorageaccount123
# Upload a blob
az storage blob upload \
--account-name mystorageaccount123 \
--container-name mycontainer \
--name ./index.html \
--file ./index.html
# List blobs
az storage blob list \
--account-name mystorageaccount123 \
--container-name mycontainer \
--output table
# Generate a SAS URL (shared access signature)
az storage blob generate-sas \
--account-name mystorageaccount123 \
--container-name mycontainer \
--name index.html \
--permissions r \
--expiry 2026-12-31 \
--full-uri
# Create a Function App
az functionapp create \
--resource-group myResourceGroup \
--consumption-plan-location eastus \
--runtime node \
--runtime-version 20 \
--functions-version 4 \
--name myFunctionApp123 \
--storage-account mystorageaccount123
# Deploy from local directory
az functionapp deployment source config-zip \
--resource-group myResourceGroup \
--name myFunctionApp123 \
--src ./function-app.zip
# Configure app settings
az functionapp config appsettings set \
--resource-group myResourceGroup \
--name myFunctionApp123 \
--settings "DATABASE_URL=postgres://..." "API_KEY=secret123"
# View logs
az functionapp logs tail \
--resource-group myResourceGroup \
--name myFunctionApp123
# Create a service principal
az ad sp create-for-rbac \
--name myServicePrincipal \
--role Contributor \
--scopes /subscriptions/{subscription-id}
# Output:
# {
# "appId": "xxxxxxxx-xxxx-xxxx-xxxx",
# "displayName": "myServicePrincipal",
# "password": "xxxx-xxxx",
# "tenant": "xxxxxxxx-xxxx-xxxx-xxxx"
# }
# List service principals
az ad sp list --display-name myServicePrincipal
# Grant a role assignment
az role assignment create \
--assignee {app-id} \
--role "Storage Blob Data Contributor" \
--scope /subscriptions/{sub-id}/resourceGroups/myRG/providers/Microsoft.Storage/storageAccounts/mystorage
# Create an NSG
az network nsg create \
--resource-group myResourceGroup \
--name myNSG
# Allow HTTP
az network nsg rule create \
--resource-group myResourceGroup \
--nsg-name myNSG \
--name AllowHTTP \
--priority 100 \
--destination-port-ranges 80 \
--access Allow \
--protocol Tcp
# Allow SSH (restrict to IP)
az network nsg rule create \
--resource-group myResourceGroup \
--nsg-name myNSG \
--name AllowSSH \
--priority 110 \
--destination-port-ranges 22 \
--access Allow \
--protocol Tcp \
--source-address-prefixes 203.0.113.50/32
# Associate NSG with a subnet
az network vnet subnet update \
--resource-group myResourceGroup \
--vnet-name myVNet \
--name mySubnet \
--network-security-group myNSG
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc.tfstate"
region = "us-east-1"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
provider "aws" {
region = "us-east-1"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
}
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Allow HTTP and SSH"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["203.0.113.50/32"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.micro"
key_name = "my-key"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "web-server"
}
}
# Initialize (download providers)
terraform init
# Format config files
terraform fmt
# Validate syntax
terraform validate
# Preview changes
terraform plan
# Apply changes
terraform apply
# Destroy all resources
terraform destroy
# Show current state
terraform show
# List all resources in state
terraform state list
# Import existing resource
terraform import aws_instance.existing i-0abc1234def567890
AWSTemplateFormatVersion: '2010-09-09'
Description: Simple EC2 Instance
Parameters:
InstanceType:
Type: String
Default: t3.micro
AllowedValues:
- t3.micro
- t3.small
- t3.medium
Resources:
WebServer:
Type: AWS::EC2::Instance
Properties:
InstanceType: !Ref InstanceType
ImageId: ami-0c55b159cbfafe1f0
KeyName: my-key
SecurityGroups:
- !Ref WebSecurityGroup
Tags:
- Key: Name
Value: WebServer
WebSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Allow HTTP and SSH
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
- IpProtocol: tcp
FromPort: 22
ToPort: 22
CidrIp: 203.0.113.50/32
Outputs:
PublicIp:
Value: !Ref WebServer
Description: Public IP of the web server
# Deploy a stack
aws cloudformation deploy \
--template-file template.yaml \
--stack-name my-web-stack \
--parameter-overrides InstanceType=t3.small
# Update a stack
aws cloudformation update-stack \
--template-file template.yaml \
--stack-name my-web-stack
# Describe stack events (for debugging)
aws cloudformation describe-stack-events \
--stack-name my-web-stack
# Delete a stack
aws cloudformation delete-stack --stack-name my-web-stack
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const vpc = new aws.ec2.Vpc("main", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
enableDnsSupport: true,
tags: { Name: "main-vpc" },
});
const subnet = new aws.ec2.Subnet("public", {
vpcId: vpc.id,
cidrBlock: "10.0.1.0/24",
availabilityZone: "us-east-1a",
mapPublicIpOnLaunch: true,
tags: { Name: "public-subnet" },
});
const webSg = new aws.ec2.SecurityGroup("web", {
description: "Allow HTTP and SSH",
vpcId: vpc.id,
ingress: [
{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
{ protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["203.0.113.50/32"] },
],
egress: [
{ protocol: "-1", fromPort: 0, toPort: 0, cidrBlocks: ["0.0.0.0/0"] },
],
});
const web = new aws.ec2.Instance("web", {
ami: "ami-0c55b159cbfafe1f0",
instanceType: "t3.micro",
keyName: "my-key",
subnetId: subnet.id,
vpcSecurityGroupIds: [webSg.id],
tags: { Name: "web-server" },
});
export const publicIp = web.publicIp;
# Create S3 bucket for state
aws s3api create-bucket \
--bucket my-terraform-state \
--region us-east-1
# Enable versioning
aws s3api put-bucket-versioning \
--bucket my-terraform-state \
--versioning-configuration Status=Enabled
# Enable encryption
aws s3api put-bucket-encryption \
--bucket my-terraform-state \
--server-side-encryption-configuration '{
"Rules": [{"ApplyServerSideEncryptionByDefault": {"SSEAlgorithm": "AES256"}}]
}'
# Create DynamoDB table for state locking
aws dynamodb create-table \
--table-name terraform-locks \
--attribute-definitions AttributeName=LockID,AttributeType=S \
--key-schema AttributeName=LockID,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
plan before every apply.
# BAD: AdminAccess for everyone
# aws iam attach-user-policy --user-name dev --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
# GOOD: Scoped policy per role
aws iam create-policy \
--policy-name S3DeployPolicy \
--policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3Bucket",
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::my-app-bucket",
"arn:aws:s3:::my-app-bucket/*"
]
},
{
"Sid": "AllowCloudFrontInvalidation",
"Effect": "Allow",
"Action": "cloudfront:CreateInvalidation",
"Resource": "arn:aws:cloudfront::123456789012:distribution/E1234567890"
}
]
}'
# Use IAM Access Analyzer
aws accessanalyzer list-findings \
--analyzer-arn arn:aws:access-analyzer:us-east-1:123456789012:analyzer/ConsoleAnalyzer-xxx
# Store a secret
aws secretsmanager create-secret \
--name prod/database/credentials \
--secret-string '{
"username": "admin",
"password": "S3cureP@ss!",
"engine": "postgres",
"host": "mydb.abc123.us-east-1.rds.amazonaws.com",
"port": 5432
}'
# Retrieve a secret
aws secretsmanager get-secret-value \
--secret-id prod/database/credentials
# Rotate a secret (auto-rotation)
aws secretsmanager rotate-secret \
--secret-id prod/database/credentials \
--rotation-lambda-arn arn:aws:lambda:us-east-1:123456789012:function:rotate-db-password \
--rotation-rules '{"AutomaticallyAfterDays": 30}'
# Store parameters
aws ssm put-parameter \
--name "/prod/app/api-url" \
--value "https://api.myapp.com" \
--type "String"
aws ssm put-parameter \
--name "/prod/app/db-password" \
--value "S3cureP@ss!" \
--type "SecureString"
# Retrieve parameters
aws ssm get-parameter --name "/prod/app/api-url"
aws ssm get-parameter --name "/prod/app/db-password" --with-decryption
# Get all parameters under a path
aws ssm get-parameters-by-path \
--path "/prod/app" \
--recursive \
--with-decryption
# .github/workflows/deploy.yml
name: Deploy to AWS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build
run: npm run build
- name: Sync to S3
run: aws s3 sync ./dist s3://my-bucket/static/ --delete
- name: Invalidate CloudFront cache
run: |
aws cloudfront create-invalidation \
--distribution-id E1234567890 \
--paths "/*"
# Create a pipeline
aws codepipeline create-pipeline \
--pipeline '{
"name": "my-pipeline",
"roleArn": "arn:aws:iam::123456789012:role/CodePipelineRole",
"artifactStore": {
"type": "S3",
"location": "my-pipeline-artifacts"
},
"stages": [
{
"name": "Source",
"actions": [{
"name": "GitHub",
"actionTypeId": {"category": "Source", "owner": "ThirdParty", "provider": "GitHub", "version": "1"},
"configuration": {"Owner": "mayank-dev-15", "Repo": "my-app", "Branch": "main", "OAuthToken": "{{secrets:github-token}}"}
}]
},
{
"name": "Build",
"actions": [{
"name": "CodeBuild",
"actionTypeId": {"category": "Build", "owner": "AWS", "provider": "CodeBuild", "version": "1"},
"configuration": {"ProjectName": "my-build-project"}
}]
},
{
"name": "Deploy",
"actions": [{
"name": "Deploy",
"actionTypeId": {"category": "Deploy", "owner": "AWS", "provider": "ECS", "version": "1"},
"configuration": {"ClusterName": "my-cluster", "ServiceName": "my-service"}
}]
}
]
}'
# Put a custom metric
aws cloudwatch put-metric-data \
--namespace "MyApp" \
--metric-name "RequestCount" \
--value 42 \
--dimensions "Service=api"
# Create an alarm for high CPU
aws cloudwatch put-metric-alarm \
--alarm-name "high-cpu" \
--metric-name "CPUUtilization" \
--namespace "AWS/EC2" \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--evaluation-periods 2 \
--dimensions "Name=InstanceId,Value=i-0abc1234" \
--alarm-actions "arn:aws:sns:us-east-1:123456789012:alerts"
# Get logs
aws logs get-log-events \
--log-group-name "/ecs/my-app" \
--log-stream-name "ecs/my-app/abc123"
# Create a dashboard
aws cloudwatch put-dashboard \
--dashboard-name "MyApp" \
--dashboard-body '{
"widgets": [{
"type": "metric",
"x": 0, "y": 0, "width": 12, "height": 6,
"properties": {
"metrics": [["AWS/EC2", "CPUUtilization"]],
"period": 300,
"stat": "Average",
"title": "EC2 CPU"
}
}]
}'
✅ Compute
- CPU/Memory utilization (per instance/container)
- Disk I/O and free space
- Network in/out bytes
✅ Storage
- S3 bucket sizes and request counts
- EBS volume read/write ops and queue depth
✅ Database
- RDS connections, query latency, free storage
- DynamoDB read/write capacity and throttled requests
✅ Application
- Request latency (p50, p95, p99)
- Error rates (4xx, 5xx)
- Active connections
✅ Security
- CloudTrail API call logging
- GuardDuty threat findings
- IAM credential rotation status
- Unusual login patterns