2017-08-16
So for me cloudformation is a tad verbose. Intrinsic functions are cute but messy, there are no loops and the conditions are a little limited.
Instead of carrying any params, choices, conditions etc in cloudformation, move it to ansible and use it in a jinja template.
Here is a recipe for hacking cloudformation with ansible, featuring a vpc as an example.
Use the console Luke, understand the concepts and components around the particular service you are hacking on, and deploy something manually. I know it feels dirty, but push on through it.
Look for an example cloudformation templates online…
If it is in json convert it to yaml with:
because comments! and you want to read it too…
and clean it up with your favourite text editor
if you can’t find any, roll your own by finding the resources in the Template Reference
Since we are using ansible’s template module to replace parameters and stamp out the cloudformation template, turn parameters into jinja variable references and clean it up so you eventually end up with something simple.
Below is an example playbook, defining the vpc we want to setup.
---
- hosts: localhost
connection: local
gather_facts: False
vars:
stack_name: foodev
vpc:
name: "{{stack_name}}"
region: eu-west-1
cidr: 10.100.1.0/24
igw: yes
subnets:
- { name: PublicSNa, az: eu-west-1a, cidr: 10.100.1.0/26, route_table: RtPub, natgw: NatGW1 }
- { name: PublicSNb, az: eu-west-1b, cidr: 10.100.1.64/26, route_table: RtPub }
- { name: PrivateSNa, az: eu-west-1a, cidr: 10.100.1.128/26, route_table: RtPriv }
- { name: PrivateSNb, az: eu-west-1b, cidr: 10.100.1.192/26, route_table: RtPriv }
route_tables:
- name: RtPub
routes:
- { name: igw, cidr: 0.0.0.0/0, igw: yes }
- name: RtPriv
routes:
- { name: NatGW1, cidr: 0.0.0.0/0, natgw: NatGW1 }
roles:
- name: vpc_cf
and the template snippet associated with it.
#jinja2: lstrip_blocks: True
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "AWS VPC template {{vpc.name}}"
Resources:
{{vpc.name}}VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: {{vpc.cidr}}
EnableDnsSupport: true
EnableDnsHostnames: true
Tags:
- { Key: Application, Value: !Ref "AWS::StackId" }
- { Key: Name, Value: {{vpc.name}}VPC }
{% if vpc.igw is defined %}
{{vpc.name}}InternetGateway:
Type: AWS::EC2::InternetGateway
Properties:
Tags:
- { Key: Application, Value: !Ref "AWS::StackId" }
- { Key: Name, Value: {{vpc.name}}InternetGateway }
{{vpc.name}}AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref {{vpc.name}}VPC
InternetGatewayId: !Ref {{vpc.name}}InternetGateway
{% endif %}
{% for rt in vpc.route_tables %}
{{vpc.name}}{{rt.name}}RouteTable:
Type: AWS::EC2::RouteTable
Properties:
VpcId: !Ref {{vpc.name}}VPC
Tags:
- { Key: Application, Value: !Ref "AWS::StackId" }
- { Key: Name, Value: {{vpc.name}}{{rt.name}}RouteTable }
{% endfor %}
{% for subnet in vpc.subnets %}
{{vpc.name}}{{subnet.name}}Subnet:
Type: AWS::EC2::Subnet
Properties:
AvailabilityZone: {{subnet.az}}
VpcId: !Ref {{vpc.name}}VPC
CidrBlock: {{subnet.cidr}}
Tags:
- { Key: Application, Value: !Ref "AWS::StackId" }
- { Key: Name, Value: {{vpc.name}}{{subnet.name}} }
{{vpc.name}}{{subnet.name}}SubnetRouteTableAssociation:
Type: AWS::EC2::SubnetRouteTableAssociation
Properties:
SubnetId: !Ref {{vpc.name}}{{subnet.name}}Subnet
RouteTableId: !Ref {{vpc.name}}{{subnet.route_table}}RouteTable
{{vpc.name}}{{subnet.name}}SubnetNetworkAclAssociation:
Type: AWS::EC2::SubnetNetworkAclAssociation
Properties:
SubnetId: !Ref {{vpc.name}}{{subnet.name}}Subnet
NetworkAclId: !Ref {{vpc.name}}NetworkAcl
{% if subnet.natgw is defined %}
{{vpc.name}}{{subnet.natgw}}IPAddress:
Type: AWS::EC2::EIP
DependsOn: {{vpc.name}}AttachGateway
Properties:
Domain: vpc
{{vpc.name}}{{subnet.natgw}}NatGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt {{vpc.name}}{{subnet.natgw}}IPAddress.AllocationId
SubnetId: !Ref {{vpc.name}}{{subnet.name}}Subnet
{% endif %}
{% endfor %}
{% for rt in vpc.route_tables %}
{% for route in rt.routes %}
{{vpc.name}}{{rt.name}}{{route.name}}Route:
Type: AWS::EC2::Route
{% if route.vpngateway is defined %}
DependsOn: [{{vpc.vpn.name}}VPNGateway, {{vpc.vpn.name}}VPCGatewayAttachment]
{% endif %}
Properties:
RouteTableId: !Ref {{vpc.name}}{{rt.name}}RouteTable
DestinationCidrBlock: {{route.cidr}}
{% if route.instance is defined %}
InstanceId: !Ref {{vpc.name}}{{route.instance}}EC2Instance
{% elif route.natgw is defined %}
NatGatewayId: !Ref {{vpc.name}}{{route.natgw}}NatGateway
{% elif route.igw is defined %}
GatewayId: !Ref {{vpc.name}}InternetGateway
{% elif route.vpngateway is defined %}
GatewayId: !Ref {{vpc.vpn.name}}VPNGateway
{% endif %}
{% endfor %}
{% endfor %}
Outputs:
StackName:
Value: !Ref AWS::StackName
Regionname:
Value: !Ref AWS::Region
{{vpc.name}}VPC:
Value: !Ref {{vpc.name}}VPC
Export:
Name: {{vpc.name}}VPC
{% for subnet in vpc.subnets %}
{{vpc.name}}{{subnet.name}}Subnet:
Value: !Ref {{vpc.name}}{{subnet.name}}Subnet
Export:
Name: {{vpc.name}}{{subnet.name}}Subnet
{% endfor %}
Here’s an example of the role (vpc_cf) that stamps out a vpc template and then creates it.
---
- block:
- name: remove stack "{{stack_name}}"
cloudformation:
stack_name: "{{stack_name}}"
state: "absent"
region: "{{region}}"
when: absent is defined
- block:
- name: stamp out the template
template: src=vpc.template.yml.j2 dest=./generated/vpc/{{stack_name}}.vpc.template.yml
- name: create / update stack "{{stack_name}}"
cloudformation:
stack_name: "{{stack_name}}"
state: "present"
region: "{{region}}"
disable_rollback: true
template: "./generated/vpc/{{stack_name}}.vpc.template.yml"
tags:
Stack: "ansible-cloudformation"
register: output
- debug: var=output.stack_outputs
when: absent is not defined
The first block will remove it if you pass -e absent=yup
on the command line.
Instead of a verbose cloudformation template, you end up with a simple variable statement and a role.
Run, look at the cloudformation events, fix errors, Run again…