Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save preetampvp/33a641b324ea68096eb1726266034694 to your computer and use it in GitHub Desktop.
Save preetampvp/33a641b324ea68096eb1726266034694 to your computer and use it in GitHub Desktop.
CDK 1.10.1 - ASG Cloudformation Init example
import autoscaling = require("@aws-cdk/aws-autoscaling")
import scriptAssets = require("./CfnInitScriptAsset")
import iam = require('@aws-cdk/aws-iam')
import cdk = require('@aws-cdk/core')
/**
* Helpful context into what was built.
* Use these to get logical ID's when constructing your userdata.
*/
export interface CfnInitArtifacts {
cfnAsg: autoscaling.CfnAutoScalingGroup,
cfnLaunchConfig: autoscaling.CfnLaunchConfiguration,
metadata: {}
}
/**
* Aids in building the awkward Cfn Init Metadata.
*
* Injects the metadata onto the provided ASG.
*
* Returns a contect object that contains lower level Cfn resources you'll need to create your userdata.
* ie: CfnLaunchConfig and it's ID for cfn-init, and a CfnAutoscalingGroup and it's ID for cfn-signal.
*/
export class CfnInitMetadataBuilder {
asg: autoscaling.AutoScalingGroup
scripts: scriptAssets.CfnInitScriptAsset[]
configSetName: any;
constructor(asg: autoscaling.AutoScalingGroup, configSetName?: string){
this.asg = asg;
this.configSetName = configSetName || 'main'; // TODO: test with 'default' ?
this.scripts = []
}
public withScript(script: scriptAssets.CfnInitScriptAsset): CfnInitMetadataBuilder{
this.scripts.push(script)
this.asg.addToRolePolicy(new iam.PolicyStatement({
actions: ['s3:*'],
resources: [
`${script.bucket.bucketArn}/${script.s3ObjectKey}`
]
}))
return this
}
public build(): CfnInitArtifacts{
const cfnLaunchConfig = this.asg.node.findAll().find((item: cdk.IConstruct) =>
item.node.id === 'LaunchConfig'
) as autoscaling.CfnLaunchConfiguration
const cfnAustoScalingGroup = this.asg.node.findAll().find((item: cdk.IConstruct) =>
item.node.id === 'ASG'
) as autoscaling.CfnAutoScalingGroup
const metadata = this.buildMetadata();
cfnLaunchConfig.addOverride("Metadata", metadata)
return {
cfnAsg: cfnAustoScalingGroup,
cfnLaunchConfig: cfnLaunchConfig,
metadata: metadata,
}as CfnInitArtifacts
}
// // Types here can be an L1 files or commands object when types are available.
private arrayReducer(obj: { [x: string]: any; }, item: { [x: string]: any; }){
Object.keys(obj).push(Object.keys(item)[0])
obj[Object.keys(item)[0]] = Object.values(item)[0]
return obj;
}
private arrayToObject(theArray: { [x: string]: any; }): { [x: string]: any; }{
let theMap: { [x: string]: any; }={}
theArray.forEach((x: { [s: string]: any; } | ArrayLike<unknown>)=> {
Object.keys(theMap).push(Object.keys(x)[0])
theMap[Object.keys(x)[0]] = Object.values(x)[0]
})
return theMap
}
/**
* All scripts should be added. Build metadata json object with them.
*/
private buildMetadata(){
const metadata = {
"AWS::CloudFormation::Authentication": {
"rolebased": {
"type": "S3",
"buckets": this.scripts.map((script) => script.bucket.bucketName),
"roleName": this.asg.role.roleName
}
},
"AWS::CloudFormation::Init": {
"configSets": {
[this.configSetName]: ["configset1"]
},
"configset1": {
"files": this.scripts.map(script => script.getFileForMetadata())
.reduce(this.arrayReducer,{}),
"commands": this.arrayToObject(
this.scripts.filter(script => script.isExecutable)
.map(script => script.getCommandForMetadata())
),
}
}
}
return metadata;
}
}
import cdk = require('@aws-cdk/core');
import s3Assets = require('@aws-cdk/aws-s3-assets')
export interface CfnInitScriptAssetProps extends s3Assets.AssetProps{
friendlyName: string,
destinationFileName: string,
env?: {
[key: string]: string;
}
/**
* defaulted to /tmp/scripts
* Must start with a slash and end without a slash
* */
destinationPath?: string,
shouldExecute?: boolean,
/** default: 000755 */
mode?: string
}
export class CfnInitScriptAsset extends s3Assets.Asset{
private destinationPath: string
private destinationFileName: string
private env: {
[key: string]: string;
}
private friendlyName: string;
private mode: string;
public readonly isExecutable: boolean;
private destinationFullPath: string;
constructor(scope: cdk.Construct, id: string, props: CfnInitScriptAssetProps){
super(scope, id, props)
this.destinationPath = props.destinationPath || '/tmp/scripts'
this.destinationFileName = props.destinationFileName
this.env = props.env || {}
this.friendlyName = props.friendlyName
this.mode = props.mode || '000755'
this.destinationFullPath = `${this.destinationPath}/${this.destinationFileName}`
if(props.shouldExecute == false){
this.isExecutable = false
}else{ //if undefined or true
this.isExecutable = true
}
}
getCommandForMetadata() {
// TODO: this could be replaced with a pseudo L1 command object if one appears.
const commandInfo = {
command: this.destinationFullPath,
cwd: this.destinationPath,
env: this.env,
}
if(!this.isExecutable) return null
return {[this.friendlyName]: commandInfo}
}
getFileForMetadata() {
// TODO: support files that are not to be executed. They'll need different permissions and no 'command' section.
const fileInfo = {
source: this.s3Url,
mode: this.mode,
owner: "root",
group: "root",
}
return {[this.destinationFullPath]: fileInfo}
}
}
const fileWebServer = new initMetadata.CfnInitScriptAsset(this, 'webserverScript', {
friendlyName: 'webserver',
destinationFileName: "webserver.sh",
path: path.join(__dirname, '../scripts/webserver.sh'),
env: {
"SERVICE_VERSION": serviceVersion
}
})
const webDisplayLoad = new initMetadata.CfnInitScriptAsset(this, 'showLoad', {
shouldExecute: false,
friendlyName: 'webContentCPUData',
destinationFileName: 'webContentCPUData.sh',
path: path.join(__dirname, '../scripts/webContentCPUData.sh'),
})
const CONFIG_SET_NAME = 'main'
const builder = new initMetadata.CfnInitMetadataBuilder(asg, CONFIG_SET_NAME)
const metadataContext = builder
.withScript(fileWebServer)
.withScript(webDisplayLoad)
.build()
const importedUserData = fs.readFileSync('userdata.sh', 'utf-8');
const importedUserDataContentsReplaced = cdk.Fn.sub(importedUserData, {
scriptBucketName: scriptBucket.bucketName,
serviceVersion: serviceVersion,
logBucketName: scriptBucket.bucketName,
s3LogPrefix: s3LogPrefix,
devMode: devMode,
s3ArtifactPath: s3ArtifactPath,
configFileExtension: dnsPrefix,
asgLogicalId: metadataContext.cfnAsg.logicalId, //asg.node.uniqueId ? different? same?
launchConfigId: metadataContext.cfnLaunchConfig.logicalId,
stackName: this.stackName,
awsRegion: cdk.Aws.REGION,
configSet: CONFIG_SET_NAME,
});
const ASG_NAME = "TestAsg"
const asg = new autoscaling.AutoScalingGroup(this, ASG_NAME, {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MICRO),
keyName: keypairSsm.stringValue, // cdk ssm.StringParameter, optionally replace with the keypair name as a string
vpc: vpc, //defined outside this gist
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
}),
cooldown: cdk.Duration.seconds(300),
resourceSignalTimeout: cdk.Duration.seconds(300),
updateType: autoscaling.UpdateType.REPLACING_UPDATE,
}as autoscaling.AutoScalingGroupProps)
const asgResources = asg.node.findAll()
const cfnLaunchConfig = asgResources.find((item: IConstruct) => item.node.id === 'LaunchConfig') as autoscaling.CfnLaunchConfiguration
const cfnAsg = this.node.findAll().find((item: IConstruct) =>
item.node.id === 'ASG'
) as autoscaling.CfnAutoScalingGroup
const fileWebServer = new s3Assets.Asset(this, 'webserverScript', {
path: path.join(__dirname, '../scripts/webserver.sh'), //install nginx, etc.
})
asg.addToRolePolicy(new iam.PolicyStatement({
actions: ['s3:*'],
resources: [
`${fileWebServer.bucket.bucketArn}/${fileWebServer.s3ObjectKey}`
]
}))
const destScriptsPath = '/tmp/scripts'
const destScriptFullPath = `${destScriptsPath}/webserver.sh`
const commandFriendlyName = 'install_and_run_web'
cfnLaunchConfig.addOverride("Metadata", {
"AWS::CloudFormation::Authentication": {
"rolebased" : {
"type": "S3",
"buckets": [
fileWebServer.bucket.bucketName
],
"roleName": asg.role.roleName
}
},
"AWS::CloudFormation::Init" : {
"configSets" : {
"main" : [ "config1" ]
},
"config1" : {
"files": {
[destScriptFullPath]: {
"source": fileWebServer.s3Url,
"mode": "000755",
"owner": "root",
"group": "root",
},
},
"commands" : {
[commandFriendlyName]: {
"command": destScriptFullPath,
"cwd": destScriptsPath,
"env": {
"SERVICE_VERSION" : serviceVersion,
}
}
}
}}}
)
const importedUserData = shawConstructs.readUserData("userdata.sh");
const importedUserDataContentsReplaced = cdk.Fn.sub(importedUserData, {
scriptBucketName: scriptBucket.bucketName,
serviceVersion: serviceVersion,
logBucketName: scriptBucket.bucketName,
s3LogPrefix: s3LogPrefix,
devMode: devMode,
s3ArtifactPath: s3ArtifactPath,
configFileExtension: dnsPrefix,
asgLogicalId: cfnAsg.logicalId,
launchConfigId: cfnLaunchConfig.logicalId,
stackName: this.stackName,
awsRegion: cdk.Aws.REGION,
configSet: 'main',
});
asg.connections.allowFromAnyIpv4(ec2.Port.tcp(22));
asg.addUserData(importedUserDataContentsReplaced);
# Both required for the trap.
set -euo pipefail
set -o errtrace
# if anything fails below, abort abort! Without this, ec2's just time out ..slowly...
trap 'cfn-signal-error $? $LINENO' EXIT
USERDATALOG=/var/log/user-data-server-setup-log.txt
INSTANCEID=$(curl -sSL http://169.254.169.254/latest/meta-data/instance-id)
WORKING_DIR=/tmp/installer
echo "scriptBucketName: ${scriptBucketName}"
echo "serviceVersion: ${serviceVersion}"
echo "logBucketName: ${logBucketName}"
echo "s3LogPrefix: ${s3LogPrefix}"
echo "devMode: ${devMode}"
echo "s3ArtifactPath: ${s3ArtifactPath}"
echo "config file used: ${configFileExtension}"
echo "asgLogicalId: ${asgLogicalId}"
echo "stackName: ${stackName}"
echo "awsRegion: ${awsRegion}"
echo "launchConfigId: ${launchConfigId}"
echo "configSet: ${configSet}"
# Extract local varables from ones injected.
# IMPORTANT: when using these, ensure it uses the ${!VAR) syntax. ie: exclamation mark prevents declaring a template variable. The template will strip it.
# TODO: I think we can make this more generic by setting env variables, as a cdk issue suggests, putting env vars into profile.d, and then cfn-init scripts can each use them.
SERVICE_VERSION=${serviceVersion}
SCRIPT_BUCKET_PATH=${scriptBucketName}
S3_ARTIFACT_PATH=${s3ArtifactPath}
S3_LOG_PREFIX=${s3LogPrefix}
DEV_MODE=${devMode}
LOG_BUCKET=${logBucketName}
CONFIG_NAME=${configFileExtension}
ASG_LOGICALID=${asgLogicalId}
STACK_NAME=${stackName}
AWS_REGION=${awsRegion}
LAUNCH_CONFIG_ID=${launchConfigId}
CONFIG_SET=${configSet}
BASE_LOG_UPLOAD_PATH=s3://${!LOG_BUCKET}/${!S3_LOG_PREFIX}/${!INSTANCEID}/
cfn-signal-error() {
echo "Error code $1 happened at line number $2"
echo "Signaling error code using cfn-signal"
/opt/aws/bin/cfn-signal -e 1 --stack ${!STACK_NAME} --resource ${!ASG_LOGICALID} --region ${!AWS_REGION} --reason='failed cfn-init scripts.. more info tbd...'
aws s3 cp ${!USERDATALOG} ${!BASE_LOG_UPLOAD_PATH}
aws s3 cp /var/log/cfn-* ${!BASE_LOG_UPLOAD_PATH}
exit 1
}
cfn-signal-success() {
echo "Signaling success code using cfn-signal"
/opt/aws/bin/cfn-signal -e 0 --stack ${!STACK_NAME} --resource ${!ASG_LOGICALID} --region ${!AWS_REGION}
trap - EXIT
set +e
aws s3 cp ${!USERDATALOG} ${!BASE_LOG_UPLOAD_PATH}
aws s3 cp /var/log/cfn-* ${!BASE_LOG_UPLOAD_PATH}
exit 1
}
# Begin!
# Use a code block to scope log redirection
{
set -x
echo "Instance ID: ${!INSTANCEID}"
/opt/aws/bin/cfn-init -v -c ${!CONFIG_SET} --stack ${!STACK_NAME} --resource ${!LAUNCH_CONFIG_ID} --region ${!AWS_REGION}
echo "Exit code from cfn-init was: $?"
cfn-signal-success
} >> ${!USERDATALOG} 2>>${!USERDATALOG}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment