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?