I’ve blogged before about my passion for automation and the use of ARM templating in the Azure world to eradicate the burden of dull and mundane tasks from the daily routine of system administrators for whom I do consulting for.
I loath repetitive tasks, its in this space where subtle differences and inconsistency love to live. Recently I was asked to help out with a simple task, provisioning a couple of EC2 Windows servers in AWS. So in the spirit of infrastructure as code, I thought, there is no better time to try out AWS CloudFormation to describe my EC2 instances . I’ve actually used CloudFormation before in the past, but always describing my stack in JSON. CloudFormation also supports YAML, so challenge accepted and away I went. . .
So what
is YAML anyway. . .Yet Another Mark-up Language. Interestingly its described at
the official YAML website (https://yaml.org) as
a “YAML Ain’t Markup Language” rather, “human friendly data serialisation
standard for all programming languages”.
What
attracted me to YAML is its simplicity, there are no curly braces {} just
indenting. Its also super easy to read. So if JSON looks a bit to cody for your
liking, YAML may be a more palatable alternative.
So how would you get started? As you’d expect AWS have extensive CloudFormation documentation. The AWS::EC2::Instance resource is described here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-instance.html#cfn-ec2-instance-volumes. You’ll notice that there is a Syntax description for JSON and YAML. The YAML looks like this:
Type: AWS::EC2::Instance
Properties:
Affinity: String
AvailabilityZone: String
BlockDeviceMappings:
- EC2 Block Device Mapping
CreditSpecification: CreditSpecification
DisableApiTermination: Boolean
EbsOptimized: Boolean
ElasticGpuSpecifications: [ ElasticGpuSpecification, ... ]
ElasticInferenceAccelerators:
- ElasticInferenceAccelerator
HostId: String
IamInstanceProfile: String
ImageId: String
InstanceInitiatedShutdownBehavior: String
InstanceType: String
Ipv6AddressCount: Integer
Ipv6Addresses:
- IPv6 Address Type
KernelId: String
KeyName: String
LaunchTemplate: LaunchTemplateSpecification
LicenseSpecifications:
- LicenseSpecification
Monitoring: Boolean
NetworkInterfaces:
- EC2 Network Interface
PlacementGroupName: String
PrivateIpAddress: String
RamdiskId: String
SecurityGroupIds:
- String
SecurityGroups:
- String
SourceDestCheck: Boolean
SsmAssociations:
- SSMAssociation
SubnetId: String
Tags:
- Resource Tag
Tenancy: String
UserData: String
Volumes:
- EC2 MountPoint
AdditionalInfo: String
With this as a starting point I was quickly able to build a EC2 instance and customise my YAML so as to do some extra things.
If you’ve
got this far and YAML is starting to look like it might be the ticket for you,
its worth familiarising yourself with the CloudFormation built-in functions.
You can use these to do things like assign values to properties that are not
available until runtime.
Fn::Base64
Fn::Cidr
Condition Functions
Fn::FindInMap
Fn::GetAtt
Fn::GetAZs
Fn::Join
Fn::Select
Fn::Split
Fn::Sub
Fn::Transform
Ref
The link to the complete Intrinsic Function Reference can be found here: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference.html
With a learning curve of a couple of hours including a bit of googling and messing around I was able to achieve my goal. I built an EC2 instance, applied tagging, installed some Windows features post build via a PowerShell script (downloaded from S3 and launched with AWS::CloudFormation::Init cfn-init.exe), all without having to logon to the server or touch the console. Here is a copy of my YAML. . .
AWSTemplateFormatVersion: "2010-09-09"
Description: CloudFormation Template to deploy an EC2 instance
Parameters:
Hostname:
Type: String
Description: Hostname - maximum 15 characters
MaxLength: '15'
LatestAmiId :
Type: 'AWS::SSM::Parameter::Value'
Default: /aws/service/ami-windows-latest/Windows_Server-2019-English-Full-Base
InstanceSize:
Type: String
Description: Instance Size
Default: t2.micro
AllowedValues:
- "t2.micro"
- "t2.small"
- "t2.medium"
AvailabilityZone:
Type: String
Description: Default AZ
AllowedValues:
- ap-southeast-2a
- ap-southeast-2b
- ap-southeast-2c
Default: ap-southeast-2a
KeyPair:
Type: String
Description: KeyPair Name
Default: jtwo
S3BucketName:
Default: NotARealBucket
Description: S3 bucket containing boot artefacts
Type: String
# tag values
awPurpose:
Type: String
Description: A plain English description of what the object is for.
Default: WindowsServer2019 Domain Controller
awChargeTo:
Type: String
Description: Billing Code for charge back of resource.
Default: IT-123
awRegion:
Type: String
Description: Accolade Wines Region not AWS.
Default: Australia
awExpiry:
Type: String
Description: The date when the resource(s) can be considered for decommissioning.
Default: 01-01-2022
awBusinessSegment:
Type: String
Description: Agency code.
Default: ICT
awEnvironment:
Type: String
Description: Specific environment for resource.
AllowedValues:
- prod
- prodServices
- nonprod
- uat
- dev
- test
awApplication:
Type: String
Description: A single or multiple word with the name of the application that the infrastructure supports. "JDE", "AD", "Apache", "Utility", "INFOR", "PKI".
Default: AD
Mappings:
SubnetMap:
ap-southeast-2a:
prodServices: "subnet-idGoesHere"
ap-southeast-2b:
prodServices: "subnet-idGoesHere"
ap-southeast-2c:
prodServices: "subnet-idGoesHere"
# Resources
Resources:
# IAM Instance Profile
Profile:
Type: 'AWS::IAM::InstanceProfile'
Properties:
Roles:
- !Ref HostRole
Path: /
InstanceProfileName: !Join
- ''
- - 'instance-profile-'
- !Ref S3BucketName
HostRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: !Join
- ''
- - 'role-s3-read-'
- !Ref S3BucketName
Policies:
- PolicyDocument:
Version: 2012-10-17
Statement:
- Action:
- 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref S3BucketName
- '/*'
Effect: Allow
PolicyName: s3-policy-read
Path: /
AssumeRolePolicyDocument:
Statement:
- Action:
- 'sts:AssumeRole'
Principal:
Service:
- ec2.amazonaws.com
Effect: Allow
Version: 2012-10-17
# ENI
NIC1:
Type: AWS::EC2::NetworkInterface
Properties:
Description: !Sub 'ENI for EC2 instance: ${Hostname}-${awEnvironment}'
GroupSet:
- sg-050cadbf0e159b0ac
SubnetId: !FindInMap [SubnetMap, !Ref AvailabilityZone, !Ref awEnvironment]
Tags:
- Key: Name
Value: !Sub '${Hostname}-eni'
# EC2 Instance
Instance:
Type: 'AWS::EC2::Instance'
Metadata:
'AWS::CloudFormation::Authentication':
S3AccessCreds:
type: S3
buckets:
- !Ref S3BucketName
roleName: !Ref HostRole
'AWS::CloudFormation::Init':
configSets:
config:
- get-files
- configure-instance
get-files:
files:
'c:\s3-downloads\scripts\Add-WindowsFeature.ps1':
source: https://NotARealBucket.s3.amazonaws.com/scripts/Add-WindowsFeature.ps1
authentication: S3AccessCreds
configure-instance:
commands:
1-set-powershell-execution-policy:
command: >-
powershell.exe -Command "Set-ExecutionPolicy UnRestricted -Force"
waitAfterCompletion: '0'
2-rename-computer:
command: !Join
- ''
- - >-
- powershell.exe -Command "Rename-Computer -Restart -NewName "
- !Ref Hostname
waitAfterCompletion: forever
3-install-windows-components:
command: >-
powershell.exe -Command "c:\s3-downloads\scripts\Add-WindowsFeature.ps1"
waitAfterCompletion: '0'
Properties:
DisableApiTermination: 'false'
AvailabilityZone: !Sub "${AvailabilityZone}"
InstanceInitiatedShutdownBehavior: stop
IamInstanceProfile: !Ref Profile
ImageId: !Ref LatestAmiId
InstanceType: !Sub "${InstanceSize}"
KeyName: !Sub "${KeyPair}"
UserData: !Base64
'Fn::Join':
- ''
- - "\n"
- "cfn-init.exe "
- " --stack "
- "Ref": "AWS::StackId"
- " --resource Instance"
- " --region "
- "Ref": "AWS::Region"
- " --configsets config"
- " -v \n"
- "cfn-signal.exe "
- " ---exit-code 0"
- " --region "
- "Ref": "AWS::Region"
- " --resource Instance"
- " --stack "
- "Ref": "AWS::StackName"
- "\n"
- "\n"
Tags:
- Key: Name
Value: !Sub "${Hostname}"
- Key: awPurpose
Value: !Sub "${awPurpose}"
- Key: awChargeTo
Value: !Sub "${awChargeTo}"
- Key: awRegion
Value: !Sub "${awRegion}"
- Key: awExpiry
Value: !Sub "${awExpiry}"
- Key: awBusinessSegment
Value: !Sub "${awBusinessSegment}"
- Key: awEnvironment
Value: !Sub "${awEnvironment}"
- Key: awApplication
Value: !Sub "${awApplication}"
NetworkInterfaces:
- NetworkInterfaceId: !Ref NIC1
DeviceIndex: 0
Outputs:
InstanceId:
Description: 'InstanceId'
Value: !Ref Instance
Export:
Name: !Sub '${Hostname}-${awEnvironment}-InstanceId'
InstancePrivateIP:
Description: 'InstancePrivateIP'
Value: !GetAtt Instance.PrivateIp
Export:
Name: !Sub '${Hostname}-${awEnvironment}-InstancePrivateIP'
So my
question now is, why doesn’t Azure also support YAML?