packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts (889 lines of code) (raw):

import { Construct } from 'constructs'; import { ScalableTaskCount } from './scalable-task-count'; import { ServiceManagedVolume } from './service-managed-volume'; import * as appscaling from '../../../aws-applicationautoscaling'; import * as cloudwatch from '../../../aws-cloudwatch'; import * as ec2 from '../../../aws-ec2'; import * as elb from '../../../aws-elasticloadbalancing'; import * as elbv2 from '../../../aws-elasticloadbalancingv2'; import * as iam from '../../../aws-iam'; import * as kms from '../../../aws-kms'; import * as cloudmap from '../../../aws-servicediscovery'; import { Annotations, Duration, IResolvable, IResource, Lazy, Resource, Stack, ArnFormat, FeatureFlags, Token, Arn, Fn, } from '../../../core'; import * as cxapi from '../../../cx-api'; import { RegionInfo } from '../../../region-info'; import { LoadBalancerTargetOptions, NetworkMode, TaskDefinition, TaskDefinitionRevision, } from '../base/task-definition'; import { ICluster, CapacityProviderStrategy, ExecuteCommandLogging, Cluster } from '../cluster'; import { ContainerDefinition, Protocol } from '../container-definition'; import { CfnService } from '../ecs.generated'; import { LogDriver, LogDriverConfig } from '../log-drivers/log-driver'; /** * The interface for a service. */ export interface IService extends IResource { /** * The Amazon Resource Name (ARN) of the service. * * @attribute */ readonly serviceArn: string; /** * The name of the service. * * @attribute */ readonly serviceName: string; } /** * The deployment controller to use for the service. */ export interface DeploymentController { /** * The deployment controller type to use. * * @default DeploymentControllerType.ECS */ readonly type?: DeploymentControllerType; } /** * The deployment circuit breaker to use for the service */ export interface DeploymentCircuitBreaker { /** * Whether to enable the deployment circuit breaker logic * @default true */ readonly enable?: boolean; /** * Whether to enable rollback on deployment failure * * @default false */ readonly rollback?: boolean; } /** * Deployment behavior when an ECS Service Deployment Alarm is triggered */ export enum AlarmBehavior { /** * ROLLBACK_ON_ALARM causes the service to roll back to the previous deployment * when any deployment alarm enters the 'Alarm' state. The Cloudformation stack * will be rolled back and enter state "UPDATE_ROLLBACK_COMPLETE". */ ROLLBACK_ON_ALARM = 'ROLLBACK_ON_ALARM', /** * FAIL_ON_ALARM causes the deployment to fail immediately when any deployment * alarm enters the 'Alarm' state. In order to restore functionality, you must * roll the stack forward by pushing a new version of the ECS service. */ FAIL_ON_ALARM = 'FAIL_ON_ALARM', } /** * Options for deployment alarms */ export interface DeploymentAlarmOptions { /** * Default rollback on alarm * @default AlarmBehavior.ROLLBACK_ON_ALARM */ readonly behavior?: AlarmBehavior; } /** * Configuration for deployment alarms */ export interface DeploymentAlarmConfig extends DeploymentAlarmOptions { /** * List of alarm names to monitor during deployments */ readonly alarmNames: string[]; } export interface EcsTarget { /** * The name of the container. */ readonly containerName: string; /** * The port number of the container. Only applicable when using application/network load balancers. * * @default - Container port of the first added port mapping. */ readonly containerPort?: number; /** * The protocol used for the port mapping. Only applicable when using application load balancers. * * @default Protocol.TCP */ readonly protocol?: Protocol; /** * ID for a target group to be created. */ readonly newTargetGroupId: string; /** * Listener and properties for adding target group to the listener. */ readonly listener: ListenerConfig; } /** * Interface for ECS load balancer target. */ export interface IEcsLoadBalancerTarget extends elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { } /** * Interface for Service Connect configuration. */ export interface ServiceConnectProps { /** * The cloudmap namespace to register this service into. * * @default the cloudmap namespace specified on the cluster. */ readonly namespace?: string; /** * The list of Services, including a port mapping, terse client alias, and optional intermediate DNS name. * * This property may be left blank if the current ECS service does not need to advertise any ports via Service Connect. * * @default none */ readonly services?: ServiceConnectService[]; /** * The log driver configuration to use for the Service Connect agent logs. * * @default - none */ readonly logDriver?: LogDriver; } /** * Interface for service connect Service props. */ export interface ServiceConnectService { /** * portMappingName specifies which port and protocol combination should be used for this * service connect service. */ readonly portMappingName: string; /** * Optionally specifies an intermediate dns name to register in the CloudMap namespace. * This is required if you wish to use the same port mapping name in more than one service. * * @default - port mapping name */ readonly discoveryName?: string; /** * The terse DNS alias to use for this port mapping in the service connect mesh. * Service Connect-enabled clients will be able to reach this service at * http://dnsName:port. * * @default - No alias is created. The service is reachable at `portMappingName.namespace:port`. */ readonly dnsName?: string; /** * The port for clients to use to communicate with this service via Service Connect. * * @default the container port specified by the port mapping in portMappingName. */ readonly port?: number; /** * Optional. The port on the Service Connect agent container to use for traffic ingress to this service. * * @default - none */ readonly ingressPortOverride?: number; /** * The amount of time in seconds a connection for Service Connect will stay active while idle. * * A value of 0 can be set to disable `idleTimeout`. * * If `idleTimeout` is set to a time that is less than `perRequestTimeout`, the connection will close * when the `idleTimeout` is reached and not the `perRequestTimeout`. * * @default - Duration.minutes(5) for HTTP/HTTP2/GRPC, Duration.hours(1) for TCP. */ readonly idleTimeout?: Duration; /** * The amount of time waiting for the upstream to respond with a complete response per request for * Service Connect. * * A value of 0 can be set to disable `perRequestTimeout`. * Can only be set when the `appProtocol` for the application container is HTTP/HTTP2/GRPC. * * If `idleTimeout` is set to a time that is less than `perRequestTimeout`, the connection will close * when the `idleTimeout` is reached and not the `perRequestTimeout`. * * @default - Duration.seconds(15) */ readonly perRequestTimeout?: Duration; /** * A reference to an object that represents a Transport Layer Security (TLS) configuration. * * @default - none */ readonly tls?: ServiceConnectTlsConfiguration; } /** * TLS configuration for Service Connect service */ export interface ServiceConnectTlsConfiguration { /** * The ARN of the certificate root authority that secures your service. * * @default - none */ readonly awsPcaAuthorityArn?: string; /** * The KMS key used for encryption and decryption. * * @default - none */ readonly kmsKey?: kms.IKey; /** * The IAM role that's associated with the Service Connect TLS. * * @default - none */ readonly role?: iam.IRole; } /** * The properties for the base Ec2Service or FargateService service. */ export interface BaseServiceOptions { /** * The name of the cluster that hosts the service. */ readonly cluster: ICluster; /** * The desired number of instantiations of the task definition to keep running on the service. * * @default - When creating the service, default is 1; when updating the service, default uses * the current task number. */ readonly desiredCount?: number; /** * The name of the service. * * @default - CloudFormation-generated name. */ readonly serviceName?: string; /** * The maximum number of tasks, specified as a percentage of the Amazon ECS * service's DesiredCount value, that can run in a service during a * deployment. * * @default - 100 if daemon, otherwise 200 */ readonly maxHealthyPercent?: number; /** * The minimum number of tasks, specified as a percentage of * the Amazon ECS service's DesiredCount value, that must * continue to run and remain healthy during a deployment. * * @default - 0 if daemon, otherwise 50 */ readonly minHealthyPercent?: number; /** * The period of time, in seconds, that the Amazon ECS service scheduler ignores unhealthy * Elastic Load Balancing target health checks after a task has first started. * * @default - defaults to 60 seconds if at least one load balancer is in-use and it is not already set */ readonly healthCheckGracePeriod?: Duration; /** * The options for configuring an Amazon ECS service to use service discovery. * * @default - AWS Cloud Map service discovery is not enabled. */ readonly cloudMapOptions?: CloudMapOptions; /** * Specifies whether to propagate the tags from the task definition or the service to the tasks in the service * * Valid values are: PropagatedTagSource.SERVICE, PropagatedTagSource.TASK_DEFINITION or PropagatedTagSource.NONE * * @default PropagatedTagSource.NONE */ readonly propagateTags?: PropagatedTagSource; /** * Specifies whether to propagate the tags from the task definition or the service to the tasks in the service. * Tags can only be propagated to the tasks within the service during service creation. * * @deprecated Use `propagateTags` instead. * @default PropagatedTagSource.NONE */ readonly propagateTaskTagsFrom?: PropagatedTagSource; /** * Specifies whether to enable Amazon ECS managed tags for the tasks within the service. For more information, see * [Tagging Your Amazon ECS Resources](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-using-tags.html) * * @default false */ readonly enableECSManagedTags?: boolean; /** * Specifies which deployment controller to use for the service. For more information, see * [Amazon ECS Deployment Types](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-types.html) * * @default - Rolling update (ECS) */ readonly deploymentController?: DeploymentController; /** * Whether to enable the deployment circuit breaker. If this property is defined, circuit breaker will be implicitly * enabled. * @default - disabled */ readonly circuitBreaker?: DeploymentCircuitBreaker; /** * The alarm(s) to monitor during deployment, and behavior to apply if at least one enters a state of alarm * during the deployment or bake time. * * * @default - No alarms will be monitored during deployment. */ readonly deploymentAlarms?: DeploymentAlarmConfig; /** * A list of Capacity Provider strategies used to place a service. * * @default - undefined * */ readonly capacityProviderStrategies?: CapacityProviderStrategy[]; /** * Whether to enable the ability to execute into a container * * @default - undefined */ readonly enableExecuteCommand?: boolean; /** * Configuration for Service Connect. * * @default No ports are advertised via Service Connect on this service, and the service * cannot make requests to other services via Service Connect. */ readonly serviceConnectConfiguration?: ServiceConnectProps; /** * Revision number for the task definition or `latest` to use the latest active task revision. * * @default - Uses the revision of the passed task definition deployed by CloudFormation */ readonly taskDefinitionRevision?: TaskDefinitionRevision; /** * Configuration details for a volume used by the service. This allows you to specify * details about the EBS volume that can be attched to ECS tasks. * * @default - undefined */ readonly volumeConfigurations?: ServiceManagedVolume[]; } /** * Complete base service properties that are required to be supplied by the implementation * of the BaseService class. */ export interface BaseServiceProps extends BaseServiceOptions { /** * The launch type on which to run your service. * * LaunchType will be omitted if capacity provider strategies are specified on the service. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html#cfn-ecs-service-capacityproviderstrategy * * Valid values are: LaunchType.ECS or LaunchType.FARGATE or LaunchType.EXTERNAL */ readonly launchType: LaunchType; } /** * Base class for configuring listener when registering targets. */ export abstract class ListenerConfig { /** * Create a config for adding target group to ALB listener. */ public static applicationListener(listener: elbv2.ApplicationListener, props?: elbv2.AddApplicationTargetsProps): ListenerConfig { return new ApplicationListenerConfig(listener, props); } /** * Create a config for adding target group to NLB listener. */ public static networkListener(listener: elbv2.NetworkListener, props?: elbv2.AddNetworkTargetsProps): ListenerConfig { return new NetworkListenerConfig(listener, props); } /** * Create and attach a target group to listener. */ public abstract addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService): void; } /** * Class for configuring application load balancer listener when registering targets. */ class ApplicationListenerConfig extends ListenerConfig { constructor(private readonly listener: elbv2.ApplicationListener, private readonly props?: elbv2.AddApplicationTargetsProps) { super(); } /** * Create and attach a target group to listener. */ public addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService) { const props = this.props || {}; const protocol = props.protocol; const port = props.port ?? (protocol === elbv2.ApplicationProtocol.HTTPS ? 443 : 80); this.listener.addTargets(id, { ... props, targets: [ service.loadBalancerTarget({ ...target, }), ], port, }); } } /** * Class for configuring network load balancer listener when registering targets. */ class NetworkListenerConfig extends ListenerConfig { constructor(private readonly listener: elbv2.NetworkListener, private readonly props?: elbv2.AddNetworkTargetsProps) { super(); } /** * Create and attach a target group to listener. */ public addTargets(id: string, target: LoadBalancerTargetOptions, service: BaseService) { const port = this.props?.port ?? 80; this.listener.addTargets(id, { ... this.props, targets: [ service.loadBalancerTarget({ ...target, }), ], port, }); } } /** * The interface for BaseService. */ export interface IBaseService extends IService { /** * The cluster that hosts the service. */ readonly cluster: ICluster; } /** * The base class for Ec2Service and FargateService services. */ export abstract class BaseService extends Resource implements IBaseService, elbv2.IApplicationLoadBalancerTarget, elbv2.INetworkLoadBalancerTarget, elb.ILoadBalancerTarget { /** * Import an existing ECS/Fargate Service using the service cluster format. * The format is the "new" format "arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name". * @see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-account-settings.html#ecs-resource-ids */ public static fromServiceArnWithCluster(scope: Construct, id: string, serviceArn: string): IBaseService { const stack = Stack.of(scope); const arn = stack.splitArn(serviceArn, ArnFormat.SLASH_RESOURCE_NAME); const resourceName = Arn.extractResourceName(serviceArn, 'service'); let clusterName: string; let serviceName: string; if (Token.isUnresolved(resourceName)) { clusterName = Fn.select(0, Fn.split('/', resourceName)); serviceName = Fn.select(1, Fn.split('/', resourceName)); } else { const resourceNameParts = resourceName.split('/'); if (resourceNameParts.length !== 2) { throw new Error(`resource name ${resourceName} from service ARN: ${serviceArn} is not using the ARN cluster format`); } clusterName = resourceNameParts[0]; serviceName = resourceNameParts[1]; } const clusterArn = Stack.of(scope).formatArn({ partition: arn.partition, region: arn.region, account: arn.account, service: 'ecs', resource: 'cluster', resourceName: clusterName, }); const cluster = Cluster.fromClusterArn(scope, `${id}Cluster`, clusterArn); class Import extends Resource implements IBaseService { public readonly serviceArn = serviceArn; public readonly serviceName = serviceName; public readonly cluster = cluster; } return new Import(scope, id, { environmentFromArn: serviceArn, }); } private static MIN_PORT = 1; private static MAX_PORT = 65535; /** * The security groups which manage the allowed network traffic for the service. */ public readonly connections: ec2.Connections = new ec2.Connections(); /** * The Amazon Resource Name (ARN) of the service. */ public readonly serviceArn: string; /** * The name of the service. * * @attribute */ public readonly serviceName: string; /** * The task definition to use for tasks in the service. */ public readonly taskDefinition: TaskDefinition; /** * The cluster that hosts the service. */ public readonly cluster: ICluster; /** * The details of the AWS Cloud Map service. */ protected cloudmapService?: cloudmap.Service; /** * A list of Elastic Load Balancing load balancer objects, containing the load balancer name, the container * name (as it appears in a container definition), and the container port to access from the load balancer. */ protected loadBalancers = new Array<CfnService.LoadBalancerProperty>(); /** * A list of Elastic Load Balancing load balancer objects, containing the load balancer name, the container * name (as it appears in a container definition), and the container port to access from the load balancer. */ protected networkConfiguration?: CfnService.NetworkConfigurationProperty; /** * The deployment alarms property - this will be rendered directly and lazily as the CfnService.alarms * property. */ protected deploymentAlarms?: CfnService.DeploymentAlarmsProperty; /** * The details of the service discovery registries to assign to this service. * For more information, see Service Discovery. */ protected serviceRegistries = new Array<CfnService.ServiceRegistryProperty>(); /** * The service connect configuration for this service. * @internal */ protected _serviceConnectConfig?: CfnService.ServiceConnectConfigurationProperty; private readonly resource: CfnService; private scalableTaskCount?: ScalableTaskCount; /** * All volumes */ private readonly volumes: ServiceManagedVolume[] = []; /** * Constructs a new instance of the BaseService class. */ constructor( scope: Construct, id: string, props: BaseServiceProps, additionalProps: any, taskDefinition: TaskDefinition) { super(scope, id, { physicalName: props.serviceName, }); if (props.propagateTags && props.propagateTaskTagsFrom) { throw new Error('You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank'); } this.taskDefinition = taskDefinition; // launchType will set to undefined if using external DeploymentController or capacityProviderStrategies const launchType = props.deploymentController?.type === DeploymentControllerType.EXTERNAL || props.capacityProviderStrategies !== undefined ? undefined : props.launchType; const propagateTagsFromSource = props.propagateTaskTagsFrom ?? props.propagateTags ?? PropagatedTagSource.NONE; const deploymentController = this.getDeploymentController(props); this.resource = new CfnService(this, 'Service', { desiredCount: props.desiredCount, serviceName: this.physicalName, loadBalancers: Lazy.any({ produce: () => this.loadBalancers }, { omitEmptyArray: true }), deploymentConfiguration: { maximumPercent: props.maxHealthyPercent || 200, minimumHealthyPercent: props.minHealthyPercent === undefined ? 50 : props.minHealthyPercent, deploymentCircuitBreaker: props.circuitBreaker ? { enable: props.circuitBreaker.enable ?? true, rollback: props.circuitBreaker.rollback ?? false, } : undefined, alarms: Lazy.any({ produce: () => this.deploymentAlarms }, { omitEmptyArray: true }), }, propagateTags: propagateTagsFromSource === PropagatedTagSource.NONE ? undefined : props.propagateTags, enableEcsManagedTags: props.enableECSManagedTags ?? false, deploymentController: deploymentController, launchType: launchType, enableExecuteCommand: props.enableExecuteCommand, capacityProviderStrategy: props.capacityProviderStrategies, healthCheckGracePeriodSeconds: this.evaluateHealthGracePeriod(props.healthCheckGracePeriod), /* role: never specified, supplanted by Service Linked Role */ networkConfiguration: Lazy.any({ produce: () => this.networkConfiguration }, { omitEmptyArray: true }), serviceRegistries: Lazy.any({ produce: () => this.serviceRegistries }, { omitEmptyArray: true }), serviceConnectConfiguration: Lazy.any({ produce: () => this._serviceConnectConfig }, { omitEmptyArray: true }), volumeConfigurations: Lazy.any({ produce: () => this.renderVolumes() }, { omitEmptyArray: true }), ...additionalProps, }); this.node.addDependency(this.taskDefinition.taskRole); if (props.deploymentController?.type === DeploymentControllerType.EXTERNAL) { Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:externalDeploymentController', 'taskDefinition and launchType are blanked out when using external deployment controller.'); } if (props.circuitBreaker && deploymentController && deploymentController.type !== DeploymentControllerType.ECS) { Annotations.of(this).addError('Deployment circuit breaker requires the ECS deployment controller.'); } if (props.deploymentAlarms && deploymentController && deploymentController.type !== DeploymentControllerType.ECS) { throw new Error('Deployment alarms requires the ECS deployment controller.'); } if ( props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY && props.taskDefinitionRevision && props.taskDefinitionRevision !== TaskDefinitionRevision.LATEST ) { throw new Error('CODE_DEPLOY deploymentController can only be used with the `latest` task definition revision'); } if (props.minHealthyPercent === undefined) { Annotations.of(this).addWarningV2('@aws-cdk/aws-ecs:minHealthyPercent', 'minHealthyPercent has not been configured so the default value of 50% is used. The number of running tasks will decrease below the desired count during deployments etc. See https://github.com/aws/aws-cdk/issues/31705'); } if (props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY) { // Strip the revision ID from the service's task definition property to // prevent new task def revisions in the stack from triggering updates // to the stack's ECS service resource this.resource.taskDefinition = taskDefinition.family; this.node.addDependency(taskDefinition); } else if (props.taskDefinitionRevision) { this.resource.taskDefinition = taskDefinition.family; if (props.taskDefinitionRevision !== TaskDefinitionRevision.LATEST) { this.resource.taskDefinition += `:${props.taskDefinitionRevision.revision}`; } this.node.addDependency(taskDefinition); } this.serviceArn = this.getResourceArnAttribute(this.resource.ref, { service: 'ecs', resource: 'service', resourceName: `${props.cluster.clusterName}/${this.physicalName}`, }); this.serviceName = this.getResourceNameAttribute(this.resource.attrName); this.cluster = props.cluster; if (props.cloudMapOptions) { this.enableCloudMap(props.cloudMapOptions); } if (props.serviceConnectConfiguration) { this.enableServiceConnect(props.serviceConnectConfiguration); } if (props.volumeConfigurations) { props.volumeConfigurations.forEach(v => this.addVolume(v)); } if (props.enableExecuteCommand) { this.enableExecuteCommand(); const logging = this.cluster.executeCommandConfiguration?.logging ?? ExecuteCommandLogging.DEFAULT; if (this.cluster.executeCommandConfiguration?.kmsKey) { this.enableExecuteCommandEncryption(logging); } if (logging !== ExecuteCommandLogging.NONE) { this.executeCommandLogConfiguration(); } } if (props.deploymentAlarms) { if (props.deploymentAlarms.alarmNames.length === 0) { throw new Error('at least one alarm name is required when specifying deploymentAlarms, received empty array'); } this.deploymentAlarms = { alarmNames: props.deploymentAlarms.alarmNames, enable: true, rollback: props.deploymentAlarms.behavior !== AlarmBehavior.FAIL_ON_ALARM, }; // CloudWatch alarms is only supported for Amazon ECS services that use the rolling update (ECS) deployment controller. } else if ((!props.deploymentController || props.deploymentController?.type === DeploymentControllerType.ECS) && this.deploymentAlarmsAvailableInRegion()) { // Only set default deployment alarms settings when feature flag is not enabled. if (!FeatureFlags.of(this).isEnabled(cxapi.ECS_REMOVE_DEFAULT_DEPLOYMENT_ALARM)) { this.deploymentAlarms = { alarmNames: [], enable: false, rollback: false, }; } } this.node.defaultChild = this.resource; } /** * Adds a volume to the Service. */ public addVolume(volume: ServiceManagedVolume) { this.volumes.push(volume); } private renderVolumes(): CfnService.ServiceVolumeConfigurationProperty[] { if (this.volumes.length > 1) { throw new Error(`Only one EBS volume can be specified for 'volumeConfigurations', got: ${this.volumes.length}`); } return this.volumes.map(renderVolume); function renderVolume(spec: ServiceManagedVolume): CfnService.ServiceVolumeConfigurationProperty { const tagSpecifications = spec.config?.tagSpecifications?.map(ebsTagSpec => { return { resourceType: 'volume', propagateTags: ebsTagSpec.propagateTags, tags: ebsTagSpec.tags ? Object.entries(ebsTagSpec.tags).map(([key, value]) => ({ key: key, value: value, })) : undefined, } as CfnService.EBSTagSpecificationProperty; }); return { name: spec.name, managedEbsVolume: spec.config && { roleArn: spec.role.roleArn, encrypted: spec.config.encrypted, filesystemType: spec.config.fileSystemType, iops: spec.config.iops, kmsKeyId: spec.config.kmsKeyId?.keyId, throughput: spec.config.throughput, volumeType: spec.config.volumeType, snapshotId: spec.config.snapShotId, sizeInGiB: spec.config.size?.toGibibytes(), tagSpecifications: tagSpecifications, }, }; } } /** * Enable Deployment Alarms which take advantage of arbitrary alarms and configure them after service initialization. * If you have already enabled deployment alarms, this function can be used to tell ECS about additional alarms that * should interrupt a deployment. * * New alarms specified in subsequent calls of this function will be appended to the existing list of alarms. * * The same Alarm Behavior must be used on all deployment alarms. If you specify different AlarmBehavior values in * multiple calls to this function, or the Alarm Behavior used here doesn't match the one used in the service * constructor, an error will be thrown. * * If the alarm's metric references the service, you cannot pass `Alarm.alarmName` here. That will cause a circular * dependency between the service and its deployment alarm. See this package's README for options to alarm on service * metrics, and avoid this circular dependency. * */ public enableDeploymentAlarms(alarmNames: string[], options?: DeploymentAlarmOptions) { if (alarmNames.length === 0 ) { throw new Error('at least one alarm name is required when calling enableDeploymentAlarms(), received empty array'); } alarmNames.forEach(alarmName => { if (Token.isUnresolved(alarmName)) { Annotations.of(this).addInfo( `Deployment alarm (${JSON.stringify(this.stack.resolve(alarmName))}) enabled on ${this.node.id} may cause a circular dependency error when this stack deploys. The alarm name references the alarm's logical id, or another resource. See the 'Deployment alarms' section in the module README for more details.`, ); } }); if (this.deploymentAlarms?.enable && options?.behavior) { if ( (AlarmBehavior.ROLLBACK_ON_ALARM === options.behavior && !this.deploymentAlarms.rollback) || (AlarmBehavior.FAIL_ON_ALARM === options.behavior && this.deploymentAlarms.rollback) ) { throw new Error(`all deployment alarms on an ECS service must have the same AlarmBehavior. Attempted to enable deployment alarms with ${options.behavior}, but alarms were previously enabled with ${this.deploymentAlarms.rollback ? AlarmBehavior.ROLLBACK_ON_ALARM : AlarmBehavior.FAIL_ON_ALARM}`); } } if (!this.deploymentAlarms?.enable) { this.deploymentAlarms = { enable: true, alarmNames: alarmNames, rollback: options?.behavior !== AlarmBehavior.FAIL_ON_ALARM, }; } else { // If deployment alarms have previously been enabled, we only need to add // the new alarm names, since rollback behaviors can't be updated/mixed. this.deploymentAlarms.alarmNames.concat(alarmNames); } } /** * Enable Service Connect on this service. */ public enableServiceConnect(config?: ServiceConnectProps) { if (this._serviceConnectConfig) { throw new Error('Service connect configuration cannot be specified more than once.'); } this.validateServiceConnectConfiguration(config); let cfg = config || {}; /** * Namespace already exists as validated in validateServiceConnectConfiguration. * Resolve which namespace to use by picking: * 1. The namespace defined in service connect config. * 2. The namespace defined in the cluster's defaultCloudMapNamespace property. */ let namespace; if (this.cluster.defaultCloudMapNamespace) { namespace = this.cluster.defaultCloudMapNamespace.namespaceName; } if (cfg.namespace) { namespace = cfg.namespace; } /** * Map services to CFN property types. This block manages: * 1. Finding the correct port. * 2. Client alias enumeration */ const services = cfg.services?.map(svc => { const containerPort = this.taskDefinition.findPortMappingByName(svc.portMappingName)?.containerPort; if (!containerPort) { throw new Error(`Port mapping with name ${svc.portMappingName} does not exist.`); } const alias = { port: svc.port || containerPort, dnsName: svc.dnsName, }; const tls: CfnService.ServiceConnectTlsConfigurationProperty | undefined = svc.tls ? { issuerCertificateAuthority: { awsPcaAuthorityArn: svc.tls.awsPcaAuthorityArn, }, kmsKey: svc.tls.kmsKey?.keyArn, roleArn: svc.tls.role?.roleArn, } : undefined; return { portName: svc.portMappingName, discoveryName: svc.discoveryName, ingressPortOverride: svc.ingressPortOverride, clientAliases: [alias], timeout: this.renderTimeout(svc.idleTimeout, svc.perRequestTimeout), tls, } as CfnService.ServiceConnectServiceProperty; }); let logConfig: LogDriverConfig | undefined; if (cfg.logDriver && this.taskDefinition.defaultContainer) { // Default container existence is validated in validateServiceConnectConfiguration. // We only need the default container so that bind() can get the task definition from the container definition. logConfig = cfg.logDriver.bind(this, this.taskDefinition.defaultContainer); } this._serviceConnectConfig = { enabled: true, logConfiguration: logConfig, namespace: namespace, services: services, }; } /** * Validate Service Connect Configuration */ private validateServiceConnectConfiguration(config?: ServiceConnectProps) { if (!this.taskDefinition.defaultContainer) { throw new Error('Task definition must have at least one container to enable service connect.'); } // 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. if ((!config || !config.namespace) && !this.cluster.defaultCloudMapNamespace) { throw new Error('Namespace must be defined either in serviceConnectConfig or cluster.defaultCloudMapNamespace'); } // When config isn't specified, return. if (!config) { return; } if (!config.services) { return; } let portNames = new Map<string, string[]>(); config.services.forEach(serviceConnectService => { // port must exist on the task definition if (!this.taskDefinition.findPortMappingByName(serviceConnectService.portMappingName)) { throw new Error(`Port Mapping '${serviceConnectService.portMappingName}' does not exist on the task definition.`); } // Check that no two service connect services use the same discovery name. const discoveryName = serviceConnectService.discoveryName || serviceConnectService.portMappingName; if (portNames.get(serviceConnectService.portMappingName)?.includes(discoveryName)) { throw new Error(`Cannot create multiple services with the discoveryName '${discoveryName}'.`); } let currentDiscoveries = portNames.get(serviceConnectService.portMappingName); if (!currentDiscoveries) { portNames.set(serviceConnectService.portMappingName, [discoveryName]); } else { currentDiscoveries.push(discoveryName); portNames.set(serviceConnectService.portMappingName, currentDiscoveries); } // IngressPortOverride should be within the valid port range if it exists. if (serviceConnectService.ingressPortOverride && !this.isValidPort(serviceConnectService.ingressPortOverride)) { throw new Error(`ingressPortOverride ${serviceConnectService.ingressPortOverride} is not valid.`); } // clientAlias.port should be within the valid port range if (serviceConnectService.port && !this.isValidPort(serviceConnectService.port)) { throw new Error(`Client Alias port ${serviceConnectService.port} is not valid.`); } // tls.awsPcaAuthorityArn should be an ARN const awsPcaAuthorityArn = serviceConnectService.tls?.awsPcaAuthorityArn; if (awsPcaAuthorityArn && !Token.isUnresolved(awsPcaAuthorityArn) && !awsPcaAuthorityArn.startsWith('arn:')) { throw new Error(`awsPcaAuthorityArn must start with "arn:" and have at least 6 components; received ${awsPcaAuthorityArn}`); } }); } /** * Determines if a port is valid * * @param port: The port number * @returns boolean whether the port is valid */ private isValidPort(port?: number): boolean { return !!(port && Number.isInteger(port) && port >= BaseService.MIN_PORT && port <= BaseService.MAX_PORT); } /** * The CloudMap service created for this service, if any. */ public get cloudMapService(): cloudmap.IService | undefined { return this.cloudmapService; } private getDeploymentController(props: BaseServiceProps): DeploymentController | undefined { if (props.deploymentController) { // The customer is always right return props.deploymentController; } const disableCircuitBreakerEcsDeploymentControllerFeatureFlag = FeatureFlags.of(this).isEnabled(cxapi.ECS_DISABLE_EXPLICIT_DEPLOYMENT_CONTROLLER_FOR_CIRCUIT_BREAKER); if (!disableCircuitBreakerEcsDeploymentControllerFeatureFlag && props.circuitBreaker) { // This is undesirable behavior (the controller is implicitly ECS anyway when left // undefined, so specifying it is not necessary but DOES trigger a CFN replacement) // but we leave it in for backwards compat. return { type: DeploymentControllerType.ECS, }; } return undefined; } private executeCommandLogConfiguration() { const reducePermissions = FeatureFlags.of(this).isEnabled(cxapi.REDUCE_EC2_FARGATE_CLOUDWATCH_PERMISSIONS); const logConfiguration = this.cluster.executeCommandConfiguration?.logConfiguration; // When Feature Flag is false, keep the previous behaviour for non-breaking changes. // When Feature Flag is true and when cloudwatch log group is specified in logConfiguration, then // append the necessary permissions to the task definition. if (!reducePermissions || logConfiguration?.cloudWatchLogGroup) { this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 'logs:DescribeLogGroups', ], resources: ['*'], })); const logGroupArn = logConfiguration?.cloudWatchLogGroup ? `arn:${this.stack.partition}:logs:${this.env.region}:${this.env.account}:log-group:${logConfiguration.cloudWatchLogGroup.logGroupName}:*` : '*'; this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 'logs:CreateLogStream', 'logs:DescribeLogStreams', 'logs:PutLogEvents', ], resources: [logGroupArn], })); } if (logConfiguration?.s3Bucket?.bucketName) { this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 's3:GetBucketLocation', ], resources: ['*'], })); this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 's3:PutObject', ], resources: [`arn:${this.stack.partition}:s3:::${logConfiguration.s3Bucket.bucketName}/*`], })); if (logConfiguration.s3EncryptionEnabled) { this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 's3:GetEncryptionConfiguration', ], resources: [`arn:${this.stack.partition}:s3:::${logConfiguration.s3Bucket.bucketName}`], })); } } } private enableExecuteCommandEncryption(logging: ExecuteCommandLogging) { this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 'kms:Decrypt', 'kms:GenerateDataKey', ], resources: [`${this.cluster.executeCommandConfiguration?.kmsKey?.keyArn}`], })); this.cluster.executeCommandConfiguration?.kmsKey?.addToResourcePolicy(new iam.PolicyStatement({ actions: [ 'kms:*', ], resources: ['*'], principals: [new iam.ArnPrincipal(`arn:${this.stack.partition}:iam::${this.env.account}:root`)], })); if (logging === ExecuteCommandLogging.DEFAULT || this.cluster.executeCommandConfiguration?.logConfiguration?.cloudWatchEncryptionEnabled) { this.cluster.executeCommandConfiguration?.kmsKey?.addToResourcePolicy(new iam.PolicyStatement({ actions: [ 'kms:Encrypt*', 'kms:Decrypt*', 'kms:ReEncrypt*', 'kms:GenerateDataKey*', 'kms:Describe*', ], resources: ['*'], principals: [new iam.ServicePrincipal(`logs.${this.env.region}.amazonaws.com`)], conditions: { ArnLike: { 'kms:EncryptionContext:aws:logs:arn': `arn:${this.stack.partition}:logs:${this.env.region}:${this.env.account}:*` }, }, })); } } /** * This method is called to attach this service to an Application Load Balancer. * * Don't call this function directly. Instead, call `listener.addTargets()` * to add this service to a load balancer. */ public attachToApplicationTargetGroup(targetGroup: elbv2.IApplicationTargetGroup): elbv2.LoadBalancerTargetProps { return this.defaultLoadBalancerTarget.attachToApplicationTargetGroup(targetGroup); } /** * Registers the service as a target of a Classic Load Balancer (CLB). * * Don't call this. Call `loadBalancer.addTarget()` instead. * * @param loadBalancer [disable-awslint:ref-via-interface] */ public attachToClassicLB(loadBalancer: elb.LoadBalancer): void { return this.defaultLoadBalancerTarget.attachToClassicLB(loadBalancer); } /** * Return a load balancing target for a specific container and port. * * Use this function to create a load balancer target if you want to load balance to * another container than the first essential container or the first mapped port on * the container. * * Use the return value of this function where you would normally use a load balancer * target, instead of the `Service` object itself. * * @example * * declare const listener: elbv2.ApplicationListener; * declare const service: ecs.BaseService; * listener.addTargets('ECS', { * port: 80, * targets: [service.loadBalancerTarget({ * containerName: 'MyContainer', * containerPort: 1234, * })], * }); */ public loadBalancerTarget(options: LoadBalancerTargetOptions): IEcsLoadBalancerTarget { const self = this; const target = this.taskDefinition._validateTarget(options); const connections = self.connections; return { attachToApplicationTargetGroup(targetGroup: elbv2.ApplicationTargetGroup): elbv2.LoadBalancerTargetProps { targetGroup.registerConnectable(self, self.taskDefinition._portRangeFromPortMapping(target.portMapping)); return self.attachToELBv2(targetGroup, target.containerName, target.portMapping.containerPort!); }, attachToNetworkTargetGroup(targetGroup: elbv2.NetworkTargetGroup): elbv2.LoadBalancerTargetProps { return self.attachToELBv2(targetGroup, target.containerName, target.portMapping.containerPort!); }, connections, attachToClassicLB(loadBalancer: elb.LoadBalancer): void { return self.attachToELB(loadBalancer, target.containerName, target.portMapping.containerPort!); }, }; } /** * Use this function to create all load balancer targets to be registered in this service, add them to * target groups, and attach target groups to listeners accordingly. * * Alternatively, you can use `listener.addTargets()` to create targets and add them to target groups. * * @example * * declare const listener: elbv2.ApplicationListener; * declare const service: ecs.BaseService; * service.registerLoadBalancerTargets( * { * containerName: 'web', * containerPort: 80, * newTargetGroupId: 'ECS', * listener: ecs.ListenerConfig.applicationListener(listener, { * protocol: elbv2.ApplicationProtocol.HTTPS * }), * }, * ) */ public registerLoadBalancerTargets(...targets: EcsTarget[]) { for (const target of targets) { target.listener.addTargets(target.newTargetGroupId, { containerName: target.containerName, containerPort: target.containerPort, protocol: target.protocol, }, this); } } /** * This method is called to attach this service to a Network Load Balancer. * * Don't call this function directly. Instead, call `listener.addTargets()` * to add this service to a load balancer. */ public attachToNetworkTargetGroup(targetGroup: elbv2.INetworkTargetGroup): elbv2.LoadBalancerTargetProps { return this.defaultLoadBalancerTarget.attachToNetworkTargetGroup(targetGroup); } /** * An attribute representing the minimum and maximum task count for an AutoScalingGroup. */ public autoScaleTaskCount(props: appscaling.EnableScalingProps) { if (this.scalableTaskCount) { throw new Error('AutoScaling of task count already enabled for this service'); } return this.scalableTaskCount = new ScalableTaskCount(this, 'TaskCount', { serviceNamespace: appscaling.ServiceNamespace.ECS, resourceId: `service/${this.cluster.clusterName}/${this.serviceName}`, dimension: 'ecs:service:DesiredCount', role: this.makeAutoScalingRole(), ...props, }); } /** * Enable CloudMap service discovery for the service * * @returns The created CloudMap service */ public enableCloudMap(options: CloudMapOptions): cloudmap.Service { const sdNamespace = options.cloudMapNamespace ?? this.cluster.defaultCloudMapNamespace; if (sdNamespace === undefined) { throw new Error('Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster.'); } if (sdNamespace.type === cloudmap.NamespaceType.HTTP) { throw new Error('Cannot enable DNS service discovery for HTTP Cloudmap Namespace.'); } // Determine DNS type based on network mode const networkMode = this.taskDefinition.networkMode; if (networkMode === NetworkMode.NONE) { throw new Error('Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead.'); } // Bridge or host network mode requires SRV records let dnsRecordType = options.dnsRecordType; if (networkMode === NetworkMode.BRIDGE || networkMode === NetworkMode.HOST) { if (dnsRecordType === undefined) { dnsRecordType = cloudmap.DnsRecordType.SRV; } if (dnsRecordType !== cloudmap.DnsRecordType.SRV) { throw new Error('SRV records must be used when network mode is Bridge or Host.'); } } // Default DNS record type for AwsVpc network mode is A Records if (networkMode === NetworkMode.AWS_VPC) { if (dnsRecordType === undefined) { dnsRecordType = cloudmap.DnsRecordType.A; } } const { containerName, containerPort } = determineContainerNameAndPort({ taskDefinition: this.taskDefinition, dnsRecordType: dnsRecordType!, container: options.container, containerPort: options.containerPort, }); const cloudmapService = new cloudmap.Service(this, 'CloudmapService', { namespace: sdNamespace, name: options.name, dnsRecordType: dnsRecordType!, customHealthCheck: { failureThreshold: options.failureThreshold || 1 }, dnsTtl: options.dnsTtl, }); const serviceArn = cloudmapService.serviceArn; // add Cloudmap service to the ECS Service's serviceRegistry this.addServiceRegistry({ arn: serviceArn, containerName, containerPort, }); this.cloudmapService = cloudmapService; return cloudmapService; } /** * Associates this service with a CloudMap service */ public associateCloudMapService(options: AssociateCloudMapServiceOptions): void { const service = options.service; const { containerName, containerPort } = determineContainerNameAndPort({ taskDefinition: this.taskDefinition, dnsRecordType: service.dnsRecordType, container: options.container, containerPort: options.containerPort, }); // add Cloudmap service to the ECS Service's serviceRegistry this.addServiceRegistry({ arn: service.serviceArn, containerName, containerPort, }); } /** * This method returns the specified CloudWatch metric name for this service. */ public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ namespace: 'AWS/ECS', metricName, dimensionsMap: { ClusterName: this.cluster.clusterName, ServiceName: this.serviceName }, ...props, }).attachTo(this); } /** * This method returns the CloudWatch metric for this service's memory utilization. * * @default average over 5 minutes */ public metricMemoryUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('MemoryUtilization', props); } /** * This method returns the CloudWatch metric for this service's CPU utilization. * * @default average over 5 minutes */ public metricCpuUtilization(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('CPUUtilization', props); } /** * This method is called to create a networkConfiguration. * @deprecated use configureAwsVpcNetworkingWithSecurityGroups instead. */ // eslint-disable-next-line max-len protected configureAwsVpcNetworking(vpc: ec2.IVpc, assignPublicIp?: boolean, vpcSubnets?: ec2.SubnetSelection, securityGroup?: ec2.ISecurityGroup) { if (vpcSubnets === undefined) { vpcSubnets = assignPublicIp ? { subnetType: ec2.SubnetType.PUBLIC } : {}; } if (securityGroup === undefined) { securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc }); } this.connections.addSecurityGroup(securityGroup); this.networkConfiguration = { awsvpcConfiguration: { assignPublicIp: assignPublicIp ? 'ENABLED' : 'DISABLED', subnets: vpc.selectSubnets(vpcSubnets).subnetIds, securityGroups: Lazy.list({ produce: () => [securityGroup!.securityGroupId] }), }, }; } /** * This method is called to create a networkConfiguration. */ // eslint-disable-next-line max-len 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, containerName: registry.containerName, containerPort: registry.containerPort, }; } /** * Shared logic for attaching to an ELB */ private attachToELB(loadBalancer: elb.LoadBalancer, containerName: string, containerPort: number): void { if (this.taskDefinition.networkMode === NetworkMode.AWS_VPC) { throw new Error('Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead.'); } if (this.taskDefinition.networkMode === NetworkMode.NONE) { throw new Error('Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead.'); } this.loadBalancers.push({ loadBalancerName: loadBalancer.loadBalancerName, containerName, containerPort, }); } /** * Shared logic for attaching to an ELBv2 */ private attachToELBv2(targetGroup: elbv2.ITargetGroup, containerName: string, containerPort: number): elbv2.LoadBalancerTargetProps { if (this.taskDefinition.networkMode === NetworkMode.NONE) { throw new Error('Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead.'); } this.loadBalancers.push({ targetGroupArn: targetGroup.targetGroupArn, containerName, containerPort, }); // Service creation can only happen after the load balancer has // been associated with our target group(s), so add ordering dependency. this.resource.node.addDependency(targetGroup.loadBalancerAttached); const targetType = this.taskDefinition.networkMode === NetworkMode.AWS_VPC ? elbv2.TargetType.IP : elbv2.TargetType.INSTANCE; return { targetType }; } private get defaultLoadBalancerTarget() { return this.loadBalancerTarget({ containerName: this.taskDefinition.defaultContainer!.containerName, }); } /** * Generate the role that will be used for autoscaling this service */ private makeAutoScalingRole(): iam.IRole { // Use a Service Linked Role. return iam.Role.fromRoleArn(this, 'ScalingRole', Stack.of(this).formatArn({ region: '', service: 'iam', resource: 'role/aws-service-role/ecs.application-autoscaling.amazonaws.com', resourceName: 'AWSServiceRoleForApplicationAutoScaling_ECSService', })); } /** * Associate Service Discovery (Cloud Map) service */ private addServiceRegistry(registry: ServiceRegistry) { if (this.serviceRegistries.length >= 1) { throw new Error('Cannot associate with the given service discovery registry. ECS supports at most one service registry per service.'); } const sr = this.renderServiceRegistry(registry); this.serviceRegistries.push(sr); } /** * Return the default grace period when load balancers are configured and * healthCheckGracePeriod is not already set */ private evaluateHealthGracePeriod(providedHealthCheckGracePeriod?: Duration): IResolvable { return Lazy.any({ produce: () => providedHealthCheckGracePeriod?.toSeconds() ?? (this.loadBalancers.length > 0 ? 60 : undefined), }); } private enableExecuteCommand() { this.taskDefinition.addToTaskRolePolicy(new iam.PolicyStatement({ actions: [ 'ssmmessages:CreateControlChannel', 'ssmmessages:CreateDataChannel', 'ssmmessages:OpenControlChannel', 'ssmmessages:OpenDataChannel', ], resources: ['*'], })); } private deploymentAlarmsAvailableInRegion(): boolean { const unsupportedPartitions = ['aws-cn', 'aws-us-gov', 'aws-iso', 'aws-iso-b']; const currentRegion = RegionInfo.get(this.stack.resolve(this.stack.region)); if (currentRegion.partition) { return !unsupportedPartitions.includes(currentRegion.partition); } return true; } private renderTimeout(idleTimeout?: Duration, perRequestTimeout?: Duration): CfnService.TimeoutConfigurationProperty | undefined { if (!idleTimeout && !perRequestTimeout) return undefined; if (idleTimeout && idleTimeout.toMilliseconds() > 0 && idleTimeout.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { throw new Error(`idleTimeout must be at least 1 second or 0 to disable it, got ${idleTimeout.toMilliseconds()}ms.`); } if (perRequestTimeout && perRequestTimeout.toMilliseconds() > 0 && perRequestTimeout.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { throw new Error(`perRequestTimeout must be at least 1 second or 0 to disable it, got ${perRequestTimeout.toMilliseconds()}ms.`); } return { idleTimeoutSeconds: idleTimeout?.toSeconds(), perRequestTimeoutSeconds: perRequestTimeout?.toSeconds(), }; } } /** * The options to enabling AWS Cloud Map for an Amazon ECS service. */ export interface CloudMapOptions { /** * The name of the Cloud Map service to attach to the ECS service. * * @default CloudFormation-generated name */ readonly name?: string; /** * The service discovery namespace for the Cloud Map service to attach to the ECS service. * * @default - the defaultCloudMapNamespace associated to the cluster */ readonly cloudMapNamespace?: cloudmap.INamespace; /** * The DNS record type that you want AWS Cloud Map to create. The supported record types are A or SRV. * * @default - DnsRecordType.A if TaskDefinition.networkMode = AWS_VPC, otherwise DnsRecordType.SRV */ readonly dnsRecordType?: cloudmap.DnsRecordType.A | cloudmap.DnsRecordType.SRV; /** * The amount of time that you want DNS resolvers to cache the settings for this record. * * @default Duration.minutes(1) */ readonly dnsTtl?: Duration; /** * The number of 30-second intervals that you want Cloud Map to wait after receiving an UpdateInstanceCustomHealthStatus * request before it changes the health status of a service instance. * * NOTE: This is used for HealthCheckCustomConfig */ readonly failureThreshold?: number; /** * The container to point to for a SRV record. * @default - the task definition's default container */ readonly container?: ContainerDefinition; /** * The port to point to for a SRV record. * @default - the default port of the task definition's default container */ readonly containerPort?: number; } /** * The options for using a cloudmap service. */ export interface AssociateCloudMapServiceOptions { /** * The cloudmap service to register with. */ readonly service: cloudmap.IService; /** * The container to point to for a SRV record. * @default - the task definition's default container */ readonly container?: ContainerDefinition; /** * The port to point to for a SRV record. * @default - the default port of the task definition's default container */ readonly containerPort?: number; } /** * Service Registry for ECS service */ interface ServiceRegistry { /** * Arn of the Cloud Map Service that will register a Cloud Map Instance for your ECS Service */ readonly arn: string; /** * The container name value, already specified in the task definition, to be used for your service discovery service. * If the task definition that your service task specifies uses the bridge or host network mode, * you must specify a containerName and containerPort combination from the task definition. * If the task definition that your service task specifies uses the awsvpc network mode and a type SRV DNS record is * used, you must specify either a containerName and containerPort combination or a port value, but not both. */ readonly containerName?: string; /** * The container port value, already specified in the task definition, to be used for your service discovery service. * If the task definition that your service task specifies uses the bridge or host network mode, * you must specify a containerName and containerPort combination from the task definition. * If the task definition that your service task specifies uses the awsvpc network mode and a type SRV DNS record is * used, you must specify either a containerName and containerPort combination or a port value, but not both. */ readonly containerPort?: number; } /** * The launch type of an ECS service */ export enum LaunchType { /** * The service will be launched using the EC2 launch type */ EC2 = 'EC2', /** * The service will be launched using the FARGATE launch type */ FARGATE = 'FARGATE', /** * The service will be launched using the EXTERNAL launch type */ EXTERNAL = 'EXTERNAL', } /** * The deployment controller type to use for the service. */ export enum DeploymentControllerType { /** * The rolling update (ECS) deployment type involves replacing the current * running version of the container with the latest version. */ ECS = 'ECS', /** * The blue/green (CODE_DEPLOY) deployment type uses the blue/green deployment model powered by AWS CodeDeploy */ CODE_DEPLOY = 'CODE_DEPLOY', /** * The external (EXTERNAL) deployment type enables you to use any third-party deployment controller */ EXTERNAL = 'EXTERNAL', } /** * Propagate tags from either service or task definition */ export enum PropagatedTagSource { /** * Propagate tags from service */ SERVICE = 'SERVICE', /** * Propagate tags from task definition */ TASK_DEFINITION = 'TASK_DEFINITION', /** * Do not propagate */ NONE = 'NONE', } /** * Options for `determineContainerNameAndPort` */ interface DetermineContainerNameAndPortOptions { dnsRecordType: cloudmap.DnsRecordType; taskDefinition: TaskDefinition; container?: ContainerDefinition; containerPort?: number; } /** * Determine the name of the container and port to target for the service registry. */ function determineContainerNameAndPort(options: DetermineContainerNameAndPortOptions) { // If the record type is SRV, then provide the containerName and containerPort to target. // We use the name of the default container and the default port of the default container // unless the user specifies otherwise. if (options.dnsRecordType === cloudmap.DnsRecordType.SRV) { // Ensure the user-provided container is from the right task definition. if (options.container && options.container.taskDefinition != options.taskDefinition) { throw new Error('Cannot add discovery for a container from another task definition'); } const container = options.container ?? options.taskDefinition.defaultContainer!; // Ensure that any port given by the user is mapped. if (options.containerPort && !container.portMappings.some(mapping => mapping.containerPort === options.containerPort)) { throw new Error('Cannot add discovery for a container port that has not been mapped'); } return { containerName: container.containerName, containerPort: options.containerPort ?? options.taskDefinition.defaultContainer!.containerPort, }; } return {}; }