Skip to content

Deploy Docker Swarm on AWS EC2 via cloud-formation templates - Step 1 - Network Setup

Setup network infrastructure including new VPC (Virtual Private Cloud), public and private subnets, internet gateway, security group, and ssh access.

This post is part of a thread that includes these steps:

  1. Network Setup (this post)
  2. Storage
  3. Roles
  4. Manager Instance
  5. Worker Launch Template
  6. Worker Instances
  7. Docker Swarm
  8. Cleanup

Make sure you do this setup first:

  1. Setup macOS for AWS Cloud DevOps
  2. AWS Authentication

Project Structure

For this deployment we will create 2 separate code repositories. The first one will be named swift-aws-ec2-swarm and the second one will be named swift-aws-ec2-docker.

As a start, create the swift-aws-ec2-swarm directory in your home dir and switch to it. We will call this the "project dir".

mkdir -p ~/swift-aws-ec2-swarm
cd ~/swift-aws-ec2-swarm

The rest of this post assumes we work from within the "project dir".

Configuration

Create a folder config and names.sh file in it.

mkdir -p config
touch config/names.sh
nano config/names.sh

Copy and paste this code into names.sh:

export AWS_DEFAULT_PROFILE=swift

prefix="swift-swarm"

ec2_key_pair="aws-ec2-key"

stack_vpc="$prefix-vpc"

stack_ebs="$prefix-ebs"

stack_iam_manager="$prefix-iam-manager"
stack_ec2_manager="$prefix-ec2-manager"

stack_iam_worker="$prefix-iam-worker"
stack_ec2_worker="$prefix-ec2-worker"
stack_ec2_worker_lt="$prefix-ec2-worker-lt"

vpc="$prefix-vpc-1"

subnet_pub_1="$prefix-sn-pub-1"
subnet_priv_1="$prefix-sn-priv-1"

security_group_pub_1="$prefix-sg-pub-1"
security_group_priv_1="$prefix-sg-priv-1"

IMPORTANT: Replace swift in the first line with the name of your AWS CLI profile or set to default if you don;t use different profiles.

Virtual Private Cloud (AWS VPC)

cloud-formation Template

Create a folder network and a vpc.yml file in it.

mkdir -p vpc
touch vpc/vpc.yml
nano vpc/vpc.yml

Copy and paste this code into vpc.yml:

Description: Create VPC, public subnet, private subnet, Internet gateway, NAT gateway, security groups, and Route53 hosted zone for the VPC. 

Parameters:
  Prefix:
    Description: An environment name that is prefixed to resource names
    Type: String

  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.0.0.0/16

  PublicSubnetCIDR:
    Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone
    Type: String
    Default: 10.0.10.0/24

  PrivateSubnetCIDR:
    Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone
    Type: String
    Default: 10.0.20.0/24

