How can I tag a root volume from an instance created by AWS CloudFormation?

Last updated: 2021-02-26

I want to tag the root volume of my Amazon Elastic Compute Cloud (Amazon EC2) instances that are created through AWS CloudFormation.

Short description

The tag property of the EC2 instance resource doesn't extend to the volumes that are created through AWS CloudFormation. Tagging can restrict the control that you have over your instances. Tagging helps you manage the costs of specific resources and restrict AWS Identity and Access Management (IAM) policies. Tagging also helps you exert similar control over other resources.

Bootstrapping with AWS CloudFormation allows you to tag the Amazon Elastic Block Store (Amazon EBS) root volume of your instance. The bootstrapping method is done through the UserData property of the AWS::EC2::Instance resource. To perform bootstrapping, use AWS Command Line Interface (AWS CLI) commands or standard Windows PowerShell commands after creating your instance.

Note: If you receive errors when running AWS CLI commands, make sure that you’re using the most recent AWS CLI version.

Resolution

Create an instance with an AWS CloudFormation template

1.    Open the AWS CloudFormation console.

2.    Choose Create Stack, and then choose Design template.

3.    In the code editor, on the Parameters tab, choose Template.

4.    For Choose template language, choose YAML.

5.    Copy either of the following JSON or YAML templates, and then paste that copied template into your code editor.

JSON template:

