Last active
October 2, 2022 20:36
-
-
Save brettswift/6e48a70d808a28614438520682459f0c to your computer and use it in GitHub Desktop.
Revisions
-
brettswift revised this gist
Oct 15, 2019 . 3 changed files with 226 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,115 @@ 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; } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,71 @@ 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} } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,40 @@ 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, }); -
brettswift created this gist
Oct 7, 2019 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,89 @@ 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); This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,73 @@ # 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}