Use Base64-encoding for wait handle inside UserData - amazon-web-services

I have UserData with this syntax:
UserData:
Fn::Base64: !Sub |
<script>
.........
cfn-signal.exe ${MyWaitHandle}
</script>
The problem is that ${MyWaitHandle} needs to be encoded with Base64, and I don't know how to do that. I tried with ${Base64:MyWaitHandle} but I got a validation error.
I'm migrating from JSON to YAML, and the original JSON had "cfn-signal.exe ", {"Fn::Base64" : {"Ref" : "MyWaitHandle"}}.
How do I recreate that inside the !Sub | syntax?

Related

How do I use a CloudFormation function in UserData Script

How do I get public IP of EC2 instance and write to a text file in UserData. Tried the following, but as expected it wrote the text literally, rather than resolving it.
UserData:
Fn::Base64: !Sub |
#!/bin/bash -xe
echo "Public IP: " !GetAtt Bastion.PublicIp > /home/ec2-user/readme.txt
Thanks in advance! (PS : It has to be yaml)

How to have Fn::Join nested inside of Fn::If?

I have a bash script that I want to run on my instances but I only want the second portion of the script to run if a value is true. Also, I would prefer to not have the if statement take place in the script.
Parameters:
#TestParameter = TRUE
Resource:
UserData:
Fn::Sub: |
echo "This is a test example"
#If TestParameter is true:
echo "Only is parameter is true"
In my opinion, it's better to construct Sub the parameter into the script:
UserData:
Fn::Sub: |
echo "This is a test example"
if ${TestParameter}; then
echo "Only is parameter is true"
fi
But since you don't want to have the if inside the script, you'll have to construct the body of the script in the Cloudformation template. Building strings in CFN is always messy. Try something like this:
UserData:
Fn::Join: ["\n", ["echo 'This is a test example'",
['Fn::If': [!Equals [!Ref TestParameter, "true"], "echo 'Parameter is true'", ""]]]]

Dynamic References to Specify Secret Manager Values in AWS Cloudformation

Is there anyway we can pass dynamic references to Secret Manager to AWS Launch Config User Data?
Here is the code snippet I tried:
"SampleLaunchConfig": {
"Type": "AWS::AutoScaling::LaunchConfiguration",
"Properties": {
"ImageId": {
"Fn::FindInMap": [
"AWSRegionArch2AMI",
{
"Ref": "AWS::Region"
},
"AMI"
]
},
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"",
[
"#!/bin/bash -xe\n",
"yum update -y\n",
"useradd -p <<pwd>>{{resolve:secretsmanager:Credentials:SecretString:userName}}\n",
"\n"
]
]
}
}
}
}
Seems error in getting the useradd: invalid user name '{{resolve:secretsmanager:Credentials:SecretString:userName}}'
How can I pass Secret Manager secret value to cloudformation user data ?
It seems that {{resolve:...}} dynamic references are only expanded in certain contexts within a template.
There is no precise information in the AWS docs about exactly where in a template you can use these references. The current wording with regard to {{resolve:secretsmanager:...}} says:
"The secretsmanager dynamic reference can be used in all resource properties"
However this is contradicted by your example, and I've also observed dynamic references failing to resolve inside of CloudFormation::Init data.
I have an active Support case open with AWS about this, they have agreed that the behaviour of dynamic references is inadequately documented. I'll update this answer as I learn more.
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/dynamic-references.html#dynamic-references-secretsmanager
I am not sure why this is not expanded correctly for you. However, you probably do not want CFN to expand your secret in the user data because the password would be embedded in the base64 encoded user data script which is visible in the EC2 console.
Instead you should take advantage of the fact that you have a script that executes on the host and call secrets manager at script execution time (warning untested):
"SampleLaunchConfig": {
"Type": "AWS::AutoScaling::LaunchConfiguration",
"Properties": {
"ImageId": {
"Fn::FindInMap": [
"AWSRegionArch2AMI",
{
"Ref": "AWS::Region"
},
"AMI"
]
},
"UserData": {
"Fn::Base64": {
"Fn::Join": [
"",
[
"#!/bin/bash -xe\n",
"yum update -y\n",
"yum install -y jq\n",
!Sub "useradd -p `aws --region ${AWS::Region} secretsmanager get-secret-value --secret-id Credentials --query SecretString --output text | jq -r .passwordKey` `aws --region ${AWS::Region} secretsmanager get-secret-value --secret-id Credentials --query SecretString --output text | jq -r .userName`\n",
"\n"
]
]
}
}
}
}
This is not ideal since it expands the password on the command line. It might be made more secure by putting the password in a file first and reading it from there and then shredding the file.
I can confirm that #JoeB's "warning untested" answer works, with the caveat that the machine in question must have permission to read the secret. You'll need something like
MyInstancePolicy:
Type: AWS::IAM::Policy
Properties:
PolicyName: MyPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
-
Effect: Allow
Action:
- secretsmanager:GetSecretValue
Resource: !Join
- ''
- - !Sub "arn:aws:secretsmanager:${AWS::Region}:"
- !Sub "${AWS::AccountId}:secret:Credentials-??????"
Note a couple of things:
Unlike S3 buckets, you can't do arn:aws:secretsmanager:::secret.... If you don't want to explicitly declare the region and account, you need to use a wildcard. Buried at the bottom of Using Identity-based Policies (IAM Policies) for Secrets Manager
If you don't care about the region or account that owns a secret,
you must specify a wildcard character * (not an empty field) for the
region and account ID number fields of the ARN.
Perhaps less important, and less likely to cause unexpected failures, but still worth note:
Using '??????' as a wildcard to match the 6 random characters that
are assigned by Secrets Manager avoids a problem that occurs if you
use the '*' wildcard instead. If you use the syntax
"another_secret_name-*", it matches not just the intended secret with
the 6 random characters, but it also matches
"another_secret_name-a1b2c3". Using the '??????' syntax
enables you to securely grant permissions to a secret that doesn't yet
exist.
Variant on #JoeB's answer:
Resources:
SampleLaunchConfig:
Type: AWS::AutoScaling::LaunchConfiguration
Properties:
ImageId: !FindInMap [ AWSRegionArch2AMI, !Ref: 'AWS::Region', AMI ]
UserData:
Fn::Base64: !Sub |
#!/bin/bash -xe
exec > >(tee /var/log/user-data.log | logger -t user-data) 2>&1
yum update -y
yum install -y jq
username=$(aws secretsmanager get-secret-value --secret-id Credentials \
--query SecretString \
--region ${AWS::Region} --output text | jq -r .userName)
password=$(aws secretsmanager get-secret-value --secret-id Credentials \
--query SecretString \
--region ${AWS::Region} --output text | jq -r .passwordKey)
useradd -p "$password" $username
UserData in JSON is painful to watch these days.
I've also added a technique to split out the UserData logic to it's own log file, as otherwise it goes in cloud-init.log, which is also painful to read.
The proper way to do it is to call secret manager to get your data, here is how I succeded doing it :
SftpCredsUserPasswordSecret:
Type: 'AWS::SecretsManager::Secret'
Properties:
Name: 'sftp-creds-user-password-secret'
Description: DB Credentials
GenerateSecretString:
SecretStringTemplate: '{"username":"sftpuser"}'
GenerateStringKey: "password"
PasswordLength: 30
ExcludePunctuation: true
Ec2SftpRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument: # Tells that Ec2 can assume this role
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Principal:
Service:
- ec2.amazonaws.com
Policies: # Tells that you can call for the secret value
- PolicyName: "root"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action: "secretsmanager:GetSecretValue"
Resource: !Ref SftpCredsUserPasswordSecret
RoleName: 'role-ec2-sftp'
Ec2SftpIamInstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
InstanceProfileName: 'instance-profile-ec2-sftp'
Roles:
- !Ref Ec2SftpRole
Ec2Sftp:
Type: AWS::EC2::Instance
Properties:
ImageId: "ami-06ce3edf0cff21f07"
InstanceType: t2.micro
SecurityGroupIds:
- !ImportValue SgBridgeId
- !ImportValue SgSftpId
SubnetId: !ImportValue PublicSubnetAZbId
KeyName: !Ref KeyName
UserData:
Fn::Base64: !Sub
- |
#!/bin/bash
# Get Variables
secret=`aws --region ${AWS::Region} secretsmanager get-secret-value --secret-id ${secretRef} --query SecretString --output text`
sftpuser=`echo "$secret" | sed -n 's/.*"username":["]*\([^(",})]*\)[",}].*/\1/p'`
sftppassword=`echo "$secret" | sed -n 's/.*"password":["]*\([^(",})]*\)[",}].*/\1/p'`
# Create Sftp User
adduser $sftpuser
echo "$sftpuser:$sftppassword" | chpasswd
# Configure sftp connection
echo "" >> /etc/ssh/sshd_config
echo "Match User $sftpuser" >> /etc/ssh/sshd_config
echo " PasswordAuthentication yes" >> /etc/ssh/sshd_config
echo " ForceCommand /usr/libexec/openssh/sftp-server" >> /etc/ssh/sshd_config
# Restart the service
systemctl restart sshd
-
secretRef: !Ref SftpCredsUserPasswordSecret
IamInstanceProfile: !Ref Ec2SftpIamInstanceProfile
Tags:
- Key: Name
Value: 'ec2-sftp'
I have a cfn template similar to below working
Parameters:
ASecret:
Type: String
Default: '{{resolve:secretsmanager:ASecret}}'
-
-
-
UserData:
Fn::Base64: !Sub |
#!/bin/bash -xe
echo ${ASecret}
AWS CloudFormation enhances the existing dynamic referencing of AWS Systems Manager Parameter Store parameters in CloudFormation templates. You can now reference the latest Systems Manager parameter values in CloudFormation templates without specifying parameter versions.
See more
https://aws.amazon.com/about-aws/whats-new/2021/04/now-reference-latest-aws-systems-manager-parameter-values-in-aws-cloudformation-templates-without-specifying-parameter-versions/

How to pass script to UserData field in EC2 creation on AWS Lambda?

I'm trying to pass a script in Userdata field of a new EC2 instance created by an AWS Lambda (using AWS SDK for Javascript, Node.js 6.10):
...
var paramsEC2 = {
ImageId: 'ami-28c90151',
InstanceType: 't1.micro',
KeyName: 'myawesomekwy',
MinCount: 1,
MaxCount: 1,
SecurityGroups: [groupname],
UserData:'#!/bin/sh \n echo "Hello Lambda"'
};
// Create the instance
ec2.runInstances(paramsEC2, function(err, data) {
if (err) {
console.log("Could not create instance", err);
return;
}
var instanceId = data.Instances[0].InstanceId;
console.log("Created instance", instanceId);
// Add tags to the instance
params = {Resources: [instanceId], Tags: [
{
Key: 'Name',
Value: 'taggggg'
}
]};
ec2.createTags(params, function(err) {
console.log("Tagging instance", err ? "failure" : "success");
});
});
...
I tried several things like:
- create a string and pass the string to the UserData - not working
- create a string and encode it to base64 and pass the string to the UserData - not working
- paste base64 encoded string - not working
Could you help me understanding how to pass a script in the UserData? The AWS SDK documentation is a bit lacking.
Is it also possible to pass a script put in an S3 bucket to the UserData?
Firstly, base64 encoding is required in your example. Although the docs state that this is done for you automatically, I always need it in my lambda functions creating ec2 instances with user data. Secondly, as of ES6, multi-line strings can make your life easier as long as you add scripts within your lambda function.
So try the following:
var userData= `#!/bin/bash
echo "Hello World"
touch /tmp/hello.txt
`
var userDataEncoded = new Buffer(userData).toString('base64');
var paramsEC2 = {
ImageId: 'ami-28c90151',
InstanceType: 't1.micro',
KeyName: 'AWSKey3',
MinCount: 1,
MaxCount: 1,
SecurityGroups: [groupname],
UserData: userDataEncoded
};
// Create the instance
// ...

Fn::Sub with a Mapping in a literal block to specify the user data script

Ok so reading the documentation on the The intrinsic function Fn::Sub. I can use a literal block to specify the user data script.
UserData:
"Fn::Base64":
!Sub |
#!/bin/bash -xe
yum update -y aws-cfn-bootstrap
/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchConfig --configsets wordpress_install --region ${AWS::Region}
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region}
and I can uses a mapping to substitute the ${Domain} variable with the resulting value from the Ref function.
Name: !Sub
- www.${Domain}
- { Domain: !Ref RootDomainName }
But what if I need to use a mapping substitute inside a literal block? Like for example:
"Fn::Base64": !Sub |
<powershell>
Write-host "My Domain is www.${Domain},{ Domain: !Ref RootDomainName }"
</powershell>
This example does not work, and I haven't been unable to find a method that does. Any ideas? The first example makes userdata scripts much easier to write and looks cleaner, but without being able to to use !Ref or !Findinmap it's usefulness is reduced dramatically.
Anyone got any ideas?
Since I arrived to this page through Google, and then found the solution through a different wording here(literally):
How to use !FindInMap in !Sub | userdata section
I'll just add it to save some frustrated searching to others.
Essentially, you have to write your example using the 2nd syntax but a bit more verbosely:
Fn::Base64:
Fn::Sub:
- |+
<powershell>
Write-host "My Domain is www.${Domain}"
</powershell>
- Domain:
Fn::Ref: RootDomainName
You may be able to shorten it a bit, but as the original poster said, mind your commas, quoting and usage of short forms.
P.S.: If the first solution has already served your purpose, you should mark it thus.
In that case you would write it simply as ${RootDomainName}. Local resouces in the same stack can just be mapped by using their resource name.