{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "AWS CloudFormation Sample Template Tagging Root Volumes of EC2 Instances: This template shows you how to automatically tag the root volume of the EC2 instances that are created through the AWS CloudFormation template. This is done through the UserData property of the AWS::EC2::Instance resource. **WARNING** This template creates two Amazon EC2 instances and an IAM role. You will be billed for the AWS resources used if you create a stack from this template.",
  "Parameters": {
    "KeyName": {
      "Type": "AWS::EC2::KeyPair::KeyName",
      "Description": "Name of an existing EC2 KeyPair to enable SSH access to the ECS instances."
    },
    "InstanceType": {
      "Description": "EC2 instance type",
      "Type": "String",
      "Default": "t2.micro",
      "AllowedValues": [
        "t2.micro",
        "t2.small",
        "t2.medium",
        "t2.large",
        "m3.medium",
        "m3.large",
        "m3.xlarge",
        "m3.2xlarge",
        "m4.large",
        "m4.xlarge",
        "m4.2xlarge",
        "m4.4xlarge",
        "m4.10xlarge",
        "c4.large",
        "c4.xlarge",
        "c4.2xlarge",
        "c4.4xlarge",
        "c4.8xlarge",
        "c3.large",
        "c3.xlarge",
        "c3.2xlarge",
        "c3.4xlarge",
        "c3.8xlarge",
        "r3.large",
        "r3.xlarge",
        "r3.2xlarge",
        "r3.4xlarge",
        "r3.8xlarge",
        "i2.xlarge",
        "i2.2xlarge",
        "i2.4xlarge",
        "i2.8xlarge"
      ],
      "ConstraintDescription": "Please choose a valid instance type."
    },
    "InstanceAZ": {
      "Description": "EC2 AZ.",
      "Type": "AWS::EC2::AvailabilityZone::Name",
      "ConstraintDescription": "Must be the name of an Availability Zone."
    },
    "WindowsAMIID": {
      "Description": "The Latest Windows 2016 AMI taken from the public Systems Manager Parameter Store",
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Default": "/aws/service/ami-windows-latest/Windows_Server-2016-English-Full-Base"
    },
    "LinuxAMIID": {
      "Description": "The Latest Amazon Linux 2 AMI taken from the public Systems Manager Parameter Store",
      "Type": "AWS::SSM::Parameter::Value<String>",
      "Default": "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
    }
  },
  "Resources": {
    "WindowsInstance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": {
          "Ref": "WindowsAMIID"
        },
        "InstanceType": {
          "Ref": "InstanceType"
        },
        "AvailabilityZone": {
          "Ref": "InstanceAZ"
        },
        "IamInstanceProfile": {
          "Ref": "InstanceProfile"
        },
        "KeyName": {
          "Ref": "KeyName"
        },
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "<powershell>\n",
                "try {\n",
                "$AWS_AVAIL_ZONE=(Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/placement/availability-zone' -UseBasicParsing).Content\n ",
                "$AWS_REGION=$AWS_AVAIL_ZONE.Substring(0,$AWS_AVAIL_ZONE.length-1)\n ",
                "$AWS_INSTANCE_ID=(Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/instance-id' -UseBasicParsing).Content\n ",
                "$ROOT_VOLUME_IDS=((Get-EC2Instance -Region $AWS_REGION -InstanceId $AWS_INSTANCE_ID).Instances.BlockDeviceMappings | where-object DeviceName -match '/dev/sda1').Ebs.VolumeId\n ",
                "$tag = New-Object Amazon.EC2.Model.Tag\n ",
                "$tag.key = \"MyRootTag\"\n ",
                "$tag.value = \"MyRootVolumesValue\"\n ",
                "New-EC2Tag -Resource $ROOT_VOLUME_IDS -Region $AWS_REGION -Tag $tag\n",
                "}\n",
                "catch {\n",
                "Write-Output $PSItem\n",
                "}\n",
                "</powershell>\n"
              ]
            ]
          }
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": {
              "Ref": "AWS::StackName"
            }
          }
        ],
        "BlockDeviceMappings": [
          {
            "DeviceName": "/dev/sdm",
            "Ebs": {
              "VolumeType": "io1",
              "Iops": "200",
              "DeleteOnTermination": "true",
              "VolumeSize": "10"
            }
          }
        ]
      }
    },
    "LinuxInstance": {
      "Type": "AWS::EC2::Instance",
      "Properties": {
        "ImageId": {
          "Ref": "LinuxAMIID"
        },
        "InstanceType": {
          "Ref": "InstanceType"
        },
        "AvailabilityZone": {
          "Ref": "InstanceAZ"
        },
        "IamInstanceProfile": {
          "Ref": "InstanceProfile"
        },
        "KeyName": {
          "Ref": "KeyName"
        },
        "UserData": {
          "Fn::Base64": {
            "Fn::Join": [
              "",
              [
                "#!/bin/sh\n",
                "AWS_AVAIL_ZONE=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)\n",
                "AWS_REGION=${AWS_AVAIL_ZONE::-1}\n",
                "AWS_INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)\n",
                "ROOT_VOLUME_IDS=$(aws ec2 describe-instances --region $AWS_REGION --instance-id $AWS_INSTANCE_ID --output text --query Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId)\n",
                "aws ec2 create-tags --resources $ROOT_VOLUME_IDS --region $AWS_REGION --tags Key=MyRootTag,Value=MyRootVolumesValue\n"
              ]
            ]
          }
        },
        "Tags": [
          {
            "Key": "Name",
            "Value": {
              "Ref": "AWS::StackName"
            }
          }
        ],
        "BlockDeviceMappings": [
          {
            "DeviceName": "/dev/sdm",
            "Ebs": {
              "VolumeType": "io1",
              "Iops": "200",
              "DeleteOnTermination": "true",
              "VolumeSize": "10"
            }
          }
        ]
      }
    },
    "InstanceRole": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Principal": {
                "Service": [
                  "ec2.amazonaws.com"
                ]
              },
              "Action": [
                "sts:AssumeRole"
              ]
            }
          ]
        },
        "Path": "/",
        "Policies": [
          {
            "PolicyName": "taginstancepolicy",
            "PolicyDocument": {
              "Version": "2012-10-17",
              "Statement": [
                {
                  "Effect": "Allow",
                  "Action": [
                    "ec2:Describe*"
                  ],
                  "Resource": "*"
                },
                {
                  "Effect": "Allow",
                  "Action": [
                    "ec2:CreateTags"
                  ],
                  "Resource": [
                    {
                      "Fn::Sub": "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:volume/*"
                    },
                    {
                      "Fn::Sub": "arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*"
                    }
                  ]
                }
              ]
            }
          }
        ]
      }
    },
    "InstanceProfile": {
      "Type": "AWS::IAM::InstanceProfile",
      "Properties": {
        "Path": "/",
        "Roles": [
          {
            "Ref": "InstanceRole"
          }
        ]
      }
    }
  }
}

YAML template:

AWSTemplateFormatVersion: 2010-09-09
Description: >-
  AWS CloudFormation sample template tagging root volumes of EC2 instances: This
  template shows how to automatically tag the root volume of the EC2 instances
  that are created through the AWS CloudFormation template. This is done through
  the UserData property of the AWS::EC2::Instance resource. **WARNING** This
  template creates two Amazon EC2 instances and an IAM role. You will be billed
  for the AWS resources used if you create a stack from this template.
