Cloudformation hack loop with ansible

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.

step 1: click like any good old windows user would.

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.

step 2: rtfm

step 3: grab an example and clean it up

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

step 4: turn it into an ansible template

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.

step 5: hack it till it works

Run, look at the cloudformation events, fix errors, Run again…