diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 4dfb06267c5a7..7b40984d8425c 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -249,8 +249,8 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer * Add command to the startup script of fleet instances. * The command must be in the scripting language supported by the fleet's OS (i.e. Linux/Windows). */ - public addUserData(script: string) { - this.userDataLines.push(script); + public addUserData(...scriptLines: string[]) { + scriptLines.forEach(scriptLine => this.userDataLines.push(scriptLine)); } public autoScalingGroupName() { diff --git a/packages/@aws-cdk/aws-codedeploy/README.md b/packages/@aws-cdk/aws-codedeploy/README.md index e58b1b5d77c4b..601829edab23b 100644 --- a/packages/@aws-cdk/aws-codedeploy/README.md +++ b/packages/@aws-cdk/aws-codedeploy/README.md @@ -28,6 +28,10 @@ To create a new CodeDeploy Deployment Group that deploys to EC2/on-premise insta const deploymentGroup = new codedeploy.ServerDeploymentGroup(this, 'CodeDeployDeploymentGroup', { application, deploymentGroupName: 'MyDeploymentGroup', + autoScalingGroups: [asg1, asg2], + // adds User Data that installs the CodeDeploy agent on your auto-scaling groups hosts + // default: true + installAgent: true, }); ``` diff --git a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts index dc2a48b1f6ddb..9d4a57f705c45 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/deployment-group.ts @@ -1,3 +1,6 @@ +import autoscaling = require("@aws-cdk/aws-autoscaling"); +import ec2 = require("@aws-cdk/aws-ec2"); +import s3 = require("@aws-cdk/aws-s3"); import cdk = require("@aws-cdk/cdk"); import iam = require("../../aws-iam/lib/role"); import { ServerApplication, ServerApplicationRef } from "./application"; @@ -56,9 +59,11 @@ export abstract class ServerDeploymentGroupRef extends cdk.Construct { } public abstract readonly application: ServerApplicationRef; + public abstract readonly role?: iam.Role; public abstract readonly deploymentGroupName: string; public abstract readonly deploymentGroupArn: string; public readonly deploymentConfig: IServerDeploymentConfig; + public abstract readonly autoScalingGroups?: autoscaling.AutoScalingGroup[]; constructor(parent: cdk.Construct, id: string, deploymentConfig?: IServerDeploymentConfig) { super(parent, id); @@ -71,14 +76,17 @@ export abstract class ServerDeploymentGroupRef extends cdk.Construct { deploymentGroupName: new cdk.Output(this, 'DeploymentGroupName', { value: this.deploymentGroupName }).makeImportValue().toString(), + deploymentConfig: this.deploymentConfig, }; } } class ImportedServerDeploymentGroupRef extends ServerDeploymentGroupRef { public readonly application: ServerApplicationRef; + public readonly role?: iam.Role = undefined; public readonly deploymentGroupName: string; public readonly deploymentGroupArn: string; + public readonly autoScalingGroups?: autoscaling.AutoScalingGroup[] = undefined; constructor(parent: cdk.Construct, id: string, props: ServerDeploymentGroupRefProps) { super(parent, id, props.deploymentConfig); @@ -119,6 +127,22 @@ export interface ServerDeploymentGroupProps { * @default ServerDeploymentConfig#OneAtATime */ deploymentConfig?: IServerDeploymentConfig; + + /** + * The auto-scaling groups belonging to this Deployment Group. + * + * @default [] + */ + autoScalingGroups?: autoscaling.AutoScalingGroup[]; + + /** + * If you've provided any auto-scaling groups with the {@link #autoScalingGroups} property, + * you can set this property to add User Data that installs the CodeDeploy agent on the instances. + * + * @default true + * @see https://docs.aws.amazon.com/codedeploy/latest/userguide/codedeploy-agent-operations-install.html + */ + installAgent?: boolean; } /** @@ -126,12 +150,16 @@ export interface ServerDeploymentGroupProps { */ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { public readonly application: ServerApplicationRef; - public readonly role: iam.Role; + public readonly role?: iam.Role; public readonly deploymentGroupArn: string; public readonly deploymentGroupName: string; + private readonly _autoScalingGroups: autoscaling.AutoScalingGroup[]; + private readonly installAgent: boolean; + private readonly codeDeployBucket: s3.BucketRef; + constructor(parent: cdk.Construct, id: string, props: ServerDeploymentGroupProps = {}) { - super(parent, id, props && props.deploymentConfig); + super(parent, id, props.deploymentConfig); this.application = props.application || new ServerApplication(this, 'Application'); @@ -140,18 +168,82 @@ export class ServerDeploymentGroup extends ServerDeploymentGroupRef { managedPolicyArns: ['arn:aws:iam::aws:policy/service-role/AWSCodeDeployRole'], }); + this._autoScalingGroups = props.autoScalingGroups || []; + this.installAgent = props.installAgent === undefined ? true : props.installAgent; + const region = (new cdk.AwsRegion()).toString(); + this.codeDeployBucket = s3.BucketRef.import(this, 'CodeDeployBucket', { + bucketName: `aws-codedeploy-${region}`, + }); + for (const asg of this._autoScalingGroups) { + this.addCodeDeployAgentInstallUserData(asg); + } + const resource = new cloudformation.DeploymentGroupResource(this, 'Resource', { applicationName: this.application.applicationName, deploymentGroupName: props.deploymentGroupName, serviceRoleArn: this.role.roleArn, deploymentConfigName: props.deploymentConfig && props.deploymentConfig.deploymentConfigName, + autoScalingGroups: new cdk.Token(() => + this._autoScalingGroups.length === 0 + ? undefined + : this._autoScalingGroups.map(asg => asg.autoScalingGroupName())), }); this.deploymentGroupName = resource.deploymentGroupName; this.deploymentGroupArn = deploymentGroupName2Arn(this.application.applicationName, this.deploymentGroupName); } + + public addAutoScalingGroup(asg: autoscaling.AutoScalingGroup): void { + this._autoScalingGroups.push(asg); + this.addCodeDeployAgentInstallUserData(asg); + } + + public get autoScalingGroups(): autoscaling.AutoScalingGroup[] | undefined { + return this._autoScalingGroups.slice(); + } + + private addCodeDeployAgentInstallUserData(asg: autoscaling.AutoScalingGroup): void { + if (!this.installAgent) { + return; + } + + this.codeDeployBucket.grantRead(asg.role, 'latest/*'); + + const region = (new cdk.AwsRegion()).toString(); + switch (asg.osType) { + case ec2.OperatingSystemType.Linux: + asg.addUserData( + 'PKG_CMD=`which yum 2>/dev/null`', + 'if [ -z "$PKG_CMD" ]; then', + 'PKG_CMD=apt-get', + 'else', + 'PKG=CMD=yum', + 'fi', + '$PKG_CMD update -y', + '$PKG_CMD install -y ruby2.0', + 'if [ $? -ne 0 ]; then', + '$PKG_CMD install -y ruby', + 'fi', + '$PKG_CMD install -y awscli', + 'TMP_DIR=`mktemp -d`', + 'cd $TMP_DIR', + `aws s3 cp s3://aws-codedeploy-${region}/latest/install . --region ${region}`, + 'chmod +x ./install', + './install auto', + 'rm -fr $TMP_DIR', + ); + break; + case ec2.OperatingSystemType.Windows: + asg.addUserData( + 'Set-Variable -Name TEMPDIR -Value (New-TemporaryFile).DirectoryName', + `aws s3 cp s3://aws-codedeploy-${region}/latest/codedeploy-agent.msi $TEMPDIR\\codedeploy-agent.msi`, + '$TEMPDIR\\codedeploy-agent.msi /quiet /l c:\\temp\\host-agent-install-log.txt', + ); + break; + } + } } function deploymentGroupName2Arn(applicationName: string, deploymentGroupName: string): string { diff --git a/packages/@aws-cdk/aws-codedeploy/package.json b/packages/@aws-cdk/aws-codedeploy/package.json index 29d2b797c3fe4..8dfe3cd52586e 100644 --- a/packages/@aws-cdk/aws-codedeploy/package.json +++ b/packages/@aws-cdk/aws-codedeploy/package.json @@ -53,12 +53,15 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "^0.9.2", + "@aws-cdk/aws-ec2": "^0.9.2", "cdk-build-tools": "^0.9.2", "cfn2ts": "^0.9.2", "pkglint": "^0.9.2" }, "dependencies": { + "@aws-cdk/aws-autoscaling": "^0.9.2", "@aws-cdk/aws-codepipeline-api": "^0.9.2", + "@aws-cdk/aws-s3": "^0.9.2", "@aws-cdk/cdk": "^0.9.2" }, "homepage": "https://github.com/awslabs/aws-cdk" diff --git a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts index 9e6af5add179a..99b9dc2e4411d 100644 --- a/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts +++ b/packages/@aws-cdk/aws-codedeploy/test/test.deployment-group.ts @@ -1,4 +1,6 @@ import { expect, haveResource } from '@aws-cdk/assert'; +import autoscaling = require('@aws-cdk/aws-autoscaling'); +import ec2 = require('@aws-cdk/aws-ec2'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import codedeploy = require('../lib'); @@ -38,6 +40,53 @@ export = { test.notEqual(deploymentGroup, undefined); test.done(); - } + }, + + "created with ASGs contains the ASG names"(test: Test) { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Standard3, ec2.InstanceSize.Small), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.VpcNetwork(stack, 'VPC'), + }); + + new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup', { + autoScalingGroups: [asg], + }); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { + "AutoScalingGroups": [ + { + "Ref": "ASG46ED3070", + }, + ] + })); + + test.done(); + }, + + "created without ASGs but adding them later contains the ASG names"(test: Test) { + const stack = new cdk.Stack(); + + const asg = new autoscaling.AutoScalingGroup(stack, 'ASG', { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.Standard3, ec2.InstanceSize.Small), + machineImage: new ec2.AmazonLinuxImage(), + vpc: new ec2.VpcNetwork(stack, 'VPC'), + }); + + const deploymentGroup = new codedeploy.ServerDeploymentGroup(stack, 'DeploymentGroup'); + deploymentGroup.addAutoScalingGroup(asg); + + expect(stack).to(haveResource('AWS::CodeDeploy::DeploymentGroup', { + "AutoScalingGroups": [ + { + "Ref": "ASG46ED3070", + }, + ] + })); + + test.done(); + }, }, };