Heat ASGs with Floating IP per Instance

How to assign a floating IP address to each instance of an autoscaling group in OpenStack Heat

OpenStack Heat is the orchestration project of OpenStack, which allows to describe and provision OpenStack resources based on Heat templates, similar to Amazon CloudFormation. For a research project, I recently had the requirement to assign a floating IP address to every instance of a Heat autoscaling group (ASG). Given the fact that such setup is contradictory to the common practice having a load balancer in front of an ASG, this costed me some time to figure out.

Creating a Single Instance Using Heat

Setting up a single instance using a Heat template (HOT) is pretty easy, straightforward and also covered in the Heat templates repository:

# single_server_with_floating_ip.yaml
# (abbreviated example based on hot/servers_in_existing_neutron_net.yaml)

parameters:
  # ~~~snip~~~
  network:
    type: string
    description: The network for the VM
    constraints:
      - {custom_constraint: neutron.network}
  public_net:
    type: string
    description: ID of public network for which floating IP addresses will be allocated
    constraints:
      - {custom_constraint: neutron.network}

resources:
  server1:
    type: OS::Nova::Server
    properties:
      name: Server1
      image: { get_param: image }
      networks:
        - port: { get_resource: server1_port }
      
  server1_port:
    type: OS::Neutron::Port
    properties:
      network_id: { get_param: network }
      security_groups: [{ get_resource: server_security_group }]

  server1_floating_ip:
    type: OS::Neutron::FloatingIP
    properties:
      floating_network_id: { get_param: public_net }
      port_id: { get_resource: server1_port }

As we can see, the server1_floating_ip (of type OS::Neutron::FloatingIP) is NAT’ed to the server1_port which in turn is assigned to the server instance (type OS::Nova::Server) that we instantiate.

Creating an Autoscaling Group

In order to create an ASG of multiple instances, each only with a private IP, we also find help in the Heat templates repository:

# abbreviated example based on hot/asg_of_servers.yaml

parameters:
  # ~~~snip~~~
  image:
    type: string
    description: Name or ID of the image to use for the instances.
  network:
    type: string
    description: The network for the VM
    default: private

resources:
  asg:
    type: OS::Heat::AutoScalingGroup
    properties:
      resource:
        type: OS::Nova::Server
        properties:
            key_name: { get_param: key_name }
            image: { get_param: image }
            flavor: { get_param: flavor }
            networks: [{network: {get_param: network} }]
      min_size: 1
      desired_capacity: 3
      max_size: 10

We see the AutoScalingGroup defined with the Nova server instance being scaled up and down (between 1 and 10 instances). Using the scale up and down URLs provided, one can test the functionality of auto scaling.

Recalling our goal, namely to assign a floating IP address to every instance of the ASG (and also to the ones created during scale-out), it looks intuitively right to extend the resource block of the previous template. However, only one resource can be given to the AutoScalingGroup. For our use case, we also need the floating IP resources to be added and destroyed on demand.

Scaling a Complete Stack

The solution to my problem was finally hidden in one of the other examples, the ASG of stacks. Essentially, Heat can also scale a complete stack (defined by its own template file). So what we now do is essentially to treat the server with its floating IP as defined in the first listing (single_server_with_floating_ip.yaml) as entity of scale and pass through all parameters:

# asg_of_server_with_floating.yaml
# (abbreviated example based on hot/asg_of_stacks.yaml)
parameters:
  # ~~~snip~~~
  image:
    type: string
    description: Name or ID of the image to use for the instances.
  network:
    type: string
    description: The network for the VM
    constraints:
      - {custom_constraint: neutron.network}
  public_net:
    type: string
    description: ID of public network for which floating IP addresses will be allocated
    constraints:
      - {custom_constraint: neutron.network}
resources:
  asg:
    type: OS::Heat::AutoScalingGroup
    properties:
      resource:
        # this refers to the file previously described
        type: server_with_floating.yaml
        properties:
            image: { get_param: image }
            network: { get_param: network }
            public_net: { get_param: public_net}
      min_size: 1
      desired_capacity: 3
      max_size: 10

As the type of of resource, we can also supply a file name. That’s.. let me think.. not obvious? After long search, was able to find this behavior documented under Template composition.

Mind the public_net parameter defining the public network ID, from which floating IPs are assigned. Once we trigger a scale-out, we can see antoehr instance including a floating IP being added after some seconds:

screenshot horizon

I am not aware of a way to launch such stack using the Horizon dashboard, except when the inner template is referenced via an HTTP(S) URL. Instead, one can launch it using the command line client:

$ openstack stack create my-test-stack --template asg_of_server_with_floating.yaml -e environment.yaml

All definition of the required parameters, i.e., image and network IDs, are happening in the environment.yaml file.

While my use case was a little academic (why in the world should something scale inside an OpenStack cloud and be directly reachable from the outside world), I think this (to me still unknown) concept of template composition helps solving many more problems.

The complete code can be found in this gist.
(cover image by sipa on pixabay.com)