Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions cfn/prereq-bucket.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: >-
Shared S3 bucket for tutorial artifacts. Deploy once, reference from other
tutorial stacks via cross-stack export. Empty the bucket before deleting.

Resources:
TutorialBucket:
Type: AWS::S3::Bucket
DeletionPolicy: Delete
Properties:
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionRule:
ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
Tags:
- Key: tutorial
Value: shared-bucket

Outputs:
BucketName:
Description: Name of the shared tutorial bucket
Value: !Ref TutorialBucket
Export:
Name: !Sub '${AWS::StackName}-BucketName'
BucketArn:
Description: ARN of the shared tutorial bucket
Value: !GetAtt TutorialBucket.Arn
Export:
Name: !Sub '${AWS::StackName}-BucketArn'
83 changes: 83 additions & 0 deletions cfn/prereq-vpc-private.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: >-
Shared VPC with private subnets only across two AZs.
No internet gateway or NAT gateway. Use VPC endpoints for service access.
Deploy once, reference from tutorial stacks via cross-stack exports.

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.1.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}'

PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.1.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-private-1'

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.1.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-private-2'

PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC

PrivateSubnet1RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable

PrivateSubnet2RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable

S3Endpoint:
Type: AWS::EC2::VPCEndpoint
Properties:
VpcId: !Ref VPC
ServiceName: !Sub 'com.amazonaws.${AWS::Region}.s3'
RouteTableIds:
- !Ref PrivateRouteTable

Outputs:
VpcId:
Value: !Ref VPC
Export:
Name: !Sub '${AWS::StackName}-VpcId'
PrivateSubnet1Id:
Value: !Ref PrivateSubnet1
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet1'
PrivateSubnet2Id:
Value: !Ref PrivateSubnet2
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet2'
PrivateSubnets:
Value: !Join [',', [!Ref PrivateSubnet1, !Ref PrivateSubnet2]]
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnets'
PrivateRouteTableId:
Value: !Ref PrivateRouteTable
Export:
Name: !Sub '${AWS::StackName}-PrivateRouteTable'
159 changes: 159 additions & 0 deletions cfn/prereq-vpc-public.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: >-
Shared VPC with public and private subnets across two AZs.
Includes internet gateway, NAT gateway, and route tables.
Deploy once, reference from tutorial stacks via cross-stack exports.

Resources:
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}'

InternetGateway:
Type: AWS::EC2::InternetGateway

GatewayAttachment:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway

PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.1.0/24
AvailabilityZone: !Select [0, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-public-1'

PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.2.0/24
AvailabilityZone: !Select [1, !GetAZs '']
MapPublicIpOnLaunch: true
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-public-2'

PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.3.0/24
AvailabilityZone: !Select [0, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-private-1'

PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
CidrBlock: 10.0.4.0/24
AvailabilityZone: !Select [1, !GetAZs '']
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-private-2'

NatEip:
Type: AWS::EC2::EIP
DependsOn: GatewayAttachment

NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NatEip.AllocationId
SubnetId: !Ref PublicSubnet1
Tags:
- Key: Name
Value: !Sub '${AWS::StackName}-nat'

PublicRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC

PublicRoute:
Type: AWS::EC2::Route
DependsOn: GatewayAttachment
Properties:
RouteTableId: !Ref PublicRouteTable
DestinationCidrBlock: 0.0.0.0/0
GatewayId: !Ref InternetGateway

PublicSubnet1RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet1
RouteTableId: !Ref PublicRouteTable

PublicSubnet2RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PublicSubnet2
RouteTableId: !Ref PublicRouteTable

PrivateRouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref VPC

PrivateRoute:
Type: AWS::EC2::Route
Properties:
RouteTableId: !Ref PrivateRouteTable
DestinationCidrBlock: 0.0.0.0/0
NatGatewayId: !Ref NatGateway

PrivateSubnet1RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet1
RouteTableId: !Ref PrivateRouteTable

PrivateSubnet2RouteAssoc:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref PrivateSubnet2
RouteTableId: !Ref PrivateRouteTable

Outputs:
VpcId:
Value: !Ref VPC
Export:
Name: !Sub '${AWS::StackName}-VpcId'
PublicSubnet1Id:
Value: !Ref PublicSubnet1
Export:
Name: !Sub '${AWS::StackName}-PublicSubnet1'
PublicSubnet2Id:
Value: !Ref PublicSubnet2
Export:
Name: !Sub '${AWS::StackName}-PublicSubnet2'
PrivateSubnet1Id:
Value: !Ref PrivateSubnet1
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet1'
PrivateSubnet2Id:
Value: !Ref PrivateSubnet2
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnet2'
PublicSubnets:
Value: !Join [',', [!Ref PublicSubnet1, !Ref PublicSubnet2]]
Export:
Name: !Sub '${AWS::StackName}-PublicSubnets'
PrivateSubnets:
Value: !Join [',', [!Ref PrivateSubnet1, !Ref PrivateSubnet2]]
Export:
Name: !Sub '${AWS::StackName}-PrivateSubnets'
95 changes: 95 additions & 0 deletions cleanup.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/bin/bash
# Delete a tutorial's CloudFormation stack and optionally clean up prerequisites.
# Usage: ./cleanup.sh <tutorial-dir>
# ./cleanup.sh --prereqs # delete prerequisite stacks
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PREREQ_STACK="tutorial-prereqs"

if [ "$1" = "--prereqs" ]; then
echo "=== Prerequisite stacks ==="
for STACK in $(aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE UPDATE_COMPLETE \
--query "StackSummaries[?starts_with(StackName, '$PREREQ_STACK')].StackName" --output text 2>/dev/null); do
echo " $STACK"
done

echo ""
echo "Prerequisite stacks are shared across tutorials."
echo "Only delete them when you're done with all tutorials."
read -rp "Delete all prerequisite stacks? (y/n): " CHOICE
[[ ! "$CHOICE" =~ ^[Yy]$ ]] && exit 0

# Handle bucket prereq — must empty first
BUCKET_NAME=$(aws cloudformation describe-stacks --stack-name "$PREREQ_STACK-bucket" \
--query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text 2>/dev/null)
if [ -n "$BUCKET_NAME" ] && [ "$BUCKET_NAME" != "None" ]; then
OBJ_COUNT=$(aws s3api list-objects-v2 --bucket "$BUCKET_NAME" --query 'KeyCount' --output text 2>/dev/null || echo "0")
if [ "$OBJ_COUNT" -gt 0 ] 2>/dev/null; then
echo ""
echo "Bucket $BUCKET_NAME contains $OBJ_COUNT objects."
read -rp "Empty the bucket? (y/n): " EMPTY
if [[ "$EMPTY" =~ ^[Yy]$ ]]; then
echo "Emptying bucket..."
aws s3 rm "s3://$BUCKET_NAME" --recursive --quiet
aws s3api list-object-versions --bucket "$BUCKET_NAME" \
--query '{Objects: Versions[].{Key:Key,VersionId:VersionId}, Quiet: true}' \
--output json 2>/dev/null | \
aws s3api delete-objects --bucket "$BUCKET_NAME" --delete file:///dev/stdin > /dev/null 2>&1 || true
echo " Emptied"
else
echo "Cannot delete bucket stack while bucket has objects."
exit 1
fi
fi
aws cloudformation delete-stack --stack-name "$PREREQ_STACK-bucket"
echo "Deleting $PREREQ_STACK-bucket..."
aws cloudformation wait stack-delete-complete --stack-name "$PREREQ_STACK-bucket" 2>/dev/null
echo " Deleted"
fi

# Handle VPC prereqs — delete cleanly unless tutorial stacks still reference them
for VPC_TYPE in vpc-public vpc-private; do
VPC_STACK="$PREREQ_STACK-$VPC_TYPE"
STATUS=$(aws cloudformation describe-stacks --stack-name "$VPC_STACK" --query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NONE")
if [ "$STATUS" != "NONE" ] && [ "$STATUS" != "DELETE_COMPLETE" ]; then
echo "Deleting $VPC_STACK..."
aws cloudformation delete-stack --stack-name "$VPC_STACK"
aws cloudformation wait stack-delete-complete --stack-name "$VPC_STACK" 2>/dev/null && echo " Deleted" || echo " Failed (other stacks may still import from it)"
fi
done
exit 0
fi

# Delete a tutorial stack
TUT_DIR="$1"
[ -z "$TUT_DIR" ] && echo "Usage: $0 <tutorial-dir> | --prereqs" && exit 1

STACK_NAME="tutorial-$(echo "$TUT_DIR" | sed 's/^[0-9]*-//')"

# Check if stack exists
STATUS=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" \
--query 'Stacks[0].StackStatus' --output text 2>/dev/null || echo "NONE")

if [ "$STATUS" = "NONE" ] || [ "$STATUS" = "DELETE_COMPLETE" ]; then
echo "Stack $STACK_NAME does not exist."
exit 0
fi

echo "Stack: $STACK_NAME (status: $STATUS)"
echo "Resources:"
aws cloudformation list-stack-resources --stack-name "$STACK_NAME" \
--query 'StackResourceSummaries[].{Type:ResourceType,LogicalId:LogicalResourceId,Status:ResourceStatus}' --output table

echo ""
read -rp "Delete stack $STACK_NAME? (y/n): " CHOICE
[[ ! "$CHOICE" =~ ^[Yy]$ ]] && exit 0

echo "Deleting..."
aws cloudformation delete-stack --stack-name "$STACK_NAME"
aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"
echo "Stack $STACK_NAME deleted."

echo ""
echo "Note: Prerequisite stacks are still running. To delete them:"
echo " $0 --prereqs"
Loading
Loading