cfn-pyplates and cfn-init magic


For some, this information may be old hat, but for a while I’ve been looking for a good way to run cfn-init within UserData during an instance launch. I had a catch 22 scenario happening when building UserData with references to AWS::StackId because I’m using cfn-pyplates to build the CloudFormation json. Until recently, I couldn’t find a good way to use resource references while base64 encoding a build script in LaunchConfiguration…

The solution turned out to be a combination of str and join to build the head of the script while reading in the remainder with an open call:

interpreter = str('#!/bin/bash\n')
default_region = join('', str('export AWS_DEFAULT_REGION='), ref('AWS::Region'), str('\n'))
cfn_init_cmd = join('', str('/opt/aws/bin/cfn-init -s '), ref('AWS::StackId'), str(' -r WebLaunchConfiguration --region '), ref('AWS::Region'), str('\n'))
build_script = open('./web_build.sh').read()
user_data_script = join('', interpreter, default_region, cfn_init_cmd, build_script)

and in my LaunchConfiguration resource:

cft.resources.add(
  Resource('WebLaunchConfiguration', 'AWS::AutoScaling::LaunchConfiguration',
    {
      'ImageId': ref('InstanceAMI'),
      'InstanceType': ref('EC2InstanceType'),
      'UserData': base64(user_data_script),
      'SecurityGroups': [ref('WebSG'), ref('AdminSG')],
      'KeyName': 'default',
      'IamInstanceProfile': 'WebInstanceRole',
    },
    Metadata(
      {
        'AWS::CloudFormation::Init':
          {
            'config': {
              'files': {
                '/etc/cloud-env.sh': {
                  'content': join('',
                    "export DBName=", ref('DBName'), "\n",
                    "export DBUsername=", ref('DBUsername'), "\n",
                    "export DBPassword=", ref('DBPassword'), "\n",
                    "export DBAddress=", get_att('RDSInstance', 'Endpoint.Address'), "\n",
                    "export DBPort=", get_att('RDSInstance', 'Endpoint.Port'), "\n",
                    "export DBPrefix=", ref('DBPrefix'), "\n",
                ),
                'mode': '000400',
                'owner': 'root',
                'group': 'root'
              },
            }
          }
        }
      }
    )
  )
)

Boom! Now I could source /etc/cloud-env.sh in UserData and have a secure way to pass all my parameters to the instances…