Ansible and AWS Security Groups
We use Amazon CloudFormation for a number of our deployments at $WORK. Although it’s nice to have security group creation inside the same template as the resources it will secure, CloudFormations ‘helpful’ addition of a unique string at the end of the resource names it creates can sometimes be a problem. A couple of tools assume security groups will have an absolute, unchanging name and lack a way to search for an appropriately tagged security group whose name can change on stack rebuild. In order to get around this I tried extracting some of our security group creation from CloudFormation and implemented it in Ansible instead.
For this example I’m going to assume you have a working Ansible install and an AWS account. While you can specify your AWS AWS_ACCESS_KEY and AWS_SECRET_KEY in your Ansible play books I set mine as environmental variables to avoid the risk of checking them in to a VCS. First we create our directory structure, group variables and site.yml -
export AWS_ACCESS_KEY=YOURKEY
export AWS_SECRET_KEY=YOURSECRET
# we'll do all our work under here
mkdir -p aws-sg aws-sg/{group_vars,roles/bastionhosts/tasks}
cd aws-sg
echo '127.0.0.1' > hosts
# add regions to create the security groups in here
cat << 'EOC' > group_vars/all
---
regions:
- eu-west-1
- us-east-1
- us-east-2
EOC
# add the task to run
cat << 'EOC' > site.yml
---
- hosts: all
roles:
- role: bastionhosts
EOC
Now we’ve created our scaffolding we can actually write tasks to create our security group
cat << 'EOC' > ./roles/bastionhosts/tasks/main.yml
---
- name: "ssh ingress security group in region {{ item }}"
local_action:
module: ec2_group
name: bastion-ingress
description: Bastion host servers
region: "{{ item }}"
rules:
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0
register: bastion_ingress
with_items: regions
EOC
The above code creates the ‘bastion-ingress’ security group in each
region you specified in group_vars/all
.
with_items
will run the task once for each region in
regions. It also sets item to the current array element so it can be
used within the task. The other line of interest is register
. This stores meta-data about the newly created security group in an array that we can use later.
We now have our security group allowing ssh traffic in to the bastion hosts. We’ll often want to allow ssh traffic from bastion hosts, those with the bastion-ingress group assign to them, to “internal” hosts that are not publicly reachable. To do this we add a second security group, that we assign to the internal hosts, that allows traffic in on port 22 from the existing bastion-ingress group. This reduces the number of places the internal hosts can be reached from and is often useful in default VPCs.
cat << 'EOC' >> ./roles/bastionhosts/tasks/main.yml
- name: "ssh from bastion hosts group in region {{ item }}"
local_action:
module: ec2_group
name: bastion-clients
description: Allows bastion servers to connect
region: "{{ item.item }}"
rules:
- proto: tcp
from_port: 22
to_port: 22
group_id: "{{ item.group_id }}"
with_items: bastion_ingress.results
EOC
This new security group task is run for each entry in
bastion_ingress.results
, the array we stored the
creation meta-data of the bastion-ingress group in earlier using
register
. We then use the group_id to limit where we allow
incoming traffic from for this new group.
You can then run it with -
ansible-playbook -i hosts site.yml -v
As an approach, the pattern of storing with register
and
iterating with with_items: foo.results
is a very handy one
and I suspect it will be used in many of my playbooks. In terms of the
ec2_group module itself it’s inability to handle egress rules limits the
number of places we can use it but it’s a nice proof of concept for
running under old, ec2 classic based deployments.