Initial commit

Commit set of files for turning up a Docker Swarm mode cluster on Ubuntu instances on AWS

Signed-off-by: Scott Lowe <scott.lowe@scottlowe.org>
This commit is contained in:
Scott Lowe 2017-09-17 21:04:26 -06:00
parent 228677ee8c
commit 198771b160
No known key found for this signature in database
GPG key ID: 949F43F6E6C11780
13 changed files with 2192 additions and 0 deletions

View file

@ -0,0 +1,104 @@
# Using Traefik with Docker Swarm on Ubuntu on AWS
These files help establish a Docker Swarm mode cluster on which you can deploy Traefik ([https://traefik.io](https://traefik.io)), a dynamic reverse proxy, to help direct traffic for services deployed on the Swarm cluster. The cluster is deployed on AWS using an Ubuntu-based AMI.
## Prerequisites
This demo environment assumes that you have the following software already installed on your system:
* Terraform 0.9.x (tested with 0.9.11, should work with any 0.9.x version)
* Ansible 2.x (tested with 2.3.2.0)
Before trying to use this demo environment, please ensure that you've installed the necessary prerequisites.
This demo environment was tested on macOS 10.12 "Sierra", but it should work on any recent macOS release or recent Linux distribution. I don't know what would be required to make it work on Windows, sorry.
## Contents
* **ansible.cfg**: This file configures Ansible to connect to the AWS instances. You'll need to edit this file to specify the correct SSH private key to use (should match the key specified in `terraform.tfvars`; see the Instructions).
* **compute.tf**: This Terraform configuration launches the Ubuntu-based instances used for the Docker Swarm setup.
* **create-swarm.yml**: This is the Ansible playbook that configures the Ubuntu-based AWS instances into a Docker Swarm mode cluster.
* **data.tf**: This Terraform configuration file provides information to Terraform on which AWS AMIs to use.s
* **ec2.ini**: This file configures the Ansible dynamic inventory script. If you're using an AWS region _other_ than "us-west-2", you'll want to edit this script appropriately.
* **ec2.py**: This Python script is a dynamic inventory script for Ansible.
* **networking.tf**: This Terraform configuration file creates all the networking constructs needed for the environment (VPC, Internet gateway, subnet, gateway attachment, route table, and route table associations).
* **output.tf**: This Terraform configuration outputs information about the resources created (the public IP addresses of the instances, specifically).
* **provider.tf**: This Terraform configuration files configures the AWS provider.
* **README.md**: The file you're currently reading.
* **security.tf**: This Terraform configuration creates the security groups used by the instances.
* **variables.tf**: This Terraform configuration file specifies variables that Terraform will need in order to create the desired AWS infrastructure.
## Instructions
As mentioned in "Prerequisites" above, these instructions assume that you have a working Terraform installation capable of working with AWS.
1. Place the files from the `traefik/tf-ans-swarm` directory of the "learning-tools" GitHub repository into a direcotry on your local system. You can clone the entire repository (using `git clone`) or just download the specific files from the `traefik/tf-ans-swarm` directory.
2. Create a file named `terraform.tfvars` and populate it with the name of the AWS keypair you'd like to use for the instances, the AWS region to use, the type of the instances (such as "t2.micro"), and the number of worker nodes you'd like created. Refer to [this URL](https://www.terraform.io/intro/getting-started/variables.html) for specific details on the syntax of this file. You can refer to the contents of `variables.tf` for the names of the variables that need to be defined.
3. Run `terraform validate` to ensure there are no errors in the Terraform configuration. (If using Terraform 0.10.x, you will also need to run `terraform init` first; however, I haven't tested this environment with 0.10.x yet).
4. Run `terraform plan` to have Terraform examine the current infrastructure and determine what changes are necessary to realize the desired configuration.
5. Run `terraform apply` to have Terraform make the changes necessary to realize the desired configuration. When this command has completed, it will output the public IP addresses assigned to the created instances.
6. Run `ansible-playbook create-swarm.yml` to have Ansible configure the AWS instances created by Terraform into a Docker Swarm mode cluster.
7. All remaining steps should be run while connected to the "manager" instance via SSH. First, create an overlay network:
docker network create --driver=overlay demo-net
8. Next, create a service (constrained to the manager node) to run the Traefik reverse proxy:
docker service create --name traefik \
--constraint 'node.role==manager' \
--publish 80:80 --publish 8080:8080 \
--mount type=bind,source=/var/run/docker.sock,target=/var/run/docker.sock \
--network demo-net
traefik --web --docker --docker.watch \
--docker.swarmmode --docker.domain=docker.local
If you'd like additional logging, add `--logLevel=DEBUG` to the above command.
9. Deploy a web service to be used behind Traefik:
docker service create --name web \
--label 'traefik.port=5000' \
--network demo-net slowe/flask-demo-app:1.0
10. Run this command against the IP address of the manager and note the output:
curl -H "Host:web.docker.local" http://<manager_ip_address>
If you used a name other than "web" in step 11, replace that name in the "Host" portion of the above command.
11. Run `docker service scale web=3` to scale up the "web" service (replace "web" with whatever name you used in step 9). Repeat step 10 and note that Traefik will load balance across the different containers hosting the service.
12. Deploy a new web service:
docker service create --name api \
--label 'traefik.port=5000' \
--network demo-net slowe/flask-demo-app:1.0
13. Run this command to access the "api" service behind Traefik:
curl -H "Host:api.docker.local" http://<manager_ip_address>
If you used a name other than "api", use that name in the above command.
14. Note that Traefik is taking inbound traffic to the manager and correctly routing it to the appropriate backend container based on the "Host" header. Feel free to deploy additional services with different names to see Traefik in action.
## License
This content is licensed under the MIT License.

View file

@ -0,0 +1,5 @@
[defaults]
inventory = ./ec2.py
private_key_file = ~/.ssh/aws_rsa
remote_user = ubuntu
host_key_checking = false

View file

@ -0,0 +1,33 @@
# Launch an instance to serve as a manager
resource "aws_instance" "manager" {
ami = "${data.aws_ami.xenial_ami.id}"
instance_type = "${var.mgr_type}"
key_name = "${var.keypair}"
vpc_security_group_ids = ["${aws_security_group.mgr_sg.id}"]
subnet_id = "${aws_subnet.traefik_pub_subnet.id}"
depends_on = ["aws_internet_gateway.traefik_gw"]
tags {
Name = "manager"
tool = "terraform"
demo = "traefik"
area = "compute"
role = "manager"
}
}
# Launch one or more instances to serve as worker nodes
resource "aws_instance" "worker" {
ami = "${data.aws_ami.xenial_ami.id}"
count = "${var.num_wkr_nodes}"
instance_type = "${var.wkr_type}"
key_name = "${var.keypair}"
vpc_security_group_ids = ["${aws_security_group.wkr_sg.id}"]
subnet_id = "${aws_subnet.traefik_pub_subnet.id}"
depends_on = ["aws_internet_gateway.traefik_gw"]
tags {
tool = "terraform"
demo = "traefik"
area = "compute"
role = "worker"
}
}

View file

@ -0,0 +1,66 @@
---
- hosts: "us-west-2"
become: "yes"
remote_user: "ubuntu"
tasks:
- name: "Install Pip"
apt:
name: "python-pip"
state: "present"
update_cache: "yes"
- name: "Install some Python modules"
pip:
name: "{{ item }}"
state: "present"
with_items:
- "urllib3"
- "pyopenssl"
- "ndg-httpsclient"
- "pyasn1"
- name: "Install Docker APT Key"
apt_key:
url: "https://download.docker.com/linux/ubuntu/gpg"
state: "present"
- name: "Add Docker repository"
apt_repository:
repo: "deb [arch=amd64] https://download.docker.com/linux/ubuntu trusty stable"
state: "present"
update_cache: "yes"
- name: "Install Docker CE"
package:
name: "docker-ce"
state: "present"
- name: "Add user to Docker group"
user:
name: "{{ ansible_ssh_user }}"
group: "docker"
append: "yes"
- hosts: "tag_role_manager"
become: "yes"
remote_user: "ubuntu"
tasks:
- name: "Initialize Docker Swarm from manager"
command: "docker swarm init --advertise-addr {{ ansible_default_ipv4.address }}"
- name: "Register Swarm join token"
command: "docker swarm join-token -q worker"
register: swarm_token
- name: "Establish Swarm join token as a host fact"
set_fact: swarmtoken="{{ swarm_token.stdout }}"
- hosts: "tag_role_worker"
become: "yes"
remote_user: "ubuntu"
tasks:
- name: "Join worker nodes to Swarm cluster"
command: "docker swarm join --advertise-addr {{ ansible_default_ipv4.address }} --token {{ hostvars[groups['tag_role_manager'][0]].swarmtoken }} {{ hostvars[groups['tag_role_manager'][0]].ansible_default_ipv4.address }}:2377"

View file

@ -0,0 +1,16 @@
data "aws_ami" "xenial_ami" {
most_recent = true
owners = ["099720109477"]
filter {
name = "name"
values = ["*ubuntu-trusty-14*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
}

View file

@ -0,0 +1,8 @@
---
- hosts: "all"
become: "yes"
remote_user: "ubuntu"
tasks:
- name: "Leave Swarm cluster"
command: "docker swarm leave --force"

View file

@ -0,0 +1,209 @@
# Ansible EC2 external inventory script settings
#
[ec2]
# to talk to a private eucalyptus instance uncomment these lines
# and edit edit eucalyptus_host to be the host name of your cloud controller
#eucalyptus = True
#eucalyptus_host = clc.cloud.domain.org
# AWS regions to make calls to. Set this to 'all' to make request to all regions
# in AWS and merge the results together. Alternatively, set this to a comma
# separated list of regions. E.g. 'us-east-1,us-west-1,us-west-2' and do not
# provide the 'regions_exclude' option. If this is set to 'auto', AWS_REGION or
# AWS_DEFAULT_REGION environment variable will be read to determine the region.
regions = us-west-2
#regions_exclude = us-gov-west-1, cn-north-1
# When generating inventory, Ansible needs to know how to address a server.
# Each EC2 instance has a lot of variables associated with it. Here is the list:
# http://docs.pythonboto.org/en/latest/ref/ec2.html#module-boto.ec2.instance
# Below are 2 variables that are used as the address of a server:
# - destination_variable
# - vpc_destination_variable
# This is the normal destination variable to use. If you are running Ansible
# from outside EC2, then 'public_dns_name' makes the most sense. If you are
# running Ansible from within EC2, then perhaps you want to use the internal
# address, and should set this to 'private_dns_name'. The key of an EC2 tag
# may optionally be used; however the boto instance variables hold precedence
# in the event of a collision.
destination_variable = public_dns_name
# This allows you to override the inventory_name with an ec2 variable, instead
# of using the destination_variable above. Addressing (aka ansible_ssh_host)
# will still use destination_variable. Tags should be written as 'tag_TAGNAME'.
#hostname_variable = tag_Name
# For server inside a VPC, using DNS names may not make sense. When an instance
# has 'subnet_id' set, this variable is used. If the subnet is public, setting
# this to 'ip_address' will return the public IP address. For instances in a
# private subnet, this should be set to 'private_ip_address', and Ansible must
# be run from within EC2. The key of an EC2 tag may optionally be used; however
# the boto instance variables hold precedence in the event of a collision.
# WARNING: - instances that are in the private vpc, _without_ public ip address
# will not be listed in the inventory until You set:
# vpc_destination_variable = private_ip_address
vpc_destination_variable = ip_address
# The following two settings allow flexible ansible host naming based on a
# python format string and a comma-separated list of ec2 tags. Note that:
#
# 1) If the tags referenced are not present for some instances, empty strings
# will be substituted in the format string.
# 2) This overrides both destination_variable and vpc_destination_variable.
#
#destination_format = {0}.{1}.example.com
#destination_format_tags = Name,environment
# To tag instances on EC2 with the resource records that point to them from
# Route53, set 'route53' to True.
route53 = False
# To use Route53 records as the inventory hostnames, uncomment and set
# to equal the domain name you wish to use. You must also have 'route53' (above)
# set to True.
# route53_hostnames = .example.com
# To exclude RDS instances from the inventory, uncomment and set to False.
rds = False
# To exclude ElastiCache instances from the inventory, uncomment and set to False.
elasticache = False
# Additionally, you can specify the list of zones to exclude looking up in
# 'route53_excluded_zones' as a comma-separated list.
# route53_excluded_zones = samplezone1.com, samplezone2.com
# By default, only EC2 instances in the 'running' state are returned. Set
# 'all_instances' to True to return all instances regardless of state.
all_instances = False
# By default, only EC2 instances in the 'running' state are returned. Specify
# EC2 instance states to return as a comma-separated list. This
# option is overridden when 'all_instances' is True.
# instance_states = pending, running, shutting-down, terminated, stopping, stopped
# By default, only RDS instances in the 'available' state are returned. Set
# 'all_rds_instances' to True return all RDS instances regardless of state.
all_rds_instances = False
# Include RDS cluster information (Aurora etc.)
include_rds_clusters = False
# By default, only ElastiCache clusters and nodes in the 'available' state
# are returned. Set 'all_elasticache_clusters' and/or 'all_elastic_nodes'
# to True return all ElastiCache clusters and nodes, regardless of state.
#
# Note that all_elasticache_nodes only applies to listed clusters. That means
# if you set all_elastic_clusters to false, no node will be return from
# unavailable clusters, regardless of the state and to what you set for
# all_elasticache_nodes.
all_elasticache_replication_groups = False
all_elasticache_clusters = False
all_elasticache_nodes = False
# API calls to EC2 are slow. For this reason, we cache the results of an API
# call. Set this to the path you want cache files to be written to. Two files
# will be written to this directory:
# - ansible-ec2.cache
# - ansible-ec2.index
cache_path = ./ec2-tmp
# The number of seconds a cache file is considered valid. After this many
# seconds, a new API call will be made, and the cache file will be updated.
# To disable the cache, set this value to 0
cache_max_age = 300
# Organize groups into a nested/hierarchy instead of a flat namespace.
nested_groups = False
# Replace - tags when creating groups to avoid issues with ansible
replace_dash_in_groups = True
# If set to true, any tag of the form "a,b,c" is expanded into a list
# and the results are used to create additional tag_* inventory groups.
expand_csv_tags = False
# The EC2 inventory output can become very large. To manage its size,
# configure which groups should be created.
group_by_instance_id = True
group_by_region = True
group_by_availability_zone = True
group_by_aws_account = False
group_by_ami_id = True
group_by_instance_type = True
group_by_instance_state = False
group_by_key_pair = True
group_by_vpc_id = True
group_by_security_group = True
group_by_tag_keys = True
group_by_tag_none = True
group_by_route53_names = True
group_by_rds_engine = True
group_by_rds_parameter_group = True
group_by_elasticache_engine = True
group_by_elasticache_cluster = True
group_by_elasticache_parameter_group = True
group_by_elasticache_replication_group = True
# If you only want to include hosts that match a certain regular expression
# pattern_include = staging-*
# If you want to exclude any hosts that match a certain regular expression
# pattern_exclude = staging-*
# Instance filters can be used to control which instances are retrieved for
# inventory. For the full list of possible filters, please read the EC2 API
# docs: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html#query-DescribeInstances-filters
# Filters are key/value pairs separated by '=', to list multiple filters use
# a list separated by commas. See examples below.
# If you want to apply multiple filters simultaneously, set stack_filters to
# True. Default behaviour is to combine the results of all filters. Stacking
# allows the use of multiple conditions to filter down, for example by
# environment and type of host.
stack_filters = False
# Retrieve only instances with (key=value) env=staging tag
instance_filters = tag:demo=traefik
# Retrieve only instances with role=webservers OR role=dbservers tag
# instance_filters = tag:role=webservers,tag:role=dbservers
# Retrieve only t1.micro instances OR instances with tag env=staging
# instance_filters = instance-type=t1.micro,tag:env=staging
# You can use wildcards in filter values also. Below will list instances which
# tag Name value matches webservers1*
# (ex. webservers15, webservers1a, webservers123 etc)
# instance_filters = tag:Name=webservers1*
# An IAM role can be assumed, so all requests are run as that role.
# This can be useful for connecting across different accounts, or to limit user
# access
# iam_role = role-arn
# A boto configuration profile may be used to separate out credentials
# see http://boto.readthedocs.org/en/latest/boto_config_tut.html
# boto_profile = some-boto-profile-name
[credentials]
# The AWS credentials can optionally be specified here. Credentials specified
# here are ignored if the environment variable AWS_ACCESS_KEY_ID or
# AWS_PROFILE is set, or if the boto_profile property above is set.
#
# Supplying AWS credentials here is not recommended, as it introduces
# non-trivial security concerns. When going down this route, please make sure
# to set access permissions for this file correctly, e.g. handle it the same
# way as you would a private SSH key.
#
# Unlike the boto and AWS configure files, this section does not support
# profiles.
#
# aws_access_key_id = AXXXXXXXXXXXXXX
# aws_secret_access_key = XXXXXXXXXXXXXXXXXXX
# aws_security_token = XXXXXXXXXXXXXXXXXXXXXXXXXXXX

1587
traefik/tf-ans-swarm/ec2.py Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
# Create a new VPC
resource "aws_vpc" "traefik_vpc" {
cidr_block = "10.1.0.0/16"
enable_dns_hostnames = "true"
enable_dns_support = "true"
tags {
Name = "traefik_vpc"
tool = "terraform"
demo = "traefik"
area = "networking"
}
}
# Create a public subnet in the new VPC
resource "aws_subnet" "traefik_pub_subnet" {
vpc_id = "${aws_vpc.traefik_vpc.id}"
cidr_block = "10.1.1.0/24"
map_public_ip_on_launch = "true"
tags {
Name = "traefik_pub_subnet"
tool = "terraform"
demo = "traefik"
area = "networking"
}
}
# Create a new Internet gateway
resource "aws_internet_gateway" "traefik_gw" {
vpc_id = "${aws_vpc.traefik_vpc.id}"
tags {
Name = "traefik_gw"
tool = "terraform"
demo = "traefik"
area = "networking"
}
}
# Create a route table for the new VPC
resource "aws_route_table" "traefik_rte_tbl" {
vpc_id = "${aws_vpc.traefik_vpc.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.traefik_gw.id}"
}
tags {
Name = "traefik_rte_tbl"
tool = "terraform"
demo = "traefik"
area = "networking"
}
}
# Associate route table with subnet in VPC
resource "aws_route_table_association" "traefik_rta" {
subnet_id = "${aws_subnet.traefik_pub_subnet.id}"
route_table_id = "${aws_route_table.traefik_rte_tbl.id}"
}

View file

@ -0,0 +1,7 @@
output "manager_pub_ip" {
value = ["${aws_instance.manager.public_ip}"]
}
output "worker_pub_ips" {
value = ["${aws_instance.worker.*.public_ip}"]
}

View file

@ -0,0 +1,3 @@
provider "aws" {
region = "${var.user_region}"
}

View file

@ -0,0 +1,73 @@
# Create a security group for worker nodes
resource "aws_security_group" "wkr_sg" {
vpc_id = "${aws_vpc.traefik_vpc.id}"
name = "wkr_sg"
description = "Security group for worker nodes"
ingress {
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["${aws_vpc.traefik_vpc.cidr_block}"]
}
ingress {
from_port = "22"
to_port = "22"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "wkr_sg"
tool = "terraform"
demo = "traefik"
area = "security"
}
}
# Create a security group for the manager
resource "aws_security_group" "mgr_sg" {
vpc_id = "${aws_vpc.traefik_vpc.id}"
name = "mgr_sg"
description = "Security group for the manager node"
ingress {
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["${aws_vpc.traefik_vpc.cidr_block}"]
}
ingress {
from_port = "8080"
to_port = "8080"
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
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 = ["0.0.0.0/0"]
}
egress {
from_port = "0"
to_port = "0"
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "mgr_sg"
tool = "terraform"
demo = "traefik"
area = "security"
}
}

View file

@ -0,0 +1,24 @@
variable "keypair" {
type = "string"
description = "SSH keypair to use to connect to instances"
}
variable "mgr_type" {
type = "string"
description = "AWS type to use when creating manager instances"
}
variable "wkr_type" {
type = "string"
description = "AWS type to use when creating worker instances"
}
variable "user_region" {
type = "string"
description = "AWS region to use for new resources"
}
variable "num_wkr_nodes" {
type = "string"
description = "Number of worker nodes to create"
}