Resources:
  # VPC
  Vpc:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-vpc-1

  # Internet Gateway
  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-igw-1

  InternetGatewayAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc
      InternetGatewayId: !Ref InternetGateway

  # Public Subnet
  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Ref PublicSubnetCIDR
      AvailabilityZone: !Select [ 0, !GetAZs '' ]
      MapPublicIpOnLaunch: true
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-sn-pub-1

  # Private Subnet
  PrivateSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc
      CidrBlock: !Ref PrivateSubnetCIDR
      AvailabilityZone: !Select [ 0, !GetAZs  '' ]
      MapPublicIpOnLaunch: false
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-sn-priv-1

  # NAT Elastic IP
  NatGatewayEIP:
    Type: AWS::EC2::EIP
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-eip-1

  # NAT Gateway
  NatGateway:
    Type: AWS::EC2::NatGateway
    Properties:
      AllocationId: !GetAtt NatGatewayEIP.AllocationId
      SubnetId: !Ref PublicSubnet
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-ng-1

  # Public route table
  PublicRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-rt-pub-1

  # Route to internet gateway
  DefaultPublicRoute:
    Type: AWS::EC2::Route
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # Routes for public subnet
  PublicSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet

  # Routes for private subnet
  PrivateRouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-rt-priv-1

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

  PrivateSubnetRouteTableAssociation:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      RouteTableId: !Ref PrivateRouteTable
      SubnetId: !Ref PrivateSubnet

  ## Security groups
  # Public
  PublicSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for manager nodes
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-sg-pub-1
      VpcId: !Ref Vpc
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: 0.0.0.0/0

  # Private
  PrivateSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Security group for worker nodes
      # categories:
        - Key: Name
          Value: !Sub ${Prefix}-sg-priv-1
      VpcId: !Ref Vpc

  ## Ingress rules
  # Public
  PublicSecurityGroupIngressFromSelf:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt [ PublicSecurityGroup, GroupId ]
      IpProtocol: -1
      SourceSecurityGroupId: !GetAtt [ PublicSecurityGroup, GroupId ]

  PublicSecurityGroupIngressFromPrivate:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt [ PublicSecurityGroup, GroupId ]
      IpProtocol: -1
      SourceSecurityGroupId: !GetAtt [ PrivateSecurityGroup, GroupId ]

  # Private
  PrivateSecurityGroupIngressFromSelf:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt [ PrivateSecurityGroup, GroupId ]
      IpProtocol: -1
      SourceSecurityGroupId: !GetAtt [ PrivateSecurityGroup, GroupId ]

  PrivateSecurityGroupIngressFromPublic:
    Type: AWS::EC2::SecurityGroupIngress
    Properties:
      GroupId: !GetAtt [ PrivateSecurityGroup, GroupId ]
      IpProtocol: -1
      SourceSecurityGroupId: !GetAtt [ PublicSecurityGroup, GroupId ]

  # Add DNS zone for our VPC
  HostedZone:
    Type: 'AWS::Route53::HostedZone'
    Properties:
      Name: swift.internal.
      VPCs:
        - VPCId: !Ref Vpc
          VPCRegion: !Ref "AWS::Region"
      HostedZoneConfig:
        Comment: 'Hosted zone for swift.internal'      

Outputs:
  VpcId:
    Description: A reference to the created VPC
    Value: !Ref Vpc

  PublicSubnetId:
    Description: A reference to the public subnet in the 1st Availability Zone
    Value: !Ref PublicSubnet

  PrivateSubnetId:
    Description: A reference to the private subnet in the 1st Availability Zone
    Value: !Ref PrivateSubnet

  PublicSecurityGroupId:
    Description: A reference to the public SecurityGroup
    Value: !Ref PublicSecurityGroup

  PrivateSecurityGroupId:
    Description: A reference to the private SecurityGroup
    Value: !Ref PrivateSecurityGroup

  HostedZoneId:
    Description: A reference to the Route53 HostedZone
    Value: !Ref HostedZone

Scripts

Next add a script deploy-vpc.sh and paste this code in it:

#!/usr/bin/env bash

# switch to parent directory
script_path=`dirname ${BASH_SOURCE[0]}`
pushd $script_path/..

source config/names.sh

echo
echo "Deploying $stack_vpc stack via cloud-formation:"
echo 'https://us-west-2.console.aws.amazon.com/cloudformation/home'
echo

set -x

aws cloudformation deploy \
    --profile swift \
    --template-file vpc/vpc.yml \
    --stack-name $stack_vpc \
    --parameter-overrides Prefix=$prefix

popd

Let's also add a clean up script rm-vpc.sh:

#!/usr/bin/env bash

# switch to parent directory
script_path=`dirname ${BASH_SOURCE[0]}`
pushd $script_path/..

source config/names.sh

echo
echo "Removing $stack_vpc stack via cloud-formation:"
echo 'https://us-west-2.console.aws.amazon.com/cloudformation/home'
echo

set -x

aws cloudformation delete-stack \
    --stack-name $stack_vpc 

aws cloudformation wait stack-delete-complete \
    --stack-name $stack_vpc

popd

Make the scripts executable:

chmod +x vpc/deploy-vpc.sh 
chmod +x vpc/rm-vpc.sh

Deploy

Finally let's run the "deploy" script to setup our network:

./vpc/deploy-vpc.sh

You should see output similar to this:

Deploying swift-swarm-vpc stack via cloud-formation:
https://us-west-2.console.aws.amazon.com/cloudformation/home

+ aws cloudformation deploy --template-file network/vpc.yml --stack-name swift-swarm-vpc --parameter-overrides Prefix=swift-swarm

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - swift-swarm-vpc

At this point your project structure should look like this:

.
├── config
│   └── names.sh
└── vpc
    ├── deploy-vpc.sh
    ├── rm-vpc.sh
    └── vpc.yml

Congratulations!

We are done with Step 1. Network Setup.

Next step is: Step 2. Storage