Package: cdk-ecs-service-extensions
CDK Construct library for building ECS services---This library provides a high level, extensible pattern for constructing services
deployed using Amazon ECS.import{AppMeshExtension,CloudwatchAgentExtension,Container,Environment,FireLensExtension,HttpLoadBalancerExtension,Service,ServiceDescription,XRayExtension,}from'cdk-ecs-service-extensions';TheServiceconstruct provided by this module can be extended with optionalServiceExtensionclasses that add supplemental ECS features including:AWS X-Rayfor tracing your applicationAmazon CloudWatch Agentfor capturing per task statsAWS AppMeshfor adding your application to a service meshApplication Load Balancer, for exposing your service to the publicAWS FireLens, for filtering and routing application logsInjecter Extension, for allowing your service connect to other AWS services by granting permission and injecting environment variablesQueue Extension, for allowing your service to consume messages from an SQS Queue which can be populated by one or more SNS Topics that it is subscribed toCommunity Extensions, providing support for advanced use casesTheServiceExtensionclass is an abstract class which you can also implement in
order to build your own custom service extensions for modifying your service, or
attaching your own custom resources or sidecars.Example//Createanenvironmenttodeployaservicein.constenvironment=newEnvironment(this,'production');//BuildouttheservicedescriptionconstnameDescription=newServiceDescription();nameDescription.add(newContainer({cpu:1024,memoryMiB:2048,trafficPort:80,image:ecs.ContainerImage.fromRegistry('nathanpeck/name'),environment:{PORT:'80',},}));declareconstmesh:appmesh.Mesh;nameDescription.add(newAppMeshExtension({mesh}));nameDescription.add(newFireLensExtension());nameDescription.add(newXRayExtension());nameDescription.add(newCloudwatchAgentExtension());nameDescription.add(newHttpLoadBalancerExtension());//Implementtheservicedescriptionasarealserviceinside//anenvironment.constnameService=newService(this,'name',{environment:environment,serviceDescription:nameDescription,});Creating anEnvironmentAnEnvironmentis a place to deploy your services. You can have multiple environments
on a single AWS account. For example, you could create atestenvironment as well
as aproductionenvironment so you have a place to verify that your application
works as intended before you deploy it to a live environment.Each environment is isolated from other environments. In other words,
when you create an environment, by default the construct supplies its own VPC,
ECS Cluster, and any other required resources for the environment:constenvironment=newEnvironment(this,'production');However, you can also choose to build an environment out of a pre-existing VPC
or ECS Cluster:declareconstvpc:ec2.Vpc;constcluster=newecs.Cluster(this,'Cluster',{vpc});constenvironment=newEnvironment(this,'production',{vpc,cluster,});Defining yourServiceDescriptionTheServiceDescriptiondefines what application you want the service to run and
what optional extensions you want to add to the service. The most basic form of aServiceDescriptionlooks like this:constnameDescription=newServiceDescription();nameDescription.add(newContainer({cpu:1024,memoryMiB:2048,trafficPort:80,image:ecs.ContainerImage.fromRegistry('nathanpeck/name'),environment:{PORT:'80',},}));EveryServiceDescriptionrequires at minimum that you add aContainerextension
which defines the main application (essential) container to run for the service.Logging usingawslogslog driverIf no observability extensions have been configured for a service, the ECS Service Extensions configures anawslogslog driver for the application container of the service to send the container logs to CloudWatch Logs.You can either provide a log group to theContainerextension or one will be created for you by the CDK.Following is an example of an application with anawslogslog driver configured for the application container:import*aslogsfrom'aws-cdk-lib/aws-logs';constenvironment=newEnvironment(this,'production');constnameDescription=newServiceDescription();nameDescription.add(newContainer({cpu:1024,memoryMiB:2048,trafficPort:80,image:ContainerImage.fromRegistry('nathanpeck/name'),environment:{PORT:'80',},logGroup:newlogs.LogGroup(this,'MyLogGroup'),}));If a log group is not provided, no observability extensions have been created, and theECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVERfeature flag is enabled, then logging will be configured by default and a log group will be created for you.TheECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVERfeature flag is enabled by default in any CDK apps that are created with CDK v1.140.0 or v2.8.0 and later.To enable default logging for previous versions, ensure that theECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVERflag within the application stack context is set to true, like so:import*ascxapifrom'@aws-cdk/cx-api';this.node.setContext(cxapi.ECS_SERVICE_EXTENSIONS_ENABLE_DEFAULT_LOG_DRIVER,true);Alternatively, you can also set the feature flag in thecdk.jsonfile. For more information, refer thedocs.After adding theContainerextension, you can optionally enable additional features for the service using theServiceDescription.add()method:declareconstmesh:appmesh.Mesh;constnameDescription=newServiceDescription();nameDescription.add(newAppMeshExtension({mesh}));nameDescription.add(newFireLensExtension());nameDescription.add(newXRayExtension());nameDescription.add(newCloudwatchAgentExtension());nameDescription.add(newHttpLoadBalancerExtension());nameDescription.add(newAssignPublicIpExtension());Launching theServiceDescriptionas aServiceOnce the service description is defined, you can launch it as a service:constenvironment=newEnvironment(this,'production');constnameDescription=newServiceDescription();constnameService=newService(this,'name',{environment:environment,serviceDescription:nameDescription,});At this point, all the service resources will be created. This includes the ECS Task
Definition, Service, as well as any other attached resources, such as App Mesh Virtual
Node or an Application Load Balancer.Creating your own taskRoleSometimes the taskRole should be defined outside of the service so that you can create strict resource policies (ie. S3 bucket policies) that are restricted to a given taskRole:consttaskRole=newiam.Role(this,'CustomTaskRole',{assumedBy:newiam.ServicePrincipal('ecs-tasks.amazonaws.com'),});//UsetaskRoleinanyCDKresourcepolicies//news3.BucketPolicy(this,'BucketPolicy,{});constenvironment=newEnvironment(this,'production');constnameDescription=newServiceDescription();constnameService=newService(this,'name',{environment:environment,serviceDescription:nameDescription,taskRole,});Task Auto-ScalingYou can configure the task count of a service to match demand. The recommended way of achieving this is to configure target tracking policies for your service which scales in and out in order to keep metrics around target values.You need to configure an auto scaling target for the service by setting theminTaskCount(defaults to 1) andmaxTaskCountin theServiceconstruct. Then you can specify target values for "CPU Utilization" or "Memory Utilization" across all tasks in your service. Note that thedesiredCountvalue will be set toundefinedif the auto scaling target is configured.If you want to configure auto-scaling policies based on resources like Application Load Balancer or SQS Queues, you can set the corresponding resource-specific fields in the extension. For example, you can enable target tracking scaling based on Application Load Balancer request count as follows:constenvironment=newEnvironment(this,'production');constserviceDescription=newServiceDescription();serviceDescription.add(newContainer({cpu:256,memoryMiB:512,trafficPort:80,image:ecs.ContainerImage.fromRegistry('my-alb'),}));//Addtheextensionwithtarget`requestsPerTarget`valuesetserviceDescription.add(newHttpLoadBalancerExtension({requestsPerTarget:10}));//ConfiguretheautoscalingtargetnewService(this,'my-service',{environment,serviceDescription,desiredCount:5,//Taskauto-scalingconstuctfortheserviceautoScaleTaskCount:{maxTaskCount:10,targetCpuUtilization:70,targetMemoryUtilization:50,},});You can also define your own service extensions forother auto-scaling policiesfor your service by making use of thescalableTaskCountattribute of theServiceclass.Creating your own customServiceExtensionIn addition to using the default service extensions that come with this module, you
can choose to implement your own custom service extensions. TheServiceExtensionclass is an abstract class you can implement yourself. The following example
implements a custom service extension that could be added to a service in order to
autoscale it based on scaling intervals of SQS Queue size:exportclassMyCustomAutoscalingextendsServiceExtension{constructor(){super('my-custom-autoscaling');//Scalingintervalsforthestepscalingpolicythis.scalingSteps=[{upper:0,change:-1},{lower:100,change:+1},{lower:500,change:+5}];this.sqsQueue=newsqs.Queue(this.scope,'my-queue');}//Thishookutilizestheresultingserviceconstruct//onceitiscreatedpublicuseService(service:ecs.Ec2Service|ecs.FargateService){this.parentService.scalableTaskCount.scaleOnMetric('QueueMessagesVisibleScaling',{metric:this.sqsQueue.metricApproximateNumberOfMessagesVisible(),scalingSteps:this.scalingSteps,});}}ThisServiceExtensioncan now be reused and added to any number of different
service descriptions. This allows you to develop reusable bits of configuration,
attach them to many different services, and centrally manage them. Updating theServiceExtensionin one place would update all services that use it, instead of
requiring decentralized updates to many different services.EveryServiceExtensioncan implement the following hooks to modify the properties
of constructs, or make use of the resulting constructs:addHooks()- This hook is called after all the extensions are added to a
ServiceDescription, but before any of the other extension hooks have been run.
It gives each extension a chance to do some inspection of the overall ServiceDescription
and see what other extensions have been added. Some extensions may want to register
hooks on the other extensions to modify them. For example, the Firelens extension
wants to be able to modify the settings of the application container to route logs
through Firelens.modifyTaskDefinitionProps()- This is hook is passed the proposed
ecs.TaskDefinitionProps for a TaskDefinition that is about to be created.
This allows the extension to make modifications to the task definition props
before the TaskDefinition is created. For example, the App Mesh extension modifies
the proxy settings for the task.useTaskDefinition()- After the TaskDefinition is created, this hook is
passed the actual TaskDefinition construct that was created. This allows the
extension to add containers to the task, modify the task definition's IAM role,
etc.resolveContainerDependencies()- Once all extensions have added their containers,
each extension is given a chance to modify its container'sdependsOnsettings.
Extensions need to check and see what other extensions were enabled and decide
whether their container needs to wait on another container to start first.modifyServiceProps()- Before an Ec2Service or FargateService is created, this
hook is passed a draft version of the service props to change. Each extension adds
its own modifications to the service properties. For example, the App Mesh extension
needs to modify the service settings to enable CloudMap service discovery.useService()- After the service is created, this hook is given a chance to
utilize that service. This is used by extensions like the load balancer or App Mesh
extension, which create and link other AWS resources to the ECS extension.connectToService()- This hook is called when a user wants to connect one service
to another service. It allows an extension to implement logic about how to allow
connections from one service to another. For example, the App Mesh extension implements
this method in order to easily connect one service mesh service to another, which
allows the service's Envoy proxy sidecars to route traffic to each other.Connecting servicesOne of the hooks that aServiceExtensioncan implement is a hook for connection
logic. This is utilized when connecting one service to another service, e.g.
connecting a user facing web service with a backend API. Usage looks like this:constfrontendDescription=newServiceDescription();constfrontend=newService(this,'frontend',{environment,serviceDescription:frontendDescription,});constbackendDescription=newServiceDescription();constbackend=newService(this,'backend',{environment,serviceDescription:backendDescription,});frontend.connectTo(backend);The address that a service will use to talk to another service depends on the
type of ingress that has been created by the extension that did the connecting.
For example, if an App Mesh extension has been used, then the service is accessible
at a DNS address of<service name>.<environment name>. For example:constenvironment=newEnvironment(this,'production');//DefinethefrontendtierconstfrontendDescription=newServiceDescription();frontendDescription.add(newContainer({cpu:1024,memoryMiB:2048,trafficPort:80,image:ecs.ContainerImage.fromRegistry('my-frontend-service'),environment:{BACKEND_URL:'http://backend.production',},}));constfrontend=newService(this,'frontend',{environment,serviceDescription:frontendDescription,});//DefinethebackendtierconstbackendDescription=newServiceDescription();backendDescription.add(newContainer({cpu:1024,memoryMiB:2048,trafficPort:80,image:ecs.ContainerImage.fromRegistry('my-backend-service'),environment:{FRONTEND_URL:'http://frontend.production',},}));constbackend=newService(this,'backend',{environment,serviceDescription:backendDescription,});//Connectthetwotierstoeachotherfrontend.connectTo(backend);The above code uses the well-known service discovery name for each
service, and passes it as an environment variable to the container so
that the container knows what address to use when communicating to
the other service.Importing a pre-existing clusterTo create an environment with a pre-existing cluster, you must import the cluster first, then useEnvironment.fromEnvironmentAttributes(). When a cluster is imported into an environment, the cluster is treated as immutable. As a result, no extension may modify the cluster to change a setting.declareconstcluster:ecs.Cluster;constenvironment=Environment.fromEnvironmentAttributes(this,'Environment',{capacityType:ecs.EnvironmentCapacityType.EC2,//or`FARGATE`cluster,});Injecter ExtensionThis service extension accepts a list ofInjectableresources. It grants access to these resources and adds the necessary environment variables to the tasks that are part of the service.For example, anInjectableTopicis an SNS Topic that grants permission to the task role and adds the topic ARN as an environment variable to the task definition.Publishing to SNS TopicsYou can use this extension to set up publishing permissions for SNS Topics.constnameDescription=newServiceDescription();nameDescription.add(newInjecterExtension({injectables:[newInjectableTopic({//SNSTopictheservicewillpublishtotopic:newsns.Topic(this,'my-topic'),})],}));Queue ExtensionThis service extension creates a default SQS QueueeventsQueuefor the service (if not provided) and optionally also accepts list ofISubscribableobjects that theeventsQueuecan subscribe to. The service extension creates the subscriptions and sets up permissions for the service to consume messages from the SQS Queue.Setting up SNS Topic Subscriptions for SQS QueuesYou can use this extension to set up SNS Topic subscriptions for theeventsQueue. To do this, create a new object of typeTopicSubscriptionfor every SNS Topic you want theeventsQueueto subscribe to and provide it as input to the service extension.constnameDescription=newServiceDescription();constmyServiceDescription=nameDescription.add(newQueueExtension({//Providelistoftopicsubscriptionsthatyouwantthe`eventsQueue`tosubscribetosubscriptions:[newTopicSubscription({topic:newsns.Topic(this,'my-topic'),}],}));//Toaccessthe`eventsQueue`fortheservice,usethe`eventsQueue`getterfortheextensionconstmyQueueExtension=myServiceDescription.extensions.queueasQueueExtension;constmyEventsQueue=myQueueExtension.eventsQueue;For setting up a topic-specific queue subscription, you can provide a custom queue in theTopicSubscriptionobject along with the SNS Topic. The extension will set up a topic subscription for the provided queue instead of the defaulteventsQueueof the service.declareconstmyEventsQueue:sqs.Queue;declareconstmyTopicQueue:sqs.Queue;constnameDescription=newServiceDescription();nameDescription.add(newQueueExtension({eventsQueue:myEventsQueue,subscriptions:[newTopicSubscription({topic:newsns.Topic(this,'my-topic'),//`myTopicQueue`willsubscribetothe`my-topic`insteadof`eventsQueue`topicSubscriptionQueue:{queue:myTopicQueue,},}],}));Configuring auto scaling based on SQS QueuesYou can scale your service up or down to maintain an acceptable queue latency by tracking the backlog per task. It configures a target tracking scaling policy with target value (acceptable backlog per task) calculated by dividing theacceptableLatencybymessageProcessingTime. For example, if the maximum acceptable latency for a message to be processed after its arrival in the SQS Queue is 10 mins and the average processing time for a task is 250 milliseconds per message, thenacceptableBacklogPerTask = 10 *  60 / 0.25 = 2400. Therefore, each queue can hold up to 2400 messages before the service starts to scale up. For this, a target tracking policy will be attached to the scaling target for your service with target value2400. For more information, please refer:https://docs.aws.amazon.com/autoscaling/ec2/userguide/as-using-sqs-queue.html.You can configure auto scaling based on SQS Queue for your service as follows:declareconstmyEventsQueue:sqs.Queue;declareconstmyTopicQueue:sqs.Queue;constnameDescription=newServiceDescription();nameDescription.add(newQueueExtension({eventsQueue:myEventsQueue,//Needtospecify`scaleOnLatency`toconfigureautoscalingbasedonSQSQueuescaleOnLatency:{acceptableLatency:Duration.minutes(10),messageProcessingTime:Duration.millis(250),},subscriptions:[newTopicSubscription({topic:newsns.Topic(this,'my-topic'),//`myTopicQueue`willsubscribetothe`my-topic`insteadof`eventsQueue`topicSubscriptionQueue:{queue:myTopicQueue,//Optionallyprovide`scaleOnLatency`forconfiguringseparateautoscalingfor`myTopicQueue`scaleOnLatency:{acceptableLatency:Duration.minutes(10),messageProcessingTime:Duration.millis(250),},},}],}));Publish/Subscribe Service PatternThePublish/Subscribe Service Patternis used for implementing asynchronous communication between services. It involves 'publisher' services emitting events to SNS Topics, which are passed to subscribed SQS queues and then consumed by 'worker' services.The following example adds theInjecterExtensionto aPublisherService which can publish events to an SNS Topic and adds theQueueExtensionto aWorkerService which can poll itseventsQueueto consume messages populated by the topic.constenvironment=newEnvironment(this,'production');constpubServiceDescription=newServiceDescription();pubServiceDescription.add(newContainer({cpu:256,memoryMiB:512,trafficPort:80,image:ecs.ContainerImage.fromRegistry('sns-publish'),}));constmyTopic=newsns.Topic(this,'myTopic');//Addthe`InjecterExtension`totheservicedescriptiontoallowpublishingeventsto`myTopic`pubServiceDescription.add(newInjecterExtension({injectables:[newInjectableTopic({topic:myTopic,}],}));//Createthe`Publisher`ServicenewService(this,'Publisher',{environment:environment,serviceDescription:pubServiceDescription,});constsubServiceDescription=newServiceDescription();subServiceDescription.add(newContainer({cpu:256,memoryMiB:512,trafficPort:80,image:ecs.ContainerImage.fromRegistry('sqs-reader'),}));//Addthe`QueueExtension`totheservicedescriptiontosubscribeto`myTopic`subServiceDescription.add(newQueueExtension({subscriptions:[newTopicSubscription({topic:myTopic,}],}));//Createthe`Worker`ServicenewService(this,'Worker',{environment:environment,serviceDescription:subServiceDescription,});Community ExtensionsWe encourage the development of Community Service Extensions that support
advanced features. Here are some useful extensions that we have reviewed:ListenerRulesExtensionfor more precise control over Application Load Balancer rulesPlease submit a pull request so that we can review your service extension and
list it here.