diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 54dbb34a106fa..702c4617bace2 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -267,6 +267,8 @@ const service = new ecs.FargateService(this, 'Service', { desiredCount: 5 }); ``` +`Services` by default will create a security group if not provided. +If you'd like to specify which security groups to use you can override the `securityGroups` property. ### Include an application/network load balancer diff --git a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts index b1c34e6fd0a6b..ba6aa7451cdf2 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/base-service.ts @@ -586,6 +586,7 @@ export abstract class BaseService extends Resource /** * This method is called to create a networkConfiguration. + * @deprecated use configureAwsVpcNetworkingWithSecurityGroups instead. */ // tslint:disable-next-line:max-line-length protected configureAwsVpcNetworking(vpc: ec2.IVpc, assignPublicIp?: boolean, vpcSubnets?: ec2.SubnetSelection, securityGroup?: ec2.ISecurityGroup) { @@ -606,6 +607,29 @@ export abstract class BaseService extends Resource }; } + /** + * This method is called to create a networkConfiguration. + */ + // tslint:disable-next-line:max-line-length + protected configureAwsVpcNetworkingWithSecurityGroups(vpc: ec2.IVpc, assignPublicIp?: boolean, vpcSubnets?: ec2.SubnetSelection, securityGroups?: ec2.ISecurityGroup[]) { + if (vpcSubnets === undefined) { + vpcSubnets = assignPublicIp ? { subnetType: ec2.SubnetType.PUBLIC } : {}; + } + if (securityGroups === undefined || securityGroups.length === 0) { + securityGroups = [ new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }) ]; + } + + securityGroups.forEach((sg) => { this.connections.addSecurityGroup(sg); }, this); + + this.networkConfiguration = { + awsvpcConfiguration: { + assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED', + subnets: vpc.selectSubnets(vpcSubnets).subnetIds, + securityGroups: securityGroups.map((sg) => sg.securityGroupId), + }, + }; + } + private renderServiceRegistry(registry: ServiceRegistry): CfnService.ServiceRegistryProperty { return { registryArn: registry.arn, diff --git a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts index 924d212075ae4..d492687c3263d 100644 --- a/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts @@ -43,9 +43,19 @@ export interface Ec2ServiceProps extends BaseServiceOptions { * This property is only used for tasks that use the awsvpc network mode. * * @default - A new security group is created. + * @deprecated use securityGroups instead. */ readonly securityGroup?: ec2.ISecurityGroup; + /** + * The security groups to associate with the service. If you do not specify a security group, the default security group for the VPC is used. + * + * This property is only used for tasks that use the awsvpc network mode. + * + * @default - A new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + /** * The placement constraints to use for tasks in the service. For more information, see * [Amazon ECS Task Placement Constraints](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-placement-constraints.html). @@ -166,6 +176,10 @@ export class Ec2Service extends BaseService implements IEc2Service { throw new Error('You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank'); } + if (props.securityGroup !== undefined && props.securityGroups !== undefined) { + throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); + } + const propagateTagsFromSource = props.propagateTaskTagsFrom !== undefined ? props.propagateTaskTagsFrom : (props.propagateTags !== undefined ? props.propagateTags : PropagatedTagSource.NONE); @@ -191,8 +205,15 @@ export class Ec2Service extends BaseService implements IEc2Service { this.strategies = []; this.daemon = props.daemon || false; + let securityGroups; + if (props.securityGroup !== undefined) { + securityGroups = [ props.securityGroup ]; + } else if (props.securityGroups !== undefined) { + securityGroups = props.securityGroups; + } + if (props.taskDefinition.networkMode === NetworkMode.AWS_VPC) { - this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, props.securityGroup); + this.configureAwsVpcNetworkingWithSecurityGroups(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, securityGroups); } else { // Either None, Bridge or Host networking. Copy SecurityGroups from ASG. // We have to be smart here -- by default future Security Group rules would be created @@ -251,11 +272,14 @@ export class Ec2Service extends BaseService implements IEc2Service { } /** - * Validate combinations of networking arguments + * Validate combinations of networking arguments. */ function validateNoNetworkingProps(props: Ec2ServiceProps) { - if (props.vpcSubnets !== undefined || props.securityGroup !== undefined || props.assignPublicIp) { - throw new Error('vpcSubnets, securityGroup and assignPublicIp can only be used in AwsVpc networking mode'); + if (props.vpcSubnets !== undefined + || props.securityGroup !== undefined + || props.securityGroups !== undefined + || props.assignPublicIp) { + throw new Error('vpcSubnets, securityGroup(s) and assignPublicIp can only be used in AwsVpc networking mode'); } } diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts index 855fdb9775d38..185bc800e5da9 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts @@ -36,9 +36,17 @@ export interface FargateServiceProps extends BaseServiceOptions { * The security groups to associate with the service. If you do not specify a security group, the default security group for the VPC is used. * * @default - A new security group is created. + * @deprecated use securityGroups instead. */ readonly securityGroup?: ec2.ISecurityGroup; + /** + * The security groups to associate with the service. If you do not specify a security group, the default security group for the VPC is used. + * + * @default - A new security group is created. + */ + readonly securityGroups?: ec2.ISecurityGroup[]; + /** * The platform version on which to run your service. * @@ -128,6 +136,10 @@ export class FargateService extends BaseService implements IFargateService { throw new Error('You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank'); } + if (props.securityGroup !== undefined && props.securityGroups !== undefined) { + throw new Error('Only one of SecurityGroup or SecurityGroups can be populated.'); + } + const propagateTagsFromSource = props.propagateTaskTagsFrom !== undefined ? props.propagateTaskTagsFrom : (props.propagateTags !== undefined ? props.propagateTags : PropagatedTagSource.NONE); @@ -143,7 +155,14 @@ export class FargateService extends BaseService implements IFargateService { platformVersion: props.platformVersion, }, props.taskDefinition); - this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, props.securityGroup); + let securityGroups; + if (props.securityGroup !== undefined) { + securityGroups = [ props.securityGroup ]; + } else if (props.securityGroups !== undefined) { + securityGroups = props.securityGroups; + } + + this.configureAwsVpcNetworkingWithSecurityGroups(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, securityGroups); if (!props.taskDefinition.defaultContainer) { throw new Error('A TaskDefinition must have at least one essential container'); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts index 1250066b2c09a..6b74bcf656e9a 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts @@ -237,6 +237,165 @@ export = { test.done(); }, + 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: ecs.NetworkMode.AWS_VPC, + }); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + // WHEN + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + desiredCount: 2, + assignPublicIp: true, + daemon: false, + securityGroups: [ securityGroup1, securityGroup2 ], + serviceName: 'bonjour', + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'Ec2TaskDef0226F28C', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DesiredCount: 2, + LaunchType: LaunchType.EC2, + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: 'ENABLED', + SecurityGroups: [ + { + 'Fn::GetAtt': [ + 'SecurityGroup1F554B36F', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'SecurityGroup23BE86BB7', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'MyVpcPublicSubnet1SubnetF6608456', + }, + { + Ref: 'MyVpcPublicSubnet2Subnet492B6BFB', + }, + ], + }, + }, + SchedulingStrategy: 'REPLICA', + ServiceName: 'bonjour', + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Bingo', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + VpcId: { + Ref: 'MyVpcF9F0CA6F', + }, + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Rolly', + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + VpcId: { + Ref: 'MyVpcF9F0CA6F', + }, + })); + + test.done(); + }, + + 'throws when both securityGroup and securityGroups are supplied'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: ecs.NetworkMode.AWS_VPC, + }); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + desiredCount: 2, + assignPublicIp: true, + maxHealthyPercent: 150, + minHealthyPercent: 55, + securityGroup: securityGroup1, + securityGroups: [ securityGroup2 ], + serviceName: 'bonjour', + vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, + }); + }, /Only one of SecurityGroup or SecurityGroups can be populated./); + + test.done(); + }, + 'throws when task definition is not EC2 compatible'(test: Test) { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -506,8 +665,101 @@ export = { taskDefinition, assignPublicIp: true, }); + }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + + // THEN + test.done(); + }, + + 'it errors if vpc subnets is provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const subnet = new ec2.Subnet(stack, 'MySubnet', { + vpcId: vpc.vpcId, + availabilityZone: 'eu-central-1a', + cidrBlock: '10.10.0.0/20', + }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, }); + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + vpcSubnets: { + subnets: [subnet], + }, + }); + }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + + // THEN + test.done(); + }, + + 'it errors if security group is provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const securityGroup = new ec2.SecurityGroup(stack, 'MySG', { vpc }); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + securityGroup, + }); + }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + + // THEN + test.done(); + }, + + 'it errors if multiple security groups is provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const securityGroups = [ + new ec2.SecurityGroup(stack, 'MyFirstSG', { vpc }), + new ec2.SecurityGroup(stack, 'MySecondSG', { vpc }), + ]; + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { + networkMode: ecs.NetworkMode.BRIDGE, + }); + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + memoryLimitMiB: 512, + }); + + // THEN + test.throws(() => { + new ecs.Ec2Service(stack, 'Ec2Service', { + cluster, + taskDefinition, + securityGroups, + }); + }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + // THEN test.done(); }, diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts index 9a652b61fe0d4..454d904f7592f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/test.fargate-service.ts @@ -434,6 +434,151 @@ export = { test.done(); }, + + 'throws when securityGroup and securityGroups are supplied'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + + // THEN + test.throws(() => { + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + securityGroup: securityGroup1, + securityGroups: [ securityGroup2 ], + }); + }, /Only one of SecurityGroup or SecurityGroups can be populated./); + + test.done(); + }, + + 'with multiple securty groups, it correctly updates cloudformation template'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); + const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); + const securityGroup1 = new ec2.SecurityGroup(stack, 'SecurityGroup1', { + allowAllOutbound: true, + description: 'Example', + securityGroupName: 'Bingo', + vpc, + }); + const securityGroup2 = new ec2.SecurityGroup(stack, 'SecurityGroup2', { + allowAllOutbound: false, + description: 'Example', + securityGroupName: 'Rolly', + vpc, + }); + + taskDefinition.addContainer('web', { + image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'), + }); + + new ecs.FargateService(stack, 'FargateService', { + cluster, + taskDefinition, + securityGroups: [ securityGroup1, securityGroup2 ], + }); + + // THEN + expect(stack).to(haveResource('AWS::ECS::Service', { + TaskDefinition: { + Ref: 'FargateTaskDefC6FB60B4', + }, + Cluster: { + Ref: 'EcsCluster97242B84', + }, + DeploymentConfiguration: { + MaximumPercent: 200, + MinimumHealthyPercent: 50, + }, + DesiredCount: 1, + LaunchType: LaunchType.FARGATE, + EnableECSManagedTags: false, + NetworkConfiguration: { + AwsvpcConfiguration: { + AssignPublicIp: 'DISABLED', + SecurityGroups: [ + { + 'Fn::GetAtt': [ + 'SecurityGroup1F554B36F', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'SecurityGroup23BE86BB7', + 'GroupId', + ], + }, + ], + Subnets: [ + { + Ref: 'MyVpcPrivateSubnet1Subnet5057CF7E', + }, + { + Ref: 'MyVpcPrivateSubnet2Subnet0040C983', + }, + ], + }, + }, + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Bingo', + SecurityGroupEgress: [ + { + CidrIp: '0.0.0.0/0', + Description: 'Allow all outbound traffic by default', + IpProtocol: '-1', + }, + ], + VpcId: { + Ref: 'MyVpcF9F0CA6F', + }, + })); + + expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + GroupDescription: 'Example', + GroupName: 'Rolly', + SecurityGroupEgress: [ + { + CidrIp: '255.255.255.255/32', + Description: 'Disallow all traffic', + FromPort: 252, + IpProtocol: 'icmp', + ToPort: 86, + }, + ], + VpcId: { + Ref: 'MyVpcF9F0CA6F', + }, + })); + + test.done(); + }, + }, 'When setting up a health check': {