@@ -23,6 +23,7 @@ import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition } from '../base/
23
23
import { ICluster , CapacityProviderStrategy , ExecuteCommandLogging , Cluster } from '../cluster' ;
24
24
import { ContainerDefinition , Protocol } from '../container-definition' ;
25
25
import { CfnService } from '../ecs.generated' ;
26
+ import { LogDriver , LogDriverConfig } from '../log-drivers/log-driver' ;
26
27
import { ScalableTaskCount } from './scalable-task-count' ;
27
28
28
29
/**
@@ -104,6 +105,76 @@ export interface EcsTarget {
104
105
export interface IEcsLoadBalancerTarget extends elbv2 . IApplicationLoadBalancerTarget , elbv2 . INetworkLoadBalancerTarget , elb . ILoadBalancerTarget {
105
106
}
106
107
108
+ /**
109
+ * Interface for Service Connect configuration.
110
+ */
111
+ export interface ServiceConnectProps {
112
+ /**
113
+ * The cloudmap namespace to register this service into.
114
+ *
115
+ * @default the cloudmap namespace specified on the cluster.
116
+ */
117
+ readonly namespace ?: string ;
118
+
119
+ /**
120
+ * The list of Services, including a port mapping, terse client alias, and optional intermediate DNS name.
121
+ *
122
+ * This property may be left blank if the current ECS service does not need to advertise any ports via Service Connect.
123
+ *
124
+ * @default none
125
+ */
126
+ readonly services ?: ServiceConnectService [ ] ;
127
+
128
+ /**
129
+ * The log driver configuration to use for the Service Connect agent logs.
130
+ *
131
+ * @default - none
132
+ */
133
+ readonly logDriver ?: LogDriver ;
134
+ }
135
+
136
+ /**
137
+ * Interface for service connect Service props.
138
+ */
139
+ export interface ServiceConnectService {
140
+ /**
141
+ * portMappingName specifies which port and protocol combination should be used for this
142
+ * service connect service.
143
+ */
144
+ readonly portMappingName : string ;
145
+
146
+ /**
147
+ * Optionally specifies an intermediate dns name to register in the CloudMap namespace.
148
+ * This is required if you wish to use the same port mapping name in more than one service.
149
+ *
150
+ * @default - port mapping name
151
+ */
152
+ readonly discoveryName ?: string ;
153
+
154
+ /**
155
+ * The terse DNS alias to use for this port mapping in the service connect mesh.
156
+ * Service Connect-enabled clients will be able to reach this service at
157
+ * http://dnsName:port.
158
+ *
159
+ * @default - No alias is created. The service is reachable at `portMappingName.namespace:port`.
160
+ */
161
+ readonly dnsName ?: string ;
162
+
163
+ /**
164
+ The port for clients to use to communicate with this service via Service Connect.
165
+ *
166
+ * @default the container port specified by the port mapping in portMappingName.
167
+ */
168
+ readonly port ?: number ;
169
+
170
+ /**
171
+ * Optional. The port on the Service Connect agent container to use for traffic ingress to this service.
172
+ *
173
+ * @default - none
174
+ */
175
+ readonly ingressPortOverride ?: number ;
176
+ }
177
+
107
178
/**
108
179
* The properties for the base Ec2Service or FargateService service.
109
180
*/
@@ -216,6 +287,14 @@ export interface BaseServiceOptions {
216
287
* @default - undefined
217
288
*/
218
289
readonly enableExecuteCommand ?: boolean ;
290
+
291
+ /**
292
+ * Configuration for Service Connect.
293
+ *
294
+ * @default No ports are advertised via Service Connect on this service, and the service
295
+ * cannot make requests to other services via Service Connect.
296
+ */
297
+ readonly serviceConnectConfiguration ?: ServiceConnectProps ;
219
298
}
220
299
221
300
/**
@@ -367,6 +446,9 @@ export abstract class BaseService extends Resource
367
446
} ) ;
368
447
}
369
448
449
+ private static MIN_PORT = 1 ;
450
+ private static MAX_PORT = 65535 ;
451
+
370
452
/**
371
453
* The security groups which manage the allowed network traffic for the service.
372
454
*/
@@ -417,6 +499,12 @@ export abstract class BaseService extends Resource
417
499
*/
418
500
protected serviceRegistries = new Array < CfnService . ServiceRegistryProperty > ( ) ;
419
501
502
+ /**
503
+ * The service connect configuration for this service.
504
+ * @internal
505
+ */
506
+ protected _serviceConnectConfig ?: CfnService . ServiceConnectConfigurationProperty ;
507
+
420
508
private readonly resource : CfnService ;
421
509
private scalableTaskCount ?: ScalableTaskCount ;
422
510
@@ -469,6 +557,7 @@ export abstract class BaseService extends Resource
469
557
/* role: never specified, supplanted by Service Linked Role */
470
558
networkConfiguration : Lazy . any ( { produce : ( ) => this . networkConfiguration } , { omitEmptyArray : true } ) ,
471
559
serviceRegistries : Lazy . any ( { produce : ( ) => this . serviceRegistries } , { omitEmptyArray : true } ) ,
560
+ serviceConnectConfiguration : Lazy . any ( { produce : ( ) => this . _serviceConnectConfig } , { omitEmptyArray : true } ) ,
472
561
...additionalProps ,
473
562
} ) ;
474
563
@@ -502,6 +591,10 @@ export abstract class BaseService extends Resource
502
591
this . enableCloudMap ( props . cloudMapOptions ) ;
503
592
}
504
593
594
+ if ( props . serviceConnectConfiguration ) {
595
+ this . enableServiceConnect ( props . serviceConnectConfiguration ) ;
596
+ }
597
+
505
598
if ( props . enableExecuteCommand ) {
506
599
this . enableExecuteCommand ( ) ;
507
600
@@ -517,6 +610,135 @@ export abstract class BaseService extends Resource
517
610
this . node . defaultChild = this . resource ;
518
611
}
519
612
613
+ /** * Enable Service Connect
614
+ */
615
+ public enableServiceConnect ( config ?: ServiceConnectProps ) {
616
+ if ( this . _serviceConnectConfig ) {
617
+ throw new Error ( 'Service connect configuration cannot be specified more than once.' ) ;
618
+ }
619
+
620
+ this . validateServiceConnectConfiguration ( config ) ;
621
+
622
+ let cfg = config || { } ;
623
+
624
+ /**
625
+ * Namespace already exists as validated in validateServiceConnectConfiguration.
626
+ * Resolve which namespace to use by picking:
627
+ * 1. The namespace defined in service connect config.
628
+ * 2. The namespace defined in the cluster's defaultCloudMapNamespace property.
629
+ */
630
+ let namespace ;
631
+ if ( this . cluster . defaultCloudMapNamespace ) {
632
+ namespace = this . cluster . defaultCloudMapNamespace . namespaceName ;
633
+ }
634
+
635
+ if ( cfg . namespace ) {
636
+ namespace = cfg . namespace ;
637
+ }
638
+
639
+ /**
640
+ * Map services to CFN property types. This block manages:
641
+ * 1. Finding the correct port.
642
+ * 2. Client alias enumeration
643
+ */
644
+ const services = cfg . services ?. map ( svc => {
645
+ const containerPort = this . taskDefinition . findPortMappingByName ( svc . portMappingName ) ?. containerPort ;
646
+ if ( ! containerPort ) {
647
+ throw new Error ( `Port mapping with name ${ svc . portMappingName } does not exist.` ) ;
648
+ }
649
+ const alias = {
650
+ port : svc . port || containerPort ,
651
+ dnsName : svc . dnsName ,
652
+ } ;
653
+
654
+ return {
655
+ portName : svc . portMappingName ,
656
+ discoveryName : svc . discoveryName ,
657
+ ingressPortOverride : svc . ingressPortOverride ,
658
+ clientAliases : [ alias ] ,
659
+ } as CfnService . ServiceConnectServiceProperty ;
660
+ } ) ;
661
+
662
+ let logConfig : LogDriverConfig | undefined ;
663
+ if ( cfg . logDriver && this . taskDefinition . defaultContainer ) {
664
+ // Default container existence is validated in validateServiceConnectConfiguration.
665
+ // We only need the default container so that bind() can get the task definition from the container definition.
666
+ logConfig = cfg . logDriver . bind ( this , this . taskDefinition . defaultContainer ) ;
667
+ }
668
+
669
+ this . _serviceConnectConfig = {
670
+ enabled : true ,
671
+ logConfiguration : logConfig ,
672
+ namespace : namespace ,
673
+ services : services ,
674
+ } ;
675
+ } ;
676
+
677
+ /**
678
+ * Validate Service Connect Configuration
679
+ */
680
+ private validateServiceConnectConfiguration ( config ?: ServiceConnectProps ) {
681
+ if ( ! this . taskDefinition . defaultContainer ) {
682
+ throw new Error ( 'Task definition must have at least one container to enable service connect.' ) ;
683
+ }
684
+
685
+ // Check the implicit enable case; when config isn't specified or namespace isn't specified, we need to check that there is a namespace on the cluster.
686
+ if ( ( ! config || ! config . namespace ) && ! this . cluster . defaultCloudMapNamespace ) {
687
+ throw new Error ( 'Namespace must be defined either in serviceConnectConfig or cluster.defaultCloudMapNamespace' ) ;
688
+ }
689
+
690
+ // When config isn't specified, return.
691
+ if ( ! config ) {
692
+ return ;
693
+ }
694
+
695
+ if ( ! config . services ) {
696
+ return ;
697
+ }
698
+ let portNames = new Map < string , string [ ] > ( ) ;
699
+ config . services . forEach ( serviceConnectService => {
700
+ // port must exist on the task definition
701
+ if ( ! this . taskDefinition . findPortMappingByName ( serviceConnectService . portMappingName ) ) {
702
+ throw new Error ( `Port Mapping '${ serviceConnectService . portMappingName } ' does not exist on the task definition.` ) ;
703
+ } ;
704
+
705
+ // Check that no two service connect services use the same discovery name.
706
+ const discoveryName = serviceConnectService . discoveryName || serviceConnectService . portMappingName ;
707
+ if ( portNames . get ( serviceConnectService . portMappingName ) ?. includes ( discoveryName ) ) {
708
+ throw new Error ( `Cannot create multiple services with the discoveryName '${ discoveryName } '.` ) ;
709
+ }
710
+
711
+ let currentDiscoveries = portNames . get ( serviceConnectService . portMappingName ) ;
712
+ if ( ! currentDiscoveries ) {
713
+ portNames . set ( serviceConnectService . portMappingName , [ discoveryName ] ) ;
714
+ } else {
715
+ currentDiscoveries . push ( discoveryName ) ;
716
+ portNames . set ( serviceConnectService . portMappingName , currentDiscoveries ) ;
717
+ }
718
+
719
+ // IngressPortOverride should be within the valid port range if it exists.
720
+ if ( serviceConnectService . ingressPortOverride && ! this . isValidPort ( serviceConnectService . ingressPortOverride ) ) {
721
+ throw new Error ( `ingressPortOverride ${ serviceConnectService . ingressPortOverride } is not valid.` ) ;
722
+ }
723
+
724
+ // clientAlias.port should be within the valid port range
725
+ if ( serviceConnectService . port &&
726
+ ! this . isValidPort ( serviceConnectService . port ) ) {
727
+ throw new Error ( `Client Alias port ${ serviceConnectService . port } is not valid.` ) ;
728
+ }
729
+ } ) ;
730
+ }
731
+
732
+ /**
733
+ * Determines if a port is valid
734
+ *
735
+ * @param port: The port number
736
+ * @returns boolean whether the port is valid
737
+ */
738
+ private isValidPort ( port ?: number ) : boolean {
739
+ return ! ! ( port && Number . isInteger ( port ) && port >= BaseService . MIN_PORT && port <= BaseService . MAX_PORT ) ;
740
+ }
741
+
520
742
/**
521
743
* The CloudMap service created for this service, if any.
522
744
*/
0 commit comments