Skip to content
Merged
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
50 changes: 50 additions & 0 deletions cfn/cleanup-bucket.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/bin/bash
# Empty and delete the shared tutorial S3 bucket, then delete the CloudFormation stack.
# Usage: ./cfn/cleanup-bucket.sh
set -eo pipefail

STACK_NAME="tutorial-prereqs-bucket"

BUCKET_NAME=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" \
--query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text 2>/dev/null)

if [ -z "$BUCKET_NAME" ] || [ "$BUCKET_NAME" = "None" ]; then
echo "No bucket stack found."
exit 0
fi

echo "Bucket: $BUCKET_NAME"
echo ""
echo "Contents:"
aws s3 ls "s3://$BUCKET_NAME/" 2>/dev/null || echo " (empty)"
echo ""
echo "This will permanently delete all objects and the bucket itself."
read -rp "Type the bucket name to confirm: " CONFIRM

if [ "$CONFIRM" != "$BUCKET_NAME" ]; then
echo "Bucket name does not match. Aborting."
exit 1
fi

echo ""
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

aws s3api list-object-versions --bucket "$BUCKET_NAME" \
--query '{Objects: DeleteMarkers[].{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 "Deleting bucket: $BUCKET_NAME"
aws s3api delete-bucket --bucket "$BUCKET_NAME"

echo "Deleting stack: $STACK_NAME"
aws cloudformation delete-stack --stack-name "$STACK_NAME"
aws cloudformation wait stack-delete-complete --stack-name "$STACK_NAME"

echo "Done."
26 changes: 26 additions & 0 deletions cfn/prereq-bucket.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
AWSTemplateFormatVersion: '2010-09-09'
Description: >-
Shared S3 bucket reference for tutorials. The bucket is created and deleted
by setup-bucket.sh and cleanup-bucket.sh. This stack just exports the name
so other tutorial stacks can import it.

Parameters:
BucketName:
Type: String
Description: Name of the bucket created by setup-bucket.sh

Resources:
Placeholder:
Type: AWS::CloudFormation::WaitConditionHandle

Outputs:
BucketName:
Description: Name of the shared tutorial bucket
Value: !Ref BucketName
Export:
Name: !Sub '${AWS::StackName}-BucketName'
BucketArn:
Description: ARN of the shared tutorial bucket
Value: !Sub 'arn:aws:s3:::${BucketName}'
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'
46 changes: 46 additions & 0 deletions cfn/setup-bucket.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/bin/bash
# Create the shared tutorial S3 bucket and register it with CloudFormation.
# Usage: ./cfn/setup-bucket.sh
set -eo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
STACK_NAME="tutorial-prereqs-bucket"
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
REGION=$(aws configure get region 2>/dev/null || echo "us-east-1")
BUCKET_NAME="tutorial-bucket-${ACCOUNT_ID}-${REGION}"

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

if [ "$STATUS" = "CREATE_COMPLETE" ] || [ "$STATUS" = "UPDATE_COMPLETE" ]; then
EXISTING=$(aws cloudformation describe-stacks --stack-name "$STACK_NAME" \
--query 'Stacks[0].Outputs[?OutputKey==`BucketName`].OutputValue' --output text)
echo "Bucket already exists: $EXISTING"
exit 0
fi

echo "Creating bucket: $BUCKET_NAME"
if [ "$REGION" = "us-east-1" ]; then
aws s3api create-bucket --bucket "$BUCKET_NAME"
else
aws s3api create-bucket --bucket "$BUCKET_NAME" \
--create-bucket-configuration LocationConstraint="$REGION"
fi

aws s3api put-bucket-encryption --bucket "$BUCKET_NAME" \
--server-side-encryption-configuration \
'{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}'

aws s3api put-public-access-block --bucket "$BUCKET_NAME" \
--public-access-block-configuration \
'BlockPublicAcls=true,BlockPublicPolicy=true,IgnorePublicAcls=true,RestrictPublicBuckets=true'

echo "Registering bucket with CloudFormation stack: $STACK_NAME"
aws cloudformation deploy \
--template-file "$SCRIPT_DIR/prereq-bucket.yaml" \
--stack-name "$STACK_NAME" \
--parameter-overrides "BucketName=$BUCKET_NAME"

echo "Done. Bucket: $BUCKET_NAME"
echo "Other stacks can import: !ImportValue ${STACK_NAME}-BucketName"
Loading
Loading