From 3a7cd68ae75db6e8035e4aad0c353f18fc2b1933 Mon Sep 17 00:00:00 2001 From: Mike Cowgill Date: Sun, 23 Sep 2018 23:31:34 -0700 Subject: [PATCH] feat(aws-autoscaling): Add the ability to tag ASG and propagate the tags (#766) --- .../aws-autoscaling/lib/auto-scaling-group.ts | 30 ++- ...g.asg-w-classic-loadbalancer.expected.json | 7 + .../test/test.auto-scaling-group.ts | 213 +++++++++--------- packages/@aws-cdk/cdk/lib/core/tag-manager.ts | 25 +- 4 files changed, 156 insertions(+), 119 deletions(-) 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 62b2e924a31db..dff13ce13b862 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -7,6 +7,11 @@ import cdk = require('@aws-cdk/cdk'); import { cloudformation } from './autoscaling.generated'; +/** + * Name tag constant + */ +const NAME_TAG: string = 'Name'; + /** * Properties of a Fleet */ @@ -124,6 +129,11 @@ export interface AutoScalingGroupProps { * @default 300 (5 minutes) */ resourceSignalTimeoutSec?: number; + + /** + * The AWS resource tags to associate with the ASG. + */ + tags?: cdk.Tags; } /** @@ -137,7 +147,7 @@ export interface AutoScalingGroupProps { * * The ASG spans all availability zones. */ -export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancerTarget, ec2.IConnectable, +export class AutoScalingGroup extends cdk.Construct implements cdk.ITaggable, elb.ILoadBalancerTarget, ec2.IConnectable, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget { /** * The type of OS instances of this fleet are running. @@ -154,6 +164,11 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer */ public readonly role: iam.Role; + /** + * Manage tags for this construct and children + */ + public readonly tags: cdk.TagManager; + private readonly userDataLines = new Array(); private readonly autoScalingGroup: cloudformation.AutoScalingGroupResource; private readonly securityGroup: ec2.SecurityGroupRef; @@ -167,6 +182,8 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer this.securityGroup = new ec2.SecurityGroup(this, 'InstanceSecurityGroup', { vpc: props.vpc }); this.connections = new ec2.Connections({ securityGroup: this.securityGroup }); this.securityGroups.push(this.securityGroup); + this.tags = new TagManager(this, {initialTags: props.tags}); + this.tags.setTag(NAME_TAG, this.path, { overwrite: false }); if (props.allowAllOutbound !== false) { this.connections.allowTo(new ec2.AnyIPv4(), new ec2.AllConnections(), 'Outbound traffic allowed by default'); @@ -211,6 +228,7 @@ export class AutoScalingGroup extends cdk.Construct implements elb.ILoadBalancer launchConfigurationName: launchConfig.ref, loadBalancerNames: new cdk.Token(() => this.loadBalancerNames.length > 0 ? this.loadBalancerNames : undefined), targetGroupArns: new cdk.Token(() => this.targetGroupArns.length > 0 ? this.targetGroupArns : undefined), + tags: this.tags, }; if (props.notificationsTopic) { @@ -472,6 +490,16 @@ function renderRollingUpdateConfig(config: RollingUpdateConfiguration = {}): cdk }; } +class TagManager extends cdk.TagManager { + protected tagFormatResolve(tagGroups: cdk.TagGroups): any { + const tags = {...tagGroups.nonSitckyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; + return Object.keys(tags).map( (key) => { + const propagateAtLaunch = !!tagGroups.propagateTags[key] || !!tagGroups.ancestorTags[key]; + return {key, value: tags[key], propagateAtLaunch}; + }); + } +} + /** * Render a number of seconds to a PTnX string. */ diff --git a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json index c92cd2c79f071..8a854a4d8df7f 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json +++ b/packages/@aws-cdk/aws-autoscaling/test/integ.asg-w-classic-loadbalancer.expected.json @@ -544,6 +544,13 @@ "Ref": "LB8A12904C" } ], + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-cdk-ec2-integ/Fleet" + } + ], "VPCZoneIdentifier": [ { "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" diff --git a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts index 124ef0e85386d..58d8f9b699971 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/test.auto-scaling-group.ts @@ -99,6 +99,14 @@ export = { "LaunchConfigurationName": { "Ref": "MyFleetLaunchConfig5D7F9801" }, + "LoadBalancerNames": [], + "Tags": [ + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "MyFleet" + } + ], "MaxSize": "1", "MinSize": "1", "VPCZoneIdentifier": [ @@ -123,121 +131,69 @@ export = { }); fleet.addToRolePolicy(new cdk.PolicyStatement() - .addAction('*') + .addAction('test:SpecialName') .addAllResources()); - expect(stack).toMatch({ - "Resources": { - "MyFleetInstanceSecurityGroup774E8234": { - "Type": "AWS::EC2::SecurityGroup", - "Properties": { - "GroupDescription": "MyFleet/InstanceSecurityGroup", - "SecurityGroupEgress": [ - { - "CidrIp": "0.0.0.0/0", - "Description": "Outbound traffic allowed by default", - "FromPort": -1, - "IpProtocol": "-1", - "ToPort": -1 - } - ], - "SecurityGroupIngress": [], - "VpcId": "my-vpc" - } - }, - MyFleetInstanceRole25A84AB8: { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Statement": [ - { - "Action": "sts:AssumeRole", - "Effect": "Allow", - "Principal": { - "Service": "ec2.amazonaws.com" - } - } - ], - "Version": "2012-10-17" - } - } - }, - MyFleetInstanceRoleDefaultPolicy7B0197E7: { - "Type": "AWS::IAM::Policy", - "Properties": { - "PolicyDocument": { - "Statement": [ - { - "Action": "*", - "Effect": "Allow", - "Resource": "*" - } - ], - "Version": "2012-10-17" - }, - "PolicyName": "MyFleetInstanceRoleDefaultPolicy7B0197E7", - "Roles": [ + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ { - "Ref": "MyFleetInstanceRole25A84AB8" + Action: "test:SpecialName", + Effect: "Allow", + Resource: "*" } - ] - } - }, - MyFleetInstanceProfile70A58496: { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Roles": [ + ], + Version: "2012-10-17" + }, + })); + test.done(); + }, + + 'can configure replacing update'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyFleet', { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + updateType: autoscaling.UpdateType.ReplacingUpdate, + replacingUpdateMinSuccessfulInstancesPercent: 50 + }); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { + UpdatePolicy: { + AutoScalingReplacingUpdate: { + WillReplace: true + } + }, + CreationPolicy: { + AutoScalingCreationPolicy: { + MinSuccessfulInstancesPercent: 50 + } + } + }, ResourcePart.CompleteDefinition)); + + test.done(); + }, + + 'can configure rolling update'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyFleet', { { "Ref": "MyFleetInstanceRole25A84AB8" } - ] - } - }, - MyFleetLaunchConfig5D7F9801: { - Type: "AWS::AutoScaling::LaunchConfiguration", - Properties: { - "IamInstanceProfile": { - "Ref": "MyFleetInstanceProfile70A58496" - }, - "ImageId": "dummy", - "InstanceType": "m4.micro", - "SecurityGroups": [ - { - "Fn::GetAtt": [ - "MyFleetInstanceSecurityGroup774E8234", - "GroupId" - ] - } - ], - "UserData": { - "Fn::Base64": "#!/bin/bash\n" - } - }, - DependsOn: [ - "MyFleetInstanceRole25A84AB8", - "MyFleetInstanceRoleDefaultPolicy7B0197E7" - ] - }, - MyFleetASG88E55886: { - Type: "AWS::AutoScaling::AutoScalingGroup", - UpdatePolicy: { - AutoScalingScheduledAction: { IgnoreUnmodifiedGroupSizeProperties: true } - }, - Properties: { - DesiredCapacity: "1", - LaunchConfigurationName: { - Ref: "MyFleetLaunchConfig5D7F9801" - }, - MaxSize: "1", - MinSize: "1", - VPCZoneIdentifier: [ - "pri1" - ] - } - } - } - }); - + ], + Version: "2012-10-17" + }, + })); test.done(); }, @@ -354,6 +310,47 @@ export = { })); test.done(); }, + 'can set tags'(test: Test) { + // GIVEN + const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); + const vpc = mockVpc(stack); + + // WHEN + const asg = new autoscaling.AutoScalingGroup(stack, 'MyFleet', { + instanceType: new ec2.InstanceTypePair(ec2.InstanceClass.M4, ec2.InstanceSize.Micro), + machineImage: new ec2.AmazonLinuxImage(), + vpc, + updateType: autoscaling.UpdateType.RollingUpdate, + rollingUpdateConfiguration: { + minSuccessfulInstancesPercent: 50, + pauseTimeSec: 345 + }, + tags: {superfood: 'acai'}, + }); + asg.tags.setTag('notsuper', 'caramel', {propagate: false}); + + // THEN + expect(stack).to(haveResource("AWS::AutoScaling::AutoScalingGroup", { + Tags: [ + { + Key: 'superfood', + Value: 'acai', + PropagateAtLaunch: true, + }, + { + Key: 'Name', + Value: 'MyFleet', + PropagateAtLaunch: true, + }, + { + Key: 'notsuper', + Value: 'caramel', + PropagateAtLaunch: false, + }, + ] + })); + test.done(); + }, }; function mockVpc(stack: cdk.Stack) { diff --git a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts index f8c092269fe68..d0b4b5daa00cd 100644 --- a/packages/@aws-cdk/cdk/lib/core/tag-manager.ts +++ b/packages/@aws-cdk/cdk/lib/core/tag-manager.ts @@ -48,6 +48,9 @@ export interface TagProps { overwrite?: boolean; } +/** + * This is the interface for arguments to `tagFormatResolve` to enable extensions + */ export interface TagGroups { /** * Tags that overwrite ancestor tags @@ -82,19 +85,14 @@ export interface RemoveProps { blockPropagate?: boolean; } +/** + * Properties for Tag Manager + */ export interface TagManagerProps { - initialTags?: Tags; -<<<<<<< HEAD - autoScalingGroup?: boolean; -||||||| parent of 99e87ce7... tag manager refactor to extract only a single protected method - /** - * If set this tag Manager will resolve to Autoscaling Group Tags with - * PropagateAtLaunch set based on the tag property `propagate` + * Initial tags to set on the tag manager using TAG_DEFAULTS */ - autoScalingGroup?: boolean; -======= ->>>>>>> 99e87ce7... tag manager refactor to extract only a single protected method + initialTags?: Tags; } /** @@ -257,6 +255,13 @@ export class TagManager extends Token { delete this._tags[key]; } + /** + * Handles returning the tags in the desired format + * + * This function can be overridden to support another tag format. This was + * specifically designed to enable AutoScalingGroup Tags that have an + * additional CloudFormation key for `PropagateAtLaunch` + */ protected tagFormatResolve(tagGroups: TagGroups): any { const tags = {...tagGroups.nonSitckyTags, ...tagGroups.ancestorTags, ...tagGroups.stickyTags}; for (const key of this.blockedTags) { delete tags[key]; }