Parameters:
  KeyName:
  Type: 'AWS::EC2::KeyPair::KeyName'
  Description: Name of an existing EC2 KeyPair to enable SSH access to the ECS instances.
  InstanceType:
  Description: EC2 instance type
  Type: String
  Default: t2.micro
  AllowedValues:
    - t2.micro
    - t2.small
    - t2.medium
    - t2.large
    - m3.medium
    - m3.large
    - m3.xlarge
    - m3.2xlarge
    - m4.large
    - m4.xlarge
    - m4.2xlarge
    - m4.4xlarge
    - m4.10xlarge
    - c4.large
    - c4.xlarge
    - c4.2xlarge
    - c4.4xlarge
    - c4.8xlarge
    - c3.large
    - c3.xlarge
    - c3.2xlarge
    - c3.4xlarge
    - c3.8xlarge
    - r3.large
    - r3.xlarge
    - r3.2xlarge
    - r3.4xlarge
    - r3.8xlarge
    - i2.xlarge
    - i2.2xlarge
    - i2.4xlarge
    - i2.8xlarge
  ConstraintDescription: Please choose a valid instance type.
  InstanceAZ:
  Description: EC2 AZ.
  Type: 'AWS::EC2::AvailabilityZone::Name'
  ConstraintDescription: Must be the name of an availabity zone.
  WindowsAMIID:
  Description: >-
    The Latest Windows 2016 AMI taken from the public Systems Manager
    Parameter Store
  Type: 'AWS::SSM::Parameter::Value<String>'
  Default: /aws/service/ami-windows-latest/Windows_Server-2016-English-Full-Base
  LinuxAMIID:
  Description: >-
    The Latest Amazon Linux 2 AMI taken from the public Systems Manager
    Parameter Store
  Type: 'AWS::SSM::Parameter::Value<String>'
  Default: /aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2
Resources:
  WindowsInstance:
  Type: 'AWS::EC2::Instance'
  Properties:
    ImageId: !Ref WindowsAMIID
    InstanceType: !Ref InstanceType
    AvailabilityZone: !Ref InstanceAZ
    IamInstanceProfile: !Ref InstanceProfile
    KeyName: !Ref KeyName
    UserData: !Base64 
    'Fn::Join':
      - ''
      - - |
        <powershell>
      - |
        try {
      - >-
        $AWS_AVAIL_ZONE=(Invoke-WebRequest -Uri
        'http://169.254.169.254/latest/meta-data/placement/availability-zone'
        -UseBasicParsing).Content
         
      - |-
        $AWS_REGION=$AWS_AVAIL_ZONE.Substring(0,$AWS_AVAIL_ZONE.length-1)
         
      - >-
        $AWS_INSTANCE_ID=(Invoke-WebRequest -Uri
        'http://169.254.169.254/latest/meta-data/instance-id'
        -UseBasicParsing).Content
         
      - >-
        $ROOT_VOLUME_IDS=((Get-EC2Instance -Region $AWS_REGION -InstanceId
        $AWS_INSTANCE_ID).Instances.BlockDeviceMappings | where-object
        DeviceName -match '/dev/sda1').Ebs.VolumeId
         
      - |-
        $tag = New-Object Amazon.EC2.Model.Tag
         
      - |-
        $tag.key = "MyRootTag"
         
      - |-
        $tag.value = "MyRootVolumesValue"
         
      - >
        New-EC2Tag -Resource $ROOT_VOLUME_IDS -Region $AWS_REGION -Tag
        $tag
      - |
        }
      - |
        catch {
      - |
        Write-Output $PSItem
      - |
        }
      - |
        </powershell>
    Tags:
    - Key: Name
      Value: !Ref 'AWS::StackName'
    BlockDeviceMappings:
    - DeviceName: /dev/sdm
      Ebs:
      VolumeType: io1
      Iops: '200'
      DeleteOnTermination: 'true'
      VolumeSize: '10'
  LinuxInstance:
  Type: 'AWS::EC2::Instance'
  Properties:
    ImageId: !Ref LinuxAMIID
    InstanceType: !Ref InstanceType
    AvailabilityZone: !Ref InstanceAZ
    IamInstanceProfile: !Ref InstanceProfile
    KeyName: !Ref KeyName
    UserData: !Base64 
    'Fn::Join':
      - ''
      - - |
        #!/bin/sh
      - >
        AWS_AVAIL_ZONE=$(curl
        http://169.254.169.254/latest/meta-data/placement/availability-zone)
      - |
        AWS_REGION=${AWS_AVAIL_ZONE::-1}
      - >
        AWS_INSTANCE_ID=$(curl
        http://169.254.169.254/latest/meta-data/instance-id)
      - >
        ROOT_VOLUME_IDS=$(aws ec2 describe-instances --region $AWS_REGION
        --instance-id $AWS_INSTANCE_ID --output text --query
        Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId)
      - >
        aws ec2 create-tags --resources $ROOT_VOLUME_IDS --region
        $AWS_REGION --tags Key=MyRootTag,Value=MyRootVolumesValue
    Tags:
    - Key: Name
      Value: !Ref 'AWS::StackName'
    BlockDeviceMappings:
    - DeviceName: /dev/sdm
      Ebs:
      VolumeType: io1
      Iops: '200'
      DeleteOnTermination: 'true'
      VolumeSize: '10'
  InstanceRole:
  Type: 'AWS::IAM::Role'
  Properties:
    AssumeRolePolicyDocument:
    Version: 2012-10-17
    Statement:
      - Effect: Allow
      Principal:
        Service:
        - ec2.amazonaws.com
      Action:
        - 'sts:AssumeRole'
    Path: /
    Policies:
    - PolicyName: taginstancepolicy
      PolicyDocument:
      Version: 2012-10-17
      Statement:
        - Effect: Allow
        Action:
          - 'ec2:Describe*'
        Resource: '*'
        - Effect: Allow
        Action:
          - 'ec2:CreateTags'
        Resource:
          - !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:volume/*'
          - !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*'
  InstanceProfile:
  Type: 'AWS::IAM::InstanceProfile'
  Properties:
    Path: /
    Roles:
    - !Ref InstanceRole

6.    In the UserData section of the template, update --tags Key=Name,Value=newAMI to match your requirements for a Linux instance. For a Windows instance, update $tag.key="MyRootTag" and $tag.value="MyRootVolumesValue". See the following UserData section examples for Linux and Windows.

Linux example:

#Linux UserData
  UserData:
     Fn::Base64: !Sub |
      #!/bin/bash
      AWS_AVAIL_ZONE=$(curl http://169.254.169.254/latest/meta-data/placement/availability-zone)
      AWS_REGION="`echo \"$AWS_AVAIL_ZONE\" | sed 's/[a-z]$//'`"
      AWS_INSTANCE_ID=$(curl http://169.254.169.254/latest/meta-data/instance-id)
      ROOT_VOLUME_IDS=$(aws ec2 describe-instances --region $AWS_REGION --instance-id $AWS_INSTANCE_ID --output text --query Reservations[0].Instances[0].BlockDeviceMappings[0].Ebs.VolumeId)
      aws ec2 create-tags --resources $ROOT_VOLUME_IDS --region $AWS_REGION --tags Key=MyRootTag,Value=MyRootVolumesValue

Windows example:

#Windows UserData with standard Powershell commands (no AWS CLI installed)
    UserData: 
    Fn::Base64: !Sub |
      <powershell>
      try {  
      $AWS_AVAIL_ZONE=(Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/placement/availability-zone' -UseBasicParsing).Content
      $AWS_REGION=$AWS_AVAIL_ZONE.Substring(0,$AWS_AVAIL_ZONE.length-1)
      $AWS_INSTANCE_ID=(Invoke-WebRequest -Uri 'http://169.254.169.254/latest/meta-data/instance-id' -UseBasicParsing).Content
      $ROOT_VOLUME_IDS=((Get-EC2Instance -Region $AWS_REGION -InstanceId $AWS_INSTANCE_ID).Instances.BlockDeviceMappings | where-object DeviceName -match '/dev/sda1').Ebs.VolumeId
      $tag = New-Object Amazon.EC2.Model.Tag
      $tag.key = "MyRootTag"
      $tag.value = "MyRootVolumesValue"
      New-EC2Tag -Resource $ROOT_VOLUME_IDS -Region $AWS_REGION -Tag $tag
      }
      catch {
      Write-Output $PSItem
      }
      </powershell>

Important: To use the AWS CLI commands with UserData, you must install the AWS CLI within the Amazon Machine Image (AMI) of your EC2 instances. The AWS CLI is installed by default on all Amazon Linux AMIs. You must also attach an instance profile to your EC2 instances. The instance profile includes the permissions to call the ec2:DescribeInstances and ec2:CreateTags APIs only on EC2 volumes and instances within the AWS Region and account.

7.    Choose the Create stack icon.

8.    For Stack name, enter a name for your stack.

9.    In the Parameters section, enter the appropriate information based on the needs of your environment, including your instance type, EC2 key pair, and AMI.

10.    Choose Next.

11.    In the Options section, enter the appropriate information for your stack, and then choose Next.

12.    To enable the AWS CloudFormation stack to create an IAM resource, select the "I acknowledge that AWS CloudFormation might create IAM resources" check box.

13.    Choose Create.

Tag the root volume of the instance

1.    Open the Amazon EC2 console.

2.    In the navigation pane, in the Elastic Block Store section, choose Volumes.

3.    In the Filter field, enter the tag that you set in the AWS CloudFormation stack to confirm that the volume was tagged.


Did this article help?


Do you need billing or technical support?