Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion allowed-breaking-changes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ change-return-type:@aws-cdk/aws-lambda-destinations.EventBridgeDestination.bind
change-return-type:@aws-cdk/aws-lambda-destinations.LambdaDestination.bind
change-return-type:@aws-cdk/aws-lambda-destinations.SnsDestination.bind
change-return-type:@aws-cdk/aws-lambda-destinations.SqsDestination.bind

incompatible-argument:@aws-cdk/aws-ecs.BaseService.configureAwsVpcNetworking
incompatible-argument:@aws-cdk/aws-ecs.Ec2Service.configureAwsVpcNetworking
incompatible-argument:@aws-cdk/aws-ecs.FargateService.configureAwsVpcNetworking
11 changes: 6 additions & 5 deletions packages/@aws-cdk/aws-ecs/lib/base/base-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -565,20 +565,21 @@ export abstract class BaseService extends Resource
* This method is called to create a networkConfiguration.
*/
// tslint:disable-next-line:max-line-length
protected configureAwsVpcNetworking(vpc: ec2.IVpc, assignPublicIp?: boolean, vpcSubnets?: ec2.SubnetSelection, securityGroup?: ec2.ISecurityGroup) {
protected configureAwsVpcNetworking(vpc: ec2.IVpc, assignPublicIp?: boolean, vpcSubnets?: ec2.SubnetSelection, securityGroups?: ec2.ISecurityGroup[]) {
if (vpcSubnets === undefined) {
vpcSubnets = { subnetType: assignPublicIp ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE };
}
if (securityGroup === undefined) {
securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc });
if (securityGroups === undefined || securityGroups.length === 0) {
securityGroups = [ new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }) ];
}
this.connections.addSecurityGroup(securityGroup);

securityGroups.forEach((sg) => { this.connections.addSecurityGroup(sg); }, this);

this.networkConfiguration = {
awsvpcConfiguration: {
assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED',
subnets: vpc.selectSubnets(vpcSubnets).subnetIds,
securityGroups: Lazy.listValue({ produce: () => [securityGroup!.securityGroupId] }),
securityGroups: securityGroups.map((sg) => sg.securityGroupId)
}
};
}
Expand Down
29 changes: 27 additions & 2 deletions packages/@aws-cdk/aws-ecs/lib/ec2/ec2-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,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).
Expand Down Expand Up @@ -108,6 +118,11 @@ export class Ec2Service extends BaseService implements IEc2Service {
private readonly strategies: CfnService.PlacementStrategyProperty[];
private readonly daemon: boolean;

/**
* The security groups associated to the task.
*/
private readonly securityGroups?: ec2.ISecurityGroup[];

/**
* Constructs a new instance of the Ec2Service class.
*/
Expand All @@ -132,6 +147,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);

Expand All @@ -157,8 +176,14 @@ export class Ec2Service extends BaseService implements IEc2Service {
this.strategies = [];
this.daemon = props.daemon || false;

if (props.securityGroup !== undefined) {
this.securityGroups = [ props.securityGroup ];
} else if (props.securityGroups !== undefined) {
this.securityGroups = props.securityGroups;
}

if (props.taskDefinition.networkMode === NetworkMode.AWS_VPC) {
this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, props.securityGroup);
this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, this.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
Expand Down Expand Up @@ -220,7 +245,7 @@ export class Ec2Service extends BaseService implements IEc2Service {
* Validate combinations of networking arguments
*/
function validateNoNetworkingProps(props: Ec2ServiceProps) {
if (props.vpcSubnets !== undefined || props.securityGroup !== undefined || props.assignPublicIp) {
if (props.vpcSubnets !== undefined || props.securityGroups !== undefined || props.assignPublicIp) {
throw new Error('vpcSubnets, securityGroup and assignPublicIp can only be used in AwsVpc networking mode');
}
}
Expand Down
25 changes: 24 additions & 1 deletion packages/@aws-cdk/aws-ecs/lib/fargate/fargate-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,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.
*
Expand Down Expand Up @@ -82,6 +90,11 @@ export class FargateService extends BaseService implements IFargateService {
return new Import(scope, id);
}

/**
* The security groups associated to the task.
*/
private readonly securityGroups?: ec2.ISecurityGroup[];

/**
* Constructs a new instance of the FargateService class.
*/
Expand All @@ -94,6 +107,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);

Expand All @@ -109,7 +126,13 @@ export class FargateService extends BaseService implements IFargateService {
platformVersion: props.platformVersion,
}, props.taskDefinition);

this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, props.securityGroup);
if (props.securityGroup !== undefined) {
this.securityGroups = [ props.securityGroup ];
} else if (props.securityGroups !== undefined) {
this.securityGroups = props.securityGroups;
}

