It might be useful to copy files into an EC2 instance whilst it's being built. Those can be configuration files for the application, authentication keys or even the whole pre-built application stack.
One way of doing that is to use CloudFormation's Metadata and copy files from an S3 bucket. This post shows the configuration that's needed in order to make this work.
Authenticating an EC2 instance against an S3 bucket
In order to be able to pull the files from the S3 bucket the EC2 instance must have an IamInstanceProfile that allows for access to the bucket. Snippet below shows the necessary resources:
rEC2BaseRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service:
- ec2.amazonaws.com
Action:
- sts:AssumeRole
Path: "/"
rEC2BasePolicy:
Type: "AWS::IAM::Policy"
Properties:
PolicyName: root
PolicyDocument:
Statement:
- Effect: Allow
Action:
- ec2:DescribeTags
- ec2:DescribeVolumes
- ec2:DescribeInstances
Resource: "*"
- Effect: Allow
Action:
- s3:ListBucket
- s3:GetObject
- s3:GetBucketLocation
- s3:ListAllMyBuckets
Resource:
- "arn:aws:s3:::*"
- "arn:aws:s3:::*/*"
Roles:
- Ref: rEC2BaseRole
rEC2BaseProfile:
Type: "AWS::IAM::InstanceProfile"
Properties:
Path: "/"
Roles:
- Ref: rEC2BaseRole
InstanceProfileName: "EC2BaseProfile"
The AWS::IAM::Policy above can be restricted to only one bucket and s3:GetObject only if needs be.
CloudFormation Metadata for the EC2 instance
There are two key sections here: AWS::CloudFormation::Authentication that specifies how CloudFormation is expected to authenticate itself against the S3 bucket and AWS::CloudFormation::Init that specifies what needs to be done. In our case we only want to copy a file.
AWS::CloudFormation::Authentication
This section is responsible for providing credentials:
AWS::CloudFormation::Authentication:
S3AccessCreds:
type: "S3"
roleName:
Ref: rEC2BaseRole
The main thing here is to specify the correct type of resource - S3.
Further details are available here: AWS Docs
AWS::CloudFormation::Init
This section specifies (in our case) where the file should be fetched from, where to put it and what permissions it should have (further details at link below, in this case we only have one config set, with only one file). Example below shows copies a deployment key.
AWS::CloudFormation::Init:
configSets:
init:
- repoKey
repoKey:
files:
"/root/ec2-base.key":
mode: '000400'
owner: root
group: root
source: "https://my-redacted-bucket.s3-ap-southeast-2.amazonaws.com/ec2-base.key"
authentication: "S3AccessCreds"
Further details are available here: AWS Docs.
Instance details
There two important properties for the EC2::Instance:
- IamInstanceProfile
- UserData
The IamInstanceProfile must be a profile that points to the same Role as the one specified in the AWS::CloudFormation::Authentication section. A different Role with the same set of statements will not work, cfn-init
will simply hang forever if the profile is not set up correctly.
The UserData section must contain invocation of the cfn-init
script. That script is by default available on Amazon Linux:
UserData:
Fn::Base64:
Fn::Sub:
|
#!/bin/bash -xe
#pull in key file
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --region ${AWS::Region} --resource rTestInst --configsets init
A few other details:
- UserData must be base64-encoded
- it must contain a shell script
- the
cfn-init
command must reference the resource name, as specified in the CloudFormation template
The final template for the AWS::EC2::Instance looks like this:
rTestInst:
Type: "AWS::EC2::Instance"
Metadata:
AWS::CloudFormation::Authentication:
S3AccessCreds:
type: "S3"
roleName:
Fn::ImportValue:
Fn::Sub: "rEC2BaseWithSTSAssumeRole"
AWS::CloudFormation::Init:
configSets:
init:
- repoKey
repoKey:
files:
"/root/ec2-base.key":
mode: '000400'
owner: root
group: root
source: "https://hangarau-config.s3-ap-southeast-2.amazonaws.com/ec2-base"
authentication: "S3AccessCreds"
Properties:
AvailabilityZone:
Fn::Sub: "${AWS::Region}a"
InstanceType: "t3.nano"
ImageId: "ami-04481c741a0311bbb" #Amazon Linux 2
IamInstanceProfile:
Fn::ImportValue: "rEC2BaseProfile"
InstanceInitiatedShutdownBehavior: "stop"
KeyName: "aws-dev"
SubnetId:
Fn::ImportValue: "rSubnetPublicA"
SecurityGroupIds:
- Fn::GetAtt:
- rTestInstSG
- GroupId
Tags:
- Key: "Name"
Value: "testInstance"
- Key: "Account"
Value:
Ref: AWS::AccountId
UserData:
Fn::Base64:
Fn::Sub:
|
#!/bin/bash -xe
#pull in key file
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --region ${AWS::Region} --resource rTestInst --configsets init
And when the instance is created the file is there:
[root@ip-172-31-1-250 ec2-user]# cd /root/
[root@ip-172-31-1-250 ~]# ls -al
total 24
dr-xr-x--- 5 root root 153 Jun 28 09:59 .
dr-xr-xr-x 18 root root 257 Jun 28 09:59 ..
drwxr-xr-x 4 root root 29 Jun 28 10:00 .ansible
-rw-r--r-- 1 root root 18 Oct 18 2017 .bash_logout
-rw-r--r-- 1 root root 176 Oct 18 2017 .bash_profile
-rw-r--r-- 1 root root 176 Oct 18 2017 .bashrc
drwxr-xr-x 3 root root 17 Jun 28 09:59 .cache
-rw-r--r-- 1 root root 100 Oct 18 2017 .cshrc
-r-------- 1 root root 1831 Jun 28 09:59 ec2-base.key <------------
drwx------ 2 root root 29 Jun 28 09:59 .ssh
-rw-r--r-- 1 root root 129 Oct 18 2017 .tcshrc