Get started using Amazon VPC using the CLI - AWS SDK Code Examples

There are more AWS SDK examples available in the AWS Doc SDK Examples GitHub repo.

Get started using Amazon VPC using the CLI

The following code example shows how to:

  • Set up your account

  • Create and configure a VPC

  • Configure your network

  • Configure security

  • Deploy resources

  • Test and verify

  • Clean up resources

  • Consider production implications

  • Consider security implications

Bash
AWS CLI with Bash script
Note

There's more on GitHub. Find the complete example and learn how to set up and run in the Sample developer tutorials repository.

#!/bin/bash # VPC Creation Script # This script creates a VPC with public and private subnets, internet gateway, NAT gateway, and security groups # Set up logging LOG_FILE="vpc_creation.log" exec > >(tee -a "$LOG_FILE") 2>&1 # Function to handle errors handle_error() { echo "ERROR: $1" echo "Resources created before error:" for resource in "${CREATED_RESOURCES[@]}" do echo "- $resource" done echo "Attempting to clean up resources..." cleanup_resources exit 1 } # Function to clean up resources cleanup_resources() { echo "Cleaning up resources in reverse order..." # Reverse the array to delete in reverse order of creation for ((i=${#CREATED_RESOURCES[@]}-1; i>=0; i--)) do resource="${CREATED_RESOURCES[$i]}" resource_type=$(echo "$resource" | cut -d':' -f1) resource_id=$(echo "$resource" | cut -d':' -f2) case "$resource_type" in "INSTANCE") echo "Terminating EC2 instance: $resource_id" aws ec2 terminate-instances --instance-ids "$resource_id" || echo "Failed to terminate instance: $resource_id" # Wait for instance to terminate echo "Waiting for instance to terminate..." aws ec2 wait instance-terminated --instance-ids "$resource_id" || echo "Failed to wait for instance termination: $resource_id" ;; "KEY_PAIR") echo "Deleting key pair: $resource_id" aws ec2 delete-key-pair --key-name "$resource_id" || echo "Failed to delete key pair: $resource_id" # Remove the .pem file if it exists if [ -f "${resource_id}.pem" ]; then rm -f "${resource_id}.pem" fi ;; "NAT_GATEWAY") echo "Deleting NAT Gateway: $resource_id" aws ec2 delete-nat-gateway --nat-gateway-id "$resource_id" || echo "Failed to delete NAT Gateway: $resource_id" # NAT Gateway deletion takes time, wait for it to complete echo "Waiting for NAT Gateway to be deleted..." aws ec2 wait nat-gateway-deleted --nat-gateway-ids "$resource_id" || echo "Failed to wait for NAT Gateway deletion: $resource_id" ;; "EIP") echo "Releasing Elastic IP: $resource_id" aws ec2 release-address --allocation-id "$resource_id" || echo "Failed to release Elastic IP: $resource_id" ;; "ROUTE_TABLE_ASSOCIATION") echo "Disassociating Route Table: $resource_id" aws ec2 disassociate-route-table --association-id "$resource_id" || echo "Failed to disassociate Route Table: $resource_id" ;; "ROUTE_TABLE") echo "Deleting Route Table: $resource_id" aws ec2 delete-route-table --route-table-id "$resource_id" || echo "Failed to delete Route Table: $resource_id" ;; "INTERNET_GATEWAY") echo "Detaching Internet Gateway: $resource_id from VPC: $VPC_ID" aws ec2 detach-internet-gateway --internet-gateway-id "$resource_id" --vpc-id "$VPC_ID" || echo "Failed to detach Internet Gateway: $resource_id" echo "Deleting Internet Gateway: $resource_id" aws ec2 delete-internet-gateway --internet-gateway-id "$resource_id" || echo "Failed to delete Internet Gateway: $resource_id" ;; "SECURITY_GROUP") echo "Deleting Security Group: $resource_id" aws ec2 delete-security-group --group-id "$resource_id" || echo "Failed to delete Security Group: $resource_id" ;; "SUBNET") echo "Deleting Subnet: $resource_id" aws ec2 delete-subnet --subnet-id "$resource_id" || echo "Failed to delete Subnet: $resource_id" ;; "VPC") echo "Deleting VPC: $resource_id" aws ec2 delete-vpc --vpc-id "$resource_id" || echo "Failed to delete VPC: $resource_id" ;; esac done } # Initialize array to track created resources CREATED_RESOURCES=() echo "Starting VPC creation script at $(date)" # Verify AWS CLI configuration echo "Verifying AWS CLI configuration..." aws configure list || handle_error "AWS CLI is not properly configured" # Verify identity and permissions echo "Verifying identity and permissions..." if ! aws sts get-caller-identity; then echo "ERROR: Unable to verify AWS identity. This could be due to:" echo " - Expired credentials" echo " - Missing or invalid AWS credentials" echo " - Insufficient permissions" echo "" echo "Please run 'aws configure' to update your credentials or check your IAM permissions." exit 1 fi # Create VPC echo "Creating VPC with CIDR block 10.0.0.0/16..." VPC_ID=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 --tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=MyVPC}]' --query 'Vpc.VpcId' --output text) if [ -z "$VPC_ID" ]; then handle_error "Failed to create VPC" fi CREATED_RESOURCES+=("VPC:$VPC_ID") echo "VPC created with ID: $VPC_ID" # Enable DNS support and hostnames echo "Enabling DNS support and hostnames for VPC..." aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-support || handle_error "Failed to enable DNS support" aws ec2 modify-vpc-attribute --vpc-id "$VPC_ID" --enable-dns-hostnames || handle_error "Failed to enable DNS hostnames" # Get available Availability Zones echo "Getting available Availability Zones..." AZ1=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[0].ZoneName' --output text) AZ2=$(aws ec2 describe-availability-zones --query 'AvailabilityZones[1].ZoneName' --output text) if [ -z "$AZ1" ] || [ -z "$AZ2" ]; then handle_error "Failed to get Availability Zones" fi echo "Using Availability Zones: $AZ1 and $AZ2" # Create public subnets echo "Creating public subnet in $AZ1..." PUBLIC_SUBNET_AZ1=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.0.0/24 \ --availability-zone "$AZ1" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-AZ1}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PUBLIC_SUBNET_AZ1" ]; then handle_error "Failed to create public subnet in AZ1" fi CREATED_RESOURCES+=("SUBNET:$PUBLIC_SUBNET_AZ1") echo "Public subnet created in $AZ1 with ID: $PUBLIC_SUBNET_AZ1" echo "Creating public subnet in $AZ2..." PUBLIC_SUBNET_AZ2=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.1.0/24 \ --availability-zone "$AZ2" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Public-Subnet-AZ2}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PUBLIC_SUBNET_AZ2" ]; then handle_error "Failed to create public subnet in AZ2" fi CREATED_RESOURCES+=("SUBNET:$PUBLIC_SUBNET_AZ2") echo "Public subnet created in $AZ2 with ID: $PUBLIC_SUBNET_AZ2" # Create private subnets echo "Creating private subnet in $AZ1..." PRIVATE_SUBNET_AZ1=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.2.0/24 \ --availability-zone "$AZ1" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Private-Subnet-AZ1}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PRIVATE_SUBNET_AZ1" ]; then handle_error "Failed to create private subnet in AZ1" fi CREATED_RESOURCES+=("SUBNET:$PRIVATE_SUBNET_AZ1") echo "Private subnet created in $AZ1 with ID: $PRIVATE_SUBNET_AZ1" echo "Creating private subnet in $AZ2..." PRIVATE_SUBNET_AZ2=$(aws ec2 create-subnet \ --vpc-id "$VPC_ID" \ --cidr-block 10.0.3.0/24 \ --availability-zone "$AZ2" \ --tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=Private-Subnet-AZ2}]' \ --query 'Subnet.SubnetId' \ --output text) if [ -z "$PRIVATE_SUBNET_AZ2" ]; then handle_error "Failed to create private subnet in AZ2" fi CREATED_RESOURCES+=("SUBNET:$PRIVATE_SUBNET_AZ2") echo "Private subnet created in $AZ2 with ID: $PRIVATE_SUBNET_AZ2" # Create Internet Gateway echo "Creating Internet Gateway..." IGW_ID=$(aws ec2 create-internet-gateway \ --tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=MyIGW}]' \ --query 'InternetGateway.InternetGatewayId' \ --output text) if [ -z "$IGW_ID" ]; then handle_error "Failed to create Internet Gateway" fi CREATED_RESOURCES+=("INTERNET_GATEWAY:$IGW_ID") echo "Internet Gateway created with ID: $IGW_ID" # Attach Internet Gateway to VPC echo "Attaching Internet Gateway to VPC..." aws ec2 attach-internet-gateway --internet-gateway-id "$IGW_ID" --vpc-id "$VPC_ID" || handle_error "Failed to attach Internet Gateway to VPC" # Create public route table echo "Creating public route table..." PUBLIC_RT=$(aws ec2 create-route-table \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Public-RT}]' \ --query 'RouteTable.RouteTableId' \ --output text) if [ -z "$PUBLIC_RT" ]; then handle_error "Failed to create public route table" fi CREATED_RESOURCES+=("ROUTE_TABLE:$PUBLIC_RT") echo "Public route table created with ID: $PUBLIC_RT" # Add route to Internet Gateway echo "Adding route to Internet Gateway in public route table..." aws ec2 create-route --route-table-id "$PUBLIC_RT" --destination-cidr-block 0.0.0.0/0 --gateway-id "$IGW_ID" || handle_error "Failed to add route to Internet Gateway" # Associate public subnets with public route table echo "Associating public subnet in $AZ1 with public route table..." PUBLIC_RT_ASSOC_1=$(aws ec2 associate-route-table --route-table-id "$PUBLIC_RT" --subnet-id "$PUBLIC_SUBNET_AZ1" --query 'AssociationId' --output text) if [ -z "$PUBLIC_RT_ASSOC_1" ]; then handle_error "Failed to associate public subnet in AZ1 with public route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PUBLIC_RT_ASSOC_1") echo "Associating public subnet in $AZ2 with public route table..." PUBLIC_RT_ASSOC_2=$(aws ec2 associate-route-table --route-table-id "$PUBLIC_RT" --subnet-id "$PUBLIC_SUBNET_AZ2" --query 'AssociationId' --output text) if [ -z "$PUBLIC_RT_ASSOC_2" ]; then handle_error "Failed to associate public subnet in AZ2 with public route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PUBLIC_RT_ASSOC_2") # Create private route table echo "Creating private route table..." PRIVATE_RT=$(aws ec2 create-route-table \ --vpc-id "$VPC_ID" \ --tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=Private-RT}]' \ --query 'RouteTable.RouteTableId' \ --output text) if [ -z "$PRIVATE_RT" ]; then handle_error "Failed to create private route table" fi CREATED_RESOURCES+=("ROUTE_TABLE:$PRIVATE_RT") echo "Private route table created with ID: $PRIVATE_RT" # Associate private subnets with private route table echo "Associating private subnet in $AZ1 with private route table..." PRIVATE_RT_ASSOC_1=$(aws ec2 associate-route-table --route-table-id "$PRIVATE_RT" --subnet-id "$PRIVATE_SUBNET_AZ1" --query 'AssociationId' --output text) if [ -z "$PRIVATE_RT_ASSOC_1" ]; then handle_error "Failed to associate private subnet in AZ1 with private route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PRIVATE_RT_ASSOC_1") echo "Associating private subnet in $AZ2 with private route table..." PRIVATE_RT_ASSOC_2=$(aws ec2 associate-route-table --route-table-id "$PRIVATE_RT" --subnet-id "$PRIVATE_SUBNET_AZ2" --query 'AssociationId' --output text) if [ -z "$PRIVATE_RT_ASSOC_2" ]; then handle_error "Failed to associate private subnet in AZ2 with private route table" fi CREATED_RESOURCES+=("ROUTE_TABLE_ASSOCIATION:$PRIVATE_RT_ASSOC_2") # Allocate Elastic IP for NAT Gateway echo "Allocating Elastic IP for NAT Gateway..." EIP_ALLOC=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text) if [ -z "$EIP_ALLOC" ]; then handle_error "Failed to allocate Elastic IP" fi CREATED_RESOURCES+=("EIP:$EIP_ALLOC") echo "Elastic IP allocated with ID: $EIP_ALLOC" # Create NAT Gateway echo "Creating NAT Gateway in public subnet in $AZ1..." NAT_GW=$(aws ec2 create-nat-gateway \ --subnet-id "$PUBLIC_SUBNET_AZ1" \ --allocation-id "$EIP_ALLOC" \ --tag-specifications 'ResourceType=natgateway,Tags=[{Key=Name,Value=MyNATGateway}]' \ --query 'NatGateway.NatGatewayId' \ --output text) if [ -z "$NAT_GW" ]; then handle_error "Failed to create NAT Gateway" fi CREATED_RESOURCES+=("NAT_GATEWAY:$NAT_GW") echo "NAT Gateway created with ID: $NAT_GW" # Wait for NAT Gateway to be available echo "Waiting for NAT Gateway to be available..." aws ec2 wait nat-gateway-available --nat-gateway-ids "$NAT_GW" || handle_error "NAT Gateway did not become available" # Add route to NAT Gateway in private route table echo "Adding route to NAT Gateway in private route table..." aws ec2 create-route --route-table-id "$PRIVATE_RT" --destination-cidr-block 0.0.0.0/0 --nat-gateway-id "$NAT_GW" || handle_error "Failed to add route to NAT Gateway" # Enable auto-assign public IP for instances in public subnets echo "Enabling auto-assign public IP for instances in public subnet in $AZ1..." aws ec2 modify-subnet-attribute --subnet-id "$PUBLIC_SUBNET_AZ1" --map-public-ip-on-launch || handle_error "Failed to enable auto-assign public IP for public subnet in AZ1" echo "Enabling auto-assign public IP for instances in public subnet in $AZ2..." aws ec2 modify-subnet-attribute --subnet-id "$PUBLIC_SUBNET_AZ2" --map-public-ip-on-launch || handle_error "Failed to enable auto-assign public IP for public subnet in AZ2" # Create security group for web servers echo "Creating security group for web servers..." WEB_SG=$(aws ec2 create-security-group \ --group-name "WebServerSG-$(date +%s)" \ --description "Security group for web servers" \ --vpc-id "$VPC_ID" \ --query 'GroupId' \ --output text) if [ -z "$WEB_SG" ]; then handle_error "Failed to create security group for web servers" fi CREATED_RESOURCES+=("SECURITY_GROUP:$WEB_SG") echo "Security group for web servers created with ID: $WEB_SG" # Allow HTTP and HTTPS traffic echo "Allowing HTTP traffic to web servers security group..." aws ec2 authorize-security-group-ingress --group-id "$WEB_SG" --protocol tcp --port 80 --cidr 0.0.0.0/0 || handle_error "Failed to allow HTTP traffic" echo "Allowing HTTPS traffic to web servers security group..." aws ec2 authorize-security-group-ingress --group-id "$WEB_SG" --protocol tcp --port 443 --cidr 0.0.0.0/0 || handle_error "Failed to allow HTTPS traffic" # Note: In a production environment, you should restrict the source IP ranges for security echo "NOTE: In a production environment, you should restrict the source IP ranges for HTTP and HTTPS traffic" # Create security group for database servers echo "Creating security group for database servers..." DB_SG=$(aws ec2 create-security-group \ --group-name "DBServerSG-$(date +%s)" \ --description "Security group for database servers" \ --vpc-id "$VPC_ID" \ --query 'GroupId' \ --output text) if [ -z "$DB_SG" ]; then handle_error "Failed to create security group for database servers" fi CREATED_RESOURCES+=("SECURITY_GROUP:$DB_SG") echo "Security group for database servers created with ID: $DB_SG" # Allow MySQL/Aurora traffic from web servers only echo "Allowing MySQL/Aurora traffic from web servers to database servers..." aws ec2 authorize-security-group-ingress --group-id "$DB_SG" --protocol tcp --port 3306 --source-group "$WEB_SG" || handle_error "Failed to allow MySQL/Aurora traffic" # Verify VPC configuration echo "Verifying VPC configuration..." echo "VPC:" aws ec2 describe-vpcs --vpc-id "$VPC_ID" || handle_error "Failed to describe VPC" echo "Subnets:" aws ec2 describe-subnets --filters "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe subnets" echo "Route tables:" aws ec2 describe-route-tables --filters "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe route tables" echo "Internet gateway:" aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=$VPC_ID" || handle_error "Failed to describe Internet Gateway" echo "NAT gateway:" aws ec2 describe-nat-gateways --filter "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe NAT Gateway" echo "Security groups:" aws ec2 describe-security-groups --filters "Name=vpc-id,Values=$VPC_ID" || handle_error "Failed to describe security groups" echo "" # Summary of created resources echo "VPC creation completed successfully!" echo "Summary of created resources:" echo "- VPC: $VPC_ID" echo "- Public Subnet in $AZ1: $PUBLIC_SUBNET_AZ1" echo "- Public Subnet in $AZ2: $PUBLIC_SUBNET_AZ2" echo "- Private Subnet in $AZ1: $PRIVATE_SUBNET_AZ1" echo "- Private Subnet in $AZ2: $PRIVATE_SUBNET_AZ2" echo "- Internet Gateway: $IGW_ID" echo "- Public Route Table: $PUBLIC_RT" echo "- Private Route Table: $PRIVATE_RT" echo "- Elastic IP: $EIP_ALLOC" echo "- NAT Gateway: $NAT_GW" echo "- Web Servers Security Group: $WEB_SG" echo "- Database Servers Security Group: $DB_SG" # Deploy EC2 instances echo "" echo "Deploying EC2 instances..." # Create key pair for SSH access KEY_NAME="vpc-tutorial-key-$(date +%s)" echo "Creating key pair $KEY_NAME..." aws ec2 create-key-pair --key-name "$KEY_NAME" --query 'KeyMaterial' --output text > "${KEY_NAME}.pem" || handle_error "Failed to create key pair" chmod 400 "${KEY_NAME}.pem" echo "Key pair saved to ${KEY_NAME}.pem" CREATED_RESOURCES+=("KEY_PAIR:$KEY_NAME") # Get latest Amazon Linux 2 AMI echo "Getting latest Amazon Linux 2 AMI..." AMI_ID=$(aws ec2 describe-images --owners amazon \ --filters "Name=name,Values=amzn2-ami-hvm-*-x86_64-gp2" "Name=state,Values=available" \ --query "sort_by(Images, &CreationDate)[-1].ImageId" --output text) || handle_error "Failed to get AMI" echo "Using AMI: $AMI_ID" # Launch web server in public subnet echo "Launching web server in public subnet..." WEB_INSTANCE=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --count 1 \ --instance-type t2.micro \ --key-name "$KEY_NAME" \ --security-group-ids "$WEB_SG" \ --subnet-id "$PUBLIC_SUBNET_AZ1" \ --associate-public-ip-address \ --user-data '#!/bin/bash yum update -y yum install -y httpd systemctl start httpd systemctl enable httpd echo "<h1>Hello from $(hostname -f) in the public subnet</h1>" > /var/www/html/index.html' \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=WebServer}]' \ --query 'Instances[0].InstanceId' \ --output text) || handle_error "Failed to launch web server" echo "Web server instance created with ID: $WEB_INSTANCE" CREATED_RESOURCES+=("INSTANCE:$WEB_INSTANCE") # Wait for web server to be running echo "Waiting for web server to be running..." aws ec2 wait instance-running --instance-ids "$WEB_INSTANCE" # Get web server public IP WEB_PUBLIC_IP=$(aws ec2 describe-instances --instance-ids "$WEB_INSTANCE" \ --query 'Reservations[0].Instances[0].PublicIpAddress' --output text) echo "Web server public IP: $WEB_PUBLIC_IP" echo "You can access the web server at: http://$WEB_PUBLIC_IP" # Launch database server in private subnet echo "Launching database server in private subnet..." DB_INSTANCE=$(aws ec2 run-instances \ --image-id "$AMI_ID" \ --count 1 \ --instance-type t2.micro \ --key-name "$KEY_NAME" \ --security-group-ids "$DB_SG" \ --subnet-id "$PRIVATE_SUBNET_AZ1" \ --user-data '#!/bin/bash yum update -y yum install -y mariadb-server systemctl start mariadb systemctl enable mariadb' \ --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=DBServer}]' \ --query 'Instances[0].InstanceId' \ --output text) || handle_error "Failed to launch database server" echo "Database server instance created with ID: $DB_INSTANCE" CREATED_RESOURCES+=("INSTANCE:$DB_INSTANCE") # Wait for database server to be running echo "Waiting for database server to be running..." aws ec2 wait instance-running --instance-ids "$DB_INSTANCE" # Get database server private IP DB_PRIVATE_IP=$(aws ec2 describe-instances --instance-ids "$DB_INSTANCE" \ --query 'Reservations[0].Instances[0].PrivateIpAddress' --output text) echo "Database server private IP: $DB_PRIVATE_IP" echo "EC2 instances deployed successfully!" echo "- Web Server (Public): $WEB_INSTANCE ($WEB_PUBLIC_IP)" echo "- Database Server (Private): $DB_INSTANCE ($DB_PRIVATE_IP)" echo "" echo "Note: To connect to the web server: ssh -i ${KEY_NAME}.pem ec2-user@$WEB_PUBLIC_IP" echo "To connect to the database server, you must first connect to the web server, then use it as a bastion host." echo "===========================================" echo "CLEANUP CONFIRMATION" echo "===========================================" echo "Do you want to clean up all created resources? (y/n): " read -r CLEANUP_CHOICE if [[ "$CLEANUP_CHOICE" =~ ^[Yy]$ ]]; then echo "Cleaning up resources..." cleanup_resources echo "All resources have been cleaned up." else echo "Resources will not be cleaned up. You can manually clean them up later." fi echo "Script completed at $(date)"