Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ferricoxide/2b073e0cc2a69b0ec65bcfbe4e4ecbb9 to your computer and use it in GitHub Desktop.
Save ferricoxide/2b073e0cc2a69b0ec65bcfbe4e4ecbb9 to your computer and use it in GitHub Desktop.
Describes methods for making CFn templates for Linux EC2 instances compatible across generations

Intro

With AWS's introduction of fifth generation instance types, EC2 instances' block-devices' device-naming changes. In the first- through fourth-generation instance-types, block-devices' device-naming was based on the Xen Virtual Device (XVD) storage-driver. The resulting device node-names took the form /dev/xvdN. With fifth-generation instance types, AWS switches to presenting virtual NVMe devices to the instance. These devices' node-naming take the form dev/nvmeXnY (where X is representative of the device's number on the virtual NVMe bus and Y is representative of the partition on that device). In order for CloudFormation (CFn) templates for Linux-based EC2s to function correctly for older instance-families and fifth-generation instance families, the templates need to be have device-naming selection-logic added to them. Typically, this will mean additional code within a CFn template's Conditions{} and Mappings{} sections. Additionally, AWS::EC2::Instance resource-types' Properties{} section will likely need updates to their BlockDeviceMappings[] list and UserData{} sections.

Conditions{} Section:

This section can be use to add logic for parsing an instance-type delcaration (in this case, via an InstanceType parameter declared in the CFn template's Parameters{} section) to compute whether the CFn is being used to deploy a fifth-generation or an older instance-type. The below-shown logic is a negative-selector; however, rewriting as a positive-selector would be trivial:

"NotGenFive": {
  "Fn::Not": [
    {
      "Fn::Or": [
        {
          "Fn::Equals": [
            { "Fn::Select": [
                "0",
                { "Fn::Split": [ ".", { "Ref": "InstanceType" } ] }
              ]
            },
            "c5"
          ]
        },
        {
          "Fn::Equals": [
            { "Fn::Select": [
                "0",
                { "Fn::Split": [ ".", { "Ref": "InstanceType" } ] }
              ]
            },
            "m5"
          ]
        },
        {
          "Fn::Equals": [
            { "Fn::Select": [
                "0",
                { "Fn::Split": [ ".", { "Ref": "InstanceType" } ] }
              ]
            },
            "t3"
          ]
        }
      ]
    }

The above CFn-block uses CFn's Fn::Or logic to allow for a "matches any" overall evaluation to be performed. In the above, there are three subordinate evaluations: one that checks if an instance is a "c5" instance-family; one that checks if an instance is an "m5" instance-family; and, finally, one that checks if an instance is a "t3" instance-family. As new fifth-generation (and presumably later) instance-type families are added, further match-blocks can be trivially added.

Each subordinate-evaluation uses CFn's Fn::Equals logic. This logic compares two elements — in these blocks' cases, strings, and checks to see if they are matched. If the compared strings match, the block returns a true condition up to the main, Fn::Or evaluation.

The strings compared in each of the Fn::Equals are a static value — c5, m5 and t3 — compared to a derived-value.

In the above, CFn's Fn::Select and Fn::Split are used to derive the value:

  • The Fn::Split is used to take the value of the InstanceType parameter — typically something like c5.xlarge, t3.micro, etc. — and create a list of values. The Fn::Split uses the . as its split-delimeter. In the case of a InstanceType string-value of m5.2xlarge, this creates a two-item list of m5 and 2xlarge.
  • The Fn::Select is used to extract element zero from the list created by the Fn::Split — per the preceding bullet, this would extract the string-value m5.

Given that the derived-value of m5 equals that static-value of m5 the second Fn::Equals block will return a true condition up to the main, Fn::Or evaluation.

Mappings{} Section:

The mappings section contains value-mappings that can be used elsewhere in the CFn template. In this snippet, only device-name mappings are defined:

"InstanceTypeCapabilities": {
  "IsGenFive": {
    "ExternDeviceName": "/dev/xvdf",
    "InternDeviceName": "/dev/nvme1n1"
  },
  "PreGenFive": {
    "ExternDeviceName": "/dev/xvdf",
    "InternDeviceName": "/dev/xvdf"
  }
}

At the AWS (cloud) layer, the (external) device-name will remain a /dev/xvdN name, regardless of instance type. Inside the Linux-based instance, it will be either a /dev/xvdN or /dev/nvmeXpY name depending on instance-type. The previous Conditions{} will be used elsewhere in the template to pull an appropriate value from the previous Mappings.

Properties{} Section:

Each CFn-managed Resource has a Properties{} section. This section defines the deployment-behavior for the managed-resource. For EC2 resource-types, adding the compatibility-logic is done in two sub-sections of the Properties{} section: the BlockDeviceMappings[] list and the UserData{} section.

BlockDeviceMappings[] List:

The BlockDeviceMappings[] list is list where each element in the list is a ditctionary (a data structure type). Each dictionary defines the storage-layout of an EBS-volume attached to the CFn-managed EC2 instance. Each dictionary contains two main elements:

  • The DeviceName parameter - the value of this parameter defines the EBS volume's AWS-level attachment-point to the EC2 instance
  • The Ebs sub-dictionary that defines the characteristics of the attached EBS

The sub-dictionary defines the attached EBS's characteristics by three parameter-values:

  • The DeleteOnTermination selects whether the EBS should be deleted when the attached-to EC2 insance is terminated
  • The VolumeSize selects the size of the EBS volume
  • The VolumeType selects the type of EBS volume to create/attach
        "BlockDeviceMappings": [
          {
            "DeviceName": "/dev/sda1",
            "Ebs": {
              "DeleteOnTermination": true,
              "VolumeSize": { "Ref": "RootVolumeSize" },
              "VolumeType": "gp2"
            }
          },
          {
            "Fn::If": [
              "CreateAppVolume",
              {
                "DeviceName": {
                  "Fn::If": [
                    "NotGenFive",
                    {
                      "Fn::FindInMap": [
                         "InstanceTypeCapabilities",
                         "PreGenFive",
                         "ExternDeviceName"
                      ]
                    },
                    {
                      "Fn::FindInMap": [
                        "InstanceTypeCapabilities",
                        "IsGenFive",
                        "ExternDeviceName"
                      ]
                    }
                  ]
                },
                "Ebs": {
                  "DeleteOnTermination": "true",
                  "VolumeSize": { "Ref": "AppVolumeSize" },
                  "VolumeType": { "Ref": "AppVolumeType" }
                }
              },
              { "Ref": "AWS::NoValue" }
            ]
          }
        ],

In the above block:

  • The first dictionary in the list defines the attributes of the EBS attached at /dev/sda1. This is the EC2's boot disk. The EBS is configured: to delete on termination of the EC2; be provisioned at a size defined by the template-user via the RootVolumeSize parameter; will be of the generic SSD EBS-type (gp2).
  • The second dictionary in the list is conditionally defined by layered conditions:
    • If the CreateAppVolume condition evaluates to true, defines a secondary EBS.
      • The DeviceName parameter value is determined by how the NotGenFive condition is evaluated:
        • If NotGenFive evaulates to true, the PreGenFive value for ExternDeviceName is extracted from the InstanceTypeCapabilities mapping (from the previously discussed Mappings{} section).
        • If NotGenFive evaulates to false, the IsGenFive value for ExternDeviceName is extracted from the InstanceTypeCapabilities mapping (from the previously discussed Mappings{} section).
      • The VolumeSize value is set based on the value of the AppVolumeSize parameter
      • The VolumeType value is set based on the value of the AppVolumeTypeparameter
    • If the CreateAppVolume condition evaluates to false, the second dictionary becomes null/undefined (rendering the BlockDeviceMapping list a one-element list)

UserData{} Section:

The UserData section offers several opportunities for executing actions around the attached EBS volume(s):

  • cloud-config operations:
    • growpart Directive: If supplied a list of (possibly) attached devices, cloud-config will see if any listed-device nodes are available and whether they can be grown. If growable devices exist, growpart will extend the the device (partition) onto the avialble contiguous space on the disk. If now growable devices exist, the growpart action will simply exit 0, allowing the other cloud-config actions to occur.
    • bootcmd directive: This directive allows the execution of configuration commnds early in cloud-config's operation. Anything that only might exist for modification needs to be placed inside a conditional-evaluation block (an Fn::If structure referencing the template's Conditions{} section) to prevent an error-abort.
      • cloud-init-per directive attempts to create an ext4 filesystem onto a condition-checked device: the subordinate Fn::If blocks evaluate the NotGenFive conditional to extract an InternDeviceName value from the appropriate PreGenFive or IsGenFive mapping.
      • mounts directive attempts to add an appropriate entry to the EC2's /etc/fstab file using a condition-checked device-name (using the same logic used by the cloud-init-per directive).
  • Scripted operations: These can be simple or complex, BASH-scripted actions that execute at first boot. The block:
    if [[ -x $( which pvs ) ]]
    then
       LVMPVS=($(pvs --noheadings -o pv_name))
       for PV in "${LVMPVS[@]}"
       do
          pvresize "${PV}"
       done
    fi
    
    Looks for LVM PVs to resize. This block is necessary/desirable on instances with LVM'ed boot volumes that have been grown but not directly related to instance-type compatibility. It is simply provided as storage-related example of scripted operations.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment