Lazy but Cautious: An all-inclusive AWS EC2 instance Cloudformation Template — Part 2
Develop fast, stay cautious!
This is Part 2, of a two part blog on creating the security group, IAM role, and EC2 instance. You can read about Part 1 here.
Focusing on the actual AWS resources:
1. Security Group
When creating a security group in AWS, using Cloudformation, choose to parameterize the following variables:
1. VPC ID
Unless you’re creating a VPC inside your Cloudformation template, pass the VPC ID as a parameter, rather than hard-coding it. This way you can ensure reusability!
2. Security Group Name
Although there’s no harm in hard-coding this, but parameterization helps when you have to meet certain naming standards for your use case.
3. IP Address(es) to allow
Here, the same IP address range is allowed access to multiple ports. Your use case may involve having separate IP addresses.
- Each different IP address will have to a new String type parameter to be added to the Cloudformation template.
- Do not add IP address ranges if you aren’t a 100% sure they’re trusted.
2. IAM Role, Policies, Instance Profile
When creating an IAM role specific to EC2, make sure you add ec2.amazonaws.com service under the Principal section of the AssumeRolePolicyDocument section.
This ensures that the role is assumable the EC2 service and actions can be performed on its behalf.
When you wish to create a custom, managed IAM policy, ensure you give MINIMAL IAM permissions, and restrict those permissions to specific AWS resources. It’s just IAM 101.
If you wish to attach this custom policy to the IAM role created previously, you need to ensure you put a clause in place. The DependOn condition added to this block ensures that this policy only gets created after your IAM role is created.
Notice the particular usage of the intrinsic function Fn::Sub here (!Sub). It helps substitute your S3 bucket name in the S3 ARN in your Resource section.
The idea is again, to restrict the S3 permissions to the S3 bucket you specify only!
An IAM EC2 Instance Profile has to be created separately and attached to your IAM role, when using Cloudformation. The DependsOn condition is put in to ensure that this resource doesn’t get created before the IAM role does.
You may choose to parameterize the IAM role/policy/instance profile names to meet your organization’s naming conventions as well. You might want to look into Fn::Sub or Fn::Join for that.
3. EC2 Instance
To reiterate, this Cloudformation template:
1. Creates an EC2 instance, using the latest Amazon Linux 2 AMI ID
2. Copies a Python3 file stored in a S3 bucket and runs it, as a part of its user data.
All of this would be done once automatically, and before you SSH into your instance!
But how does Cloudformation know how to get the latest Amazon Linux 2 AMI ID?
Notice the ImageId parameter’s Type and Default value.
Amazon stores the latest values inside Systems Manager Parameter Store, and the Type tells Cloudformation to resolve that value into a string type.
Now that you have the pre-requisite resources created through Cloudformation in your template, time to create the EC2 instance!
What are the individual fields though? How do I specify them?
Having tags to specify the instance “Name” key, is considered sensible so that you recognize your instance easily. Other tags can be added as per your organization’s tagging strategy, as needed.
2. Instance Type
Keeping the instance type parameterized creates reusability of code, and allows greater flexibility in terms of altering your instance size, if needed.
3. Instance Profile
Using the intrinsic !Ref function in Cloudformation, you can let Cloudformation know that you wish to reference the IAM Instance profile you just created.
4. Availability Zone
Keep this parameterized too. Allows you greater control over what AZ to choose when deploying your EC2 instance.
5. Subnet ID
When you want to deploy an EC2 instance, you must specify a subnet ID where this instance must reside. Needless to say, the Subnet ID should be in the same AZ you choose.
6. Security Group IDs
Since you are referencing the security group you created in this Cloudformation template, use the !Ref function (Fn::Ref in JSON). This particular field takes a list of strings as values.
- If you wish to attach multiple security groups to a particular instance, you will have to individually pass those as parameters to the Cloudformation template and then use !Ref in this field for EACH of them. The instance will have access to the collective of the ingress/egress rules you specify.
- Ideally, have a single security group catering to your use case. It’s easier to manage and troubleshoot, in case access related issues arise.
7. Key Name
This block particularly assumes you will be using an existing EC2 keypair in the region you are deploying this Cloudformation template.
8. Image ID
This is the AMI ID to be used by your EC2 instance. Here, the use case involves getting the latest Amazon Linx 2 AMI ID, but you may pass your own AMI ID, depending on your use case.
In case you are interested in creating mappings between AMI IDs and region, read here.
9. Block Device Mappings
The block device mapping entries that defines the block devices to attach to the instance at launch. By default, the block devices specified in the block device mapping for the AMI are used. You can override the AMI block device mapping using the instance block device mapping. To know more about block device mappings, read here.
10. User Data
User data helps run scripts on launch of an EC2 instance. If you have a bunch of automated tasks, create a script accordingly, and store it in a temporary S3 bucket. Then copy it to your instance and run it!
- User data is always base64 encoded (Hence the Fn::Base64)
- It should always begin with the #!/bin/bash shebang.
- “-xe” is added to #!/bin/bash to ensure two things:
a. x: writes each command to stderror. It’s useful for debugging.
b. e: exit immediately if any untested command fails.
- !Sub is outright the BEST way to substitute parameter values in to your user data.
Additional tip: Create your bash script separately, check its syntax/functionality and then copy-paste it to your User Data field.
For the whole code, check this link out!
Keep checking out the next parts in this series! Thanks for reading!