this.configureAwsVpcNetworking(props.cluster.vpc, props.assignPublicIp, props.vpcSubnets, this.securityGroups);

if (!props.taskDefinition.defaultContainer) {
throw new Error('A TaskDefinition must have at least one essential container');
Expand Down
217 changes: 217 additions & 0 deletions packages/@aws-cdk/aws-ecs/test/ec2/test.ec2-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,223 @@ export = {
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 });
cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') });
const taskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', {
networkMode: ecs.NetworkMode.AWS_VPC
});
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,
});

cluster.addDefaultCloudMapNamespace({
name: 'foo.com',
type: cloudmap.NamespaceType.DNS_PRIVATE
});

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512,
});

// WHEN
const service = new ecs.Ec2Service(stack, "Ec2Service", {
cluster,
taskDefinition,
desiredCount: 2,
assignPublicIp: true,
cloudMapOptions: {
name: "myapp",
dnsRecordType: cloudmap.DnsRecordType.A,
dnsTtl: cdk.Duration.seconds(50),
failureThreshold: 20
},
daemon: false,
healthCheckGracePeriod: cdk.Duration.seconds(60),
maxHealthyPercent: 150,
minHealthyPercent: 55,
securityGroups: [ securityGroup1, securityGroup2 ],
serviceName: "bonjour",
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }
});

service.addPlacementConstraints(PlacementConstraint.memberOf("attribute:ecs.instance-type =~ t2.*"));
service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE));

// THEN
expect(stack).to(haveResource("AWS::ECS::Service", {
TaskDefinition: {
Ref: "Ec2TaskDef0226F28C"
},
Cluster: {
Ref: "EcsCluster97242B84"
},
DeploymentConfiguration: {
MaximumPercent: 150,
MinimumHealthyPercent: 55
},
DesiredCount: 2,
LaunchType: LaunchType.EC2,
NetworkConfiguration: {
AwsvpcConfiguration: {
AssignPublicIp: "ENABLED",
SecurityGroups: [
{
"Fn::GetAtt": [
"SecurityGroup1F554B36F",
"GroupId"
]
},
{
"Fn::GetAtt": [
"SecurityGroup23BE86BB7",
"GroupId"
]
}
],
Subnets: [
{
Ref: "MyVpcPublicSubnet1SubnetF6608456"
},
{
Ref: "MyVpcPublicSubnet2Subnet492B6BFB"
}
]
}
},
PlacementConstraints: [
{
Expression: "attribute:ecs.instance-type =~ t2.*",
Type: "memberOf"
}
],
PlacementStrategies: [
{
Field: "attribute:ecs.availability-zone",
Type: "spread"
}
],
SchedulingStrategy: "REPLICA",
ServiceName: "bonjour",
ServiceRegistries: [
{
RegistryArn: {
"Fn::GetAtt": [
"Ec2ServiceCloudmapService45B52C0F",
"Arn"
]
}
}
]
}));

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
});
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,
});

cluster.addDefaultCloudMapNamespace({
name: 'foo.com',
type: cloudmap.NamespaceType.DNS_PRIVATE
});

taskDefinition.addContainer("web", {
image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
memoryLimitMiB: 512,
});

// THEN
test.throws(() => {
new ecs.Ec2Service(stack, "Ec2Service", {
cluster,
taskDefinition,
desiredCount: 2,
assignPublicIp: true,
cloudMapOptions: {
name: "myapp",
dnsRecordType: cloudmap.DnsRecordType.A,
dnsTtl: cdk.Duration.seconds(50),
failureThreshold: 20
},
daemon: false,
healthCheckGracePeriod: cdk.Duration.seconds(60),
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', {});
Expand Down
Loading