The aws:ecs/service:Service resource, part of the Pulumi AWS provider, defines an ECS service that maintains a desired count of running tasks, handles deployments, and integrates with load balancers and service discovery. This guide focuses on four capabilities: task placement and load balancing, deployment strategies, CloudWatch alarm integration, and Service Connect mesh.
ECS services depend on clusters and task definitions, and often reference IAM roles, load balancers, or Service Discovery namespaces. The examples are intentionally small. Combine them with your own cluster infrastructure, networking, and monitoring.
Run tasks with load balancing and placement rules
Most services run a fixed number of tasks behind a load balancer, with placement strategies that distribute tasks efficiently across availability zones or instances.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const mongo = new aws.ecs.Service("mongo", {
name: "mongodb",
cluster: fooAwsEcsCluster.id,
taskDefinition: mongoAwsEcsTaskDefinition.arn,
desiredCount: 3,
iamRole: fooAwsIamRole.arn,
orderedPlacementStrategies: [{
type: "binpack",
field: "cpu",
}],
loadBalancers: [{
targetGroupArn: fooAwsLbTargetGroup.arn,
containerName: "mongo",
containerPort: 8080,
}],
placementConstraints: [{
type: "memberOf",
expression: "attribute:ecs.availability-zone in [us-west-2a, us-west-2b]",
}],
}, {
dependsOn: [foo],
});
import pulumi
import pulumi_aws as aws
mongo = aws.ecs.Service("mongo",
name="mongodb",
cluster=foo_aws_ecs_cluster["id"],
task_definition=mongo_aws_ecs_task_definition["arn"],
desired_count=3,
iam_role=foo_aws_iam_role["arn"],
ordered_placement_strategies=[{
"type": "binpack",
"field": "cpu",
}],
load_balancers=[{
"target_group_arn": foo_aws_lb_target_group["arn"],
"container_name": "mongo",
"container_port": 8080,
}],
placement_constraints=[{
"type": "memberOf",
"expression": "attribute:ecs.availability-zone in [us-west-2a, us-west-2b]",
}],
opts = pulumi.ResourceOptions(depends_on=[foo]))
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ecs.NewService(ctx, "mongo", &ecs.ServiceArgs{
Name: pulumi.String("mongodb"),
Cluster: pulumi.Any(fooAwsEcsCluster.Id),
TaskDefinition: pulumi.Any(mongoAwsEcsTaskDefinition.Arn),
DesiredCount: pulumi.Int(3),
IamRole: pulumi.Any(fooAwsIamRole.Arn),
OrderedPlacementStrategies: ecs.ServiceOrderedPlacementStrategyArray{
&ecs.ServiceOrderedPlacementStrategyArgs{
Type: pulumi.String("binpack"),
Field: pulumi.String("cpu"),
},
},
LoadBalancers: ecs.ServiceLoadBalancerArray{
&ecs.ServiceLoadBalancerArgs{
TargetGroupArn: pulumi.Any(fooAwsLbTargetGroup.Arn),
ContainerName: pulumi.String("mongo"),
ContainerPort: pulumi.Int(8080),
},
},
PlacementConstraints: ecs.ServicePlacementConstraintArray{
&ecs.ServicePlacementConstraintArgs{
Type: pulumi.String("memberOf"),
Expression: pulumi.String("attribute:ecs.availability-zone in [us-west-2a, us-west-2b]"),
},
},
}, pulumi.DependsOn([]pulumi.Resource{
foo,
}))
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var mongo = new Aws.Ecs.Service("mongo", new()
{
Name = "mongodb",
Cluster = fooAwsEcsCluster.Id,
TaskDefinition = mongoAwsEcsTaskDefinition.Arn,
DesiredCount = 3,
IamRole = fooAwsIamRole.Arn,
OrderedPlacementStrategies = new[]
{
new Aws.Ecs.Inputs.ServiceOrderedPlacementStrategyArgs
{
Type = "binpack",
Field = "cpu",
},
},
LoadBalancers = new[]
{
new Aws.Ecs.Inputs.ServiceLoadBalancerArgs
{
TargetGroupArn = fooAwsLbTargetGroup.Arn,
ContainerName = "mongo",
ContainerPort = 8080,
},
},
PlacementConstraints = new[]
{
new Aws.Ecs.Inputs.ServicePlacementConstraintArgs
{
Type = "memberOf",
Expression = "attribute:ecs.availability-zone in [us-west-2a, us-west-2b]",
},
},
}, new CustomResourceOptions
{
DependsOn =
{
foo,
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import com.pulumi.aws.ecs.inputs.ServiceOrderedPlacementStrategyArgs;
import com.pulumi.aws.ecs.inputs.ServiceLoadBalancerArgs;
import com.pulumi.aws.ecs.inputs.ServicePlacementConstraintArgs;
import com.pulumi.resources.CustomResourceOptions;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var mongo = new Service("mongo", ServiceArgs.builder()
.name("mongodb")
.cluster(fooAwsEcsCluster.id())
.taskDefinition(mongoAwsEcsTaskDefinition.arn())
.desiredCount(3)
.iamRole(fooAwsIamRole.arn())
.orderedPlacementStrategies(ServiceOrderedPlacementStrategyArgs.builder()
.type("binpack")
.field("cpu")
.build())
.loadBalancers(ServiceLoadBalancerArgs.builder()
.targetGroupArn(fooAwsLbTargetGroup.arn())
.containerName("mongo")
.containerPort(8080)
.build())
.placementConstraints(ServicePlacementConstraintArgs.builder()
.type("memberOf")
.expression("attribute:ecs.availability-zone in [us-west-2a, us-west-2b]")
.build())
.build(), CustomResourceOptions.builder()
.dependsOn(foo)
.build());
}
}
resources:
mongo:
type: aws:ecs:Service
properties:
name: mongodb
cluster: ${fooAwsEcsCluster.id}
taskDefinition: ${mongoAwsEcsTaskDefinition.arn}
desiredCount: 3
iamRole: ${fooAwsIamRole.arn}
orderedPlacementStrategies:
- type: binpack
field: cpu
loadBalancers:
- targetGroupArn: ${fooAwsLbTargetGroup.arn}
containerName: mongo
containerPort: 8080
placementConstraints:
- type: memberOf
expression: attribute:ecs.availability-zone in [us-west-2a, us-west-2b]
options:
dependsOn:
- ${foo}
The service maintains desiredCount tasks running the specified taskDefinition. The loadBalancers block connects tasks to a target group, routing traffic to the containerPort. The orderedPlacementStrategies array controls how ECS distributes tasks; here, binpack on CPU packs tasks onto instances to minimize resource waste. The placementConstraints array restricts where tasks can run, limiting them to specific availability zones.
Run one task per instance with daemon scheduling
Infrastructure services like log collectors need exactly one task on every container instance, automatically scaling as the cluster grows or shrinks.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const bar = new aws.ecs.Service("bar", {
name: "bar",
cluster: foo.id,
taskDefinition: barAwsEcsTaskDefinition.arn,
schedulingStrategy: "DAEMON",
});
import pulumi
import pulumi_aws as aws
bar = aws.ecs.Service("bar",
name="bar",
cluster=foo["id"],
task_definition=bar_aws_ecs_task_definition["arn"],
scheduling_strategy="DAEMON")
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ecs.NewService(ctx, "bar", &ecs.ServiceArgs{
Name: pulumi.String("bar"),
Cluster: pulumi.Any(foo.Id),
TaskDefinition: pulumi.Any(barAwsEcsTaskDefinition.Arn),
SchedulingStrategy: pulumi.String("DAEMON"),
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var bar = new Aws.Ecs.Service("bar", new()
{
Name = "bar",
Cluster = foo.Id,
TaskDefinition = barAwsEcsTaskDefinition.Arn,
SchedulingStrategy = "DAEMON",
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var bar = new Service("bar", ServiceArgs.builder()
.name("bar")
.cluster(foo.id())
.taskDefinition(barAwsEcsTaskDefinition.arn())
.schedulingStrategy("DAEMON")
.build());
}
}
resources:
bar:
type: aws:ecs:Service
properties:
name: bar
cluster: ${foo.id}
taskDefinition: ${barAwsEcsTaskDefinition.arn}
schedulingStrategy: DAEMON
The schedulingStrategy of DAEMON tells ECS to run one task per instance. You cannot specify desiredCount with DAEMON; ECS manages the count automatically based on cluster size. This pattern works for monitoring agents, log forwarders, or any service that needs host-level presence.
Monitor deployments with CloudWatch alarms
Production deployments integrate CloudWatch alarms to detect failures and automatically roll back when metrics breach thresholds.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.ecs.Service("example", {
name: "example",
cluster: exampleAwsEcsCluster.id,
alarms: {
enable: true,
rollback: true,
alarmNames: [exampleAwsCloudwatchMetricAlarm.alarmName],
},
});
import pulumi
import pulumi_aws as aws
example = aws.ecs.Service("example",
name="example",
cluster=example_aws_ecs_cluster["id"],
alarms={
"enable": True,
"rollback": True,
"alarm_names": [example_aws_cloudwatch_metric_alarm["alarmName"]],
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ecs.NewService(ctx, "example", &ecs.ServiceArgs{
Name: pulumi.String("example"),
Cluster: pulumi.Any(exampleAwsEcsCluster.Id),
Alarms: &ecs.ServiceAlarmsArgs{
Enable: pulumi.Bool(true),
Rollback: pulumi.Bool(true),
AlarmNames: pulumi.StringArray{
exampleAwsCloudwatchMetricAlarm.AlarmName,
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.Ecs.Service("example", new()
{
Name = "example",
Cluster = exampleAwsEcsCluster.Id,
Alarms = new Aws.Ecs.Inputs.ServiceAlarmsArgs
{
Enable = true,
Rollback = true,
AlarmNames = new[]
{
exampleAwsCloudwatchMetricAlarm.AlarmName,
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import com.pulumi.aws.ecs.inputs.ServiceAlarmsArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new Service("example", ServiceArgs.builder()
.name("example")
.cluster(exampleAwsEcsCluster.id())
.alarms(ServiceAlarmsArgs.builder()
.enable(true)
.rollback(true)
.alarmNames(exampleAwsCloudwatchMetricAlarm.alarmName())
.build())
.build());
}
}
resources:
example:
type: aws:ecs:Service
properties:
name: example
cluster: ${exampleAwsEcsCluster.id}
alarms:
enable: true
rollback: true
alarmNames:
- ${exampleAwsCloudwatchMetricAlarm.alarmName}
The alarms block enables CloudWatch integration. When enable is true and rollback is true, ECS monitors the specified alarmNames during deployment. If any alarm enters ALARM state, ECS automatically rolls back to the previous task definition version.
Delegate deployment control to external tools
Teams using CodeDeploy or custom orchestration can configure ECS to hand off deployment control entirely.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.ecs.Service("example", {
name: "example",
cluster: exampleAwsEcsCluster.id,
deploymentController: {
type: "EXTERNAL",
},
});
import pulumi
import pulumi_aws as aws
example = aws.ecs.Service("example",
name="example",
cluster=example_aws_ecs_cluster["id"],
deployment_controller={
"type": "EXTERNAL",
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ecs.NewService(ctx, "example", &ecs.ServiceArgs{
Name: pulumi.String("example"),
Cluster: pulumi.Any(exampleAwsEcsCluster.Id),
DeploymentController: &ecs.ServiceDeploymentControllerArgs{
Type: pulumi.String("EXTERNAL"),
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.Ecs.Service("example", new()
{
Name = "example",
Cluster = exampleAwsEcsCluster.Id,
DeploymentController = new Aws.Ecs.Inputs.ServiceDeploymentControllerArgs
{
Type = "EXTERNAL",
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import com.pulumi.aws.ecs.inputs.ServiceDeploymentControllerArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new Service("example", ServiceArgs.builder()
.name("example")
.cluster(exampleAwsEcsCluster.id())
.deploymentController(ServiceDeploymentControllerArgs.builder()
.type("EXTERNAL")
.build())
.build());
}
}
resources:
example:
type: aws:ecs:Service
properties:
name: example
cluster: ${exampleAwsEcsCluster.id}
deploymentController:
type: EXTERNAL
The deploymentController type of EXTERNAL tells ECS not to manage deployments. You must specify taskDefinition when creating the service, but updates to taskDefinition are ignored. The external controller (CodeDeploy, custom tooling) handles all deployment lifecycle operations.
Roll out updates gradually with linear deployment
Linear deployments shift traffic in fixed percentage increments, allowing validation between steps.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.ecs.Service("example", {
name: "example",
cluster: exampleAwsEcsCluster.id,
deploymentConfiguration: {
strategy: "LINEAR",
bakeTimeInMinutes: "10",
linearConfiguration: {
stepPercent: 25,
stepBakeTimeInMinutes: "5",
},
},
});
import pulumi
import pulumi_aws as aws
example = aws.ecs.Service("example",
name="example",
cluster=example_aws_ecs_cluster["id"],
deployment_configuration={
"strategy": "LINEAR",
"bake_time_in_minutes": "10",
"linear_configuration": {
"step_percent": 25,
"step_bake_time_in_minutes": "5",
},
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ecs.NewService(ctx, "example", &ecs.ServiceArgs{
Name: pulumi.String("example"),
Cluster: pulumi.Any(exampleAwsEcsCluster.Id),
DeploymentConfiguration: &ecs.ServiceDeploymentConfigurationArgs{
Strategy: pulumi.String("LINEAR"),
BakeTimeInMinutes: pulumi.String("10"),
LinearConfiguration: &ecs.ServiceDeploymentConfigurationLinearConfigurationArgs{
StepPercent: pulumi.Float64(25),
StepBakeTimeInMinutes: pulumi.String("5"),
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.Ecs.Service("example", new()
{
Name = "example",
Cluster = exampleAwsEcsCluster.Id,
DeploymentConfiguration = new Aws.Ecs.Inputs.ServiceDeploymentConfigurationArgs
{
Strategy = "LINEAR",
BakeTimeInMinutes = "10",
LinearConfiguration = new Aws.Ecs.Inputs.ServiceDeploymentConfigurationLinearConfigurationArgs
{
StepPercent = 25,
StepBakeTimeInMinutes = "5",
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import com.pulumi.aws.ecs.inputs.ServiceDeploymentConfigurationArgs;
import com.pulumi.aws.ecs.inputs.ServiceDeploymentConfigurationLinearConfigurationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new Service("example", ServiceArgs.builder()
.name("example")
.cluster(exampleAwsEcsCluster.id())
.deploymentConfiguration(ServiceDeploymentConfigurationArgs.builder()
.strategy("LINEAR")
.bakeTimeInMinutes("10")
.linearConfiguration(ServiceDeploymentConfigurationLinearConfigurationArgs.builder()
.stepPercent(25.0)
.stepBakeTimeInMinutes("5")
.build())
.build())
.build());
}
}
resources:
example:
type: aws:ecs:Service
properties:
name: example
cluster: ${exampleAwsEcsCluster.id}
deploymentConfiguration:
strategy: LINEAR
bakeTimeInMinutes: 10
linearConfiguration:
stepPercent: 25
stepBakeTimeInMinutes: 5
The deploymentConfiguration strategy of LINEAR enables incremental rollouts. The linearConfiguration defines stepPercent (how much traffic shifts per step) and stepBakeTimeInMinutes (how long to wait between steps). The top-level bakeTimeInMinutes sets the initial observation period before starting the rollout.
Test new versions with canary deployment
Canary deployments route a small percentage of traffic to the new version first, detecting issues before full rollout.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const example = new aws.ecs.Service("example", {
name: "example",
cluster: exampleAwsEcsCluster.id,
deploymentConfiguration: {
strategy: "CANARY",
bakeTimeInMinutes: "15",
canaryConfiguration: {
canaryPercent: 10,
canaryBakeTimeInMinutes: "5",
},
},
});
import pulumi
import pulumi_aws as aws
example = aws.ecs.Service("example",
name="example",
cluster=example_aws_ecs_cluster["id"],
deployment_configuration={
"strategy": "CANARY",
"bake_time_in_minutes": "15",
"canary_configuration": {
"canary_percent": 10,
"canary_bake_time_in_minutes": "5",
},
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
_, err := ecs.NewService(ctx, "example", &ecs.ServiceArgs{
Name: pulumi.String("example"),
Cluster: pulumi.Any(exampleAwsEcsCluster.Id),
DeploymentConfiguration: &ecs.ServiceDeploymentConfigurationArgs{
Strategy: pulumi.String("CANARY"),
BakeTimeInMinutes: pulumi.String("15"),
CanaryConfiguration: &ecs.ServiceDeploymentConfigurationCanaryConfigurationArgs{
CanaryPercent: pulumi.Float64(10),
CanaryBakeTimeInMinutes: pulumi.String("5"),
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var example = new Aws.Ecs.Service("example", new()
{
Name = "example",
Cluster = exampleAwsEcsCluster.Id,
DeploymentConfiguration = new Aws.Ecs.Inputs.ServiceDeploymentConfigurationArgs
{
Strategy = "CANARY",
BakeTimeInMinutes = "15",
CanaryConfiguration = new Aws.Ecs.Inputs.ServiceDeploymentConfigurationCanaryConfigurationArgs
{
CanaryPercent = 10,
CanaryBakeTimeInMinutes = "5",
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import com.pulumi.aws.ecs.inputs.ServiceDeploymentConfigurationArgs;
import com.pulumi.aws.ecs.inputs.ServiceDeploymentConfigurationCanaryConfigurationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var example = new Service("example", ServiceArgs.builder()
.name("example")
.cluster(exampleAwsEcsCluster.id())
.deploymentConfiguration(ServiceDeploymentConfigurationArgs.builder()
.strategy("CANARY")
.bakeTimeInMinutes("15")
.canaryConfiguration(ServiceDeploymentConfigurationCanaryConfigurationArgs.builder()
.canaryPercent(10.0)
.canaryBakeTimeInMinutes("5")
.build())
.build())
.build());
}
}
resources:
example:
type: aws:ecs:Service
properties:
name: example
cluster: ${exampleAwsEcsCluster.id}
deploymentConfiguration:
strategy: CANARY
bakeTimeInMinutes: 15
canaryConfiguration:
canaryPercent: 10
canaryBakeTimeInMinutes: 5
The CANARY strategy sends canaryPercent of traffic to the new version initially. After canaryBakeTimeInMinutes, if no issues are detected, the remaining traffic shifts. The top-level bakeTimeInMinutes sets the initial observation period.
Enable service mesh with Service Connect
Service Connect provides service discovery and mesh capabilities, allowing tasks to communicate using logical names while ECS handles routing and observability.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
const exampleLogGroup = new aws.cloudwatch.LogGroup("example", {name: "/ecs/example/service-connect"});
const current = aws.getRegion({});
const example = new aws.ecs.Service("example", {
name: "example",
cluster: exampleAwsEcsCluster.id,
taskDefinition: exampleAwsEcsTaskDefinition.arn,
desiredCount: 1,
serviceConnectConfiguration: {
enabled: true,
namespace: exampleAwsServiceDiscoveryHttpNamespace.arn,
logConfiguration: {
logDriver: "awslogs",
options: {
"awslogs-group": exampleLogGroup.name,
"awslogs-region": current.then(current => current.name),
"awslogs-stream-prefix": "service-connect",
},
},
accessLogConfiguration: {
format: "TEXT",
includeQueryParameters: "ENABLED",
},
services: [{
portName: "http",
discoveryName: "example",
clientAlias: {
dnsName: "example",
port: 8080,
},
}],
},
});
import pulumi
import pulumi_aws as aws
example_log_group = aws.cloudwatch.LogGroup("example", name="/ecs/example/service-connect")
current = aws.get_region()
example = aws.ecs.Service("example",
name="example",
cluster=example_aws_ecs_cluster["id"],
task_definition=example_aws_ecs_task_definition["arn"],
desired_count=1,
service_connect_configuration={
"enabled": True,
"namespace": example_aws_service_discovery_http_namespace["arn"],
"log_configuration": {
"log_driver": "awslogs",
"options": {
"awslogs-group": example_log_group.name,
"awslogs-region": current.name,
"awslogs-stream-prefix": "service-connect",
},
},
"access_log_configuration": {
"format": "TEXT",
"include_query_parameters": "ENABLED",
},
"services": [{
"port_name": "http",
"discovery_name": "example",
"client_alias": {
"dnsName": "example",
"port": 8080,
},
}],
})
package main
import (
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/cloudwatch"
"github.com/pulumi/pulumi-aws/sdk/v7/go/aws/ecs"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
exampleLogGroup, err := cloudwatch.NewLogGroup(ctx, "example", &cloudwatch.LogGroupArgs{
Name: pulumi.String("/ecs/example/service-connect"),
})
if err != nil {
return err
}
current, err := aws.GetRegion(ctx, &aws.GetRegionArgs{}, nil)
if err != nil {
return err
}
_, err = ecs.NewService(ctx, "example", &ecs.ServiceArgs{
Name: pulumi.String("example"),
Cluster: pulumi.Any(exampleAwsEcsCluster.Id),
TaskDefinition: pulumi.Any(exampleAwsEcsTaskDefinition.Arn),
DesiredCount: pulumi.Int(1),
ServiceConnectConfiguration: &ecs.ServiceServiceConnectConfigurationArgs{
Enabled: pulumi.Bool(true),
Namespace: pulumi.Any(exampleAwsServiceDiscoveryHttpNamespace.Arn),
LogConfiguration: &ecs.ServiceServiceConnectConfigurationLogConfigurationArgs{
LogDriver: pulumi.String("awslogs"),
Options: pulumi.StringMap{
"awslogs-group": exampleLogGroup.Name,
"awslogs-region": pulumi.String(current.Name),
"awslogs-stream-prefix": pulumi.String("service-connect"),
},
},
AccessLogConfiguration: &ecs.ServiceServiceConnectConfigurationAccessLogConfigurationArgs{
Format: pulumi.String("TEXT"),
IncludeQueryParameters: pulumi.String("ENABLED"),
},
Services: ecs.ServiceServiceConnectConfigurationServiceArray{
&ecs.ServiceServiceConnectConfigurationServiceArgs{
PortName: pulumi.String("http"),
DiscoveryName: pulumi.String("example"),
ClientAlias: ecs.ServiceServiceConnectConfigurationServiceClientAliasArray{
DnsName: "example",
Port: 8080,
},
},
},
},
})
if err != nil {
return err
}
return nil
})
}
using System.Collections.Generic;
using System.Linq;
using Pulumi;
using Aws = Pulumi.Aws;
return await Deployment.RunAsync(() =>
{
var exampleLogGroup = new Aws.CloudWatch.LogGroup("example", new()
{
Name = "/ecs/example/service-connect",
});
var current = Aws.GetRegion.Invoke();
var example = new Aws.Ecs.Service("example", new()
{
Name = "example",
Cluster = exampleAwsEcsCluster.Id,
TaskDefinition = exampleAwsEcsTaskDefinition.Arn,
DesiredCount = 1,
ServiceConnectConfiguration = new Aws.Ecs.Inputs.ServiceServiceConnectConfigurationArgs
{
Enabled = true,
Namespace = exampleAwsServiceDiscoveryHttpNamespace.Arn,
LogConfiguration = new Aws.Ecs.Inputs.ServiceServiceConnectConfigurationLogConfigurationArgs
{
LogDriver = "awslogs",
Options =
{
{ "awslogs-group", exampleLogGroup.Name },
{ "awslogs-region", current.Apply(getRegionResult => getRegionResult.Name) },
{ "awslogs-stream-prefix", "service-connect" },
},
},
AccessLogConfiguration = new Aws.Ecs.Inputs.ServiceServiceConnectConfigurationAccessLogConfigurationArgs
{
Format = "TEXT",
IncludeQueryParameters = "ENABLED",
},
Services = new[]
{
new Aws.Ecs.Inputs.ServiceServiceConnectConfigurationServiceArgs
{
PortName = "http",
DiscoveryName = "example",
ClientAlias =
{
{ "dnsName", "example" },
{ "port", 8080 },
},
},
},
},
});
});
package generated_program;
import com.pulumi.Context;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.aws.cloudwatch.LogGroup;
import com.pulumi.aws.cloudwatch.LogGroupArgs;
import com.pulumi.aws.AwsFunctions;
import com.pulumi.aws.inputs.GetRegionArgs;
import com.pulumi.aws.ecs.Service;
import com.pulumi.aws.ecs.ServiceArgs;
import com.pulumi.aws.ecs.inputs.ServiceServiceConnectConfigurationArgs;
import com.pulumi.aws.ecs.inputs.ServiceServiceConnectConfigurationLogConfigurationArgs;
import com.pulumi.aws.ecs.inputs.ServiceServiceConnectConfigurationAccessLogConfigurationArgs;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.io.File;
import java.nio.file.Files;
import java.nio.file.Paths;
public class App {
public static void main(String[] args) {
Pulumi.run(App::stack);
}
public static void stack(Context ctx) {
var exampleLogGroup = new LogGroup("exampleLogGroup", LogGroupArgs.builder()
.name("/ecs/example/service-connect")
.build());
final var current = AwsFunctions.getRegion(GetRegionArgs.builder()
.build());
var example = new Service("example", ServiceArgs.builder()
.name("example")
.cluster(exampleAwsEcsCluster.id())
.taskDefinition(exampleAwsEcsTaskDefinition.arn())
.desiredCount(1)
.serviceConnectConfiguration(ServiceServiceConnectConfigurationArgs.builder()
.enabled(true)
.namespace(exampleAwsServiceDiscoveryHttpNamespace.arn())
.logConfiguration(ServiceServiceConnectConfigurationLogConfigurationArgs.builder()
.logDriver("awslogs")
.options(Map.ofEntries(
Map.entry("awslogs-group", exampleLogGroup.name()),
Map.entry("awslogs-region", current.name()),
Map.entry("awslogs-stream-prefix", "service-connect")
))
.build())
.accessLogConfiguration(ServiceServiceConnectConfigurationAccessLogConfigurationArgs.builder()
.format("TEXT")
.includeQueryParameters("ENABLED")
.build())
.services(ServiceServiceConnectConfigurationServiceArgs.builder()
.portName("http")
.discoveryName("example")
.clientAlias(ServiceServiceConnectConfigurationServiceClientAliasArgs.builder()
.dnsName("example")
.port(8080)
.build())
.build())
.build())
.build());
}
}
resources:
example:
type: aws:ecs:Service
properties:
name: example
cluster: ${exampleAwsEcsCluster.id}
taskDefinition: ${exampleAwsEcsTaskDefinition.arn}
desiredCount: 1
serviceConnectConfiguration:
enabled: true
namespace: ${exampleAwsServiceDiscoveryHttpNamespace.arn}
logConfiguration:
logDriver: awslogs
options:
awslogs-group: ${exampleLogGroup.name}
awslogs-region: ${current.name}
awslogs-stream-prefix: service-connect
accessLogConfiguration:
format: TEXT
includeQueryParameters: ENABLED
services:
- portName: http
discoveryName: example
clientAlias:
dnsName: example
port: 8080
exampleLogGroup:
type: aws:cloudwatch:LogGroup
name: example
properties:
name: /ecs/example/service-connect
variables:
current:
fn::invoke:
function: aws:getRegion
arguments: {}
The serviceConnectConfiguration enables the mesh. The namespace property points to a Service Discovery HTTP namespace. The services array defines how this service is discovered; clientAlias sets the DNS name and port other services use to connect. The logConfiguration sends Service Connect proxy logs to CloudWatch, and accessLogConfiguration controls request logging format.
Beyond these examples
These snippets focus on specific service-level features: load balancing and task placement, deployment strategies, CloudWatch alarm integration, and Service Connect mesh. They’re intentionally minimal rather than full application deployments.
The examples rely on pre-existing infrastructure such as ECS clusters and task definitions, IAM roles and load balancer target groups, and CloudWatch alarms or Service Discovery namespaces. They focus on configuring the service rather than provisioning everything around it.
To keep things focused, common service patterns are omitted, including:
- Network configuration for awsvpc mode (networkConfiguration)
- Capacity provider strategies for mixed EC2/Fargate
- Auto Scaling integration (ignoreChanges pattern shown but not Auto Scaling setup)
- Force redeployment triggers (shown in EX9 but not detailed)
These omissions are intentional: the goal is to illustrate how each service feature is wired, not provide drop-in deployment modules. See the ECS Service resource reference for all available configuration options.
Let's deploy AWS ECS Services
Get started with Pulumi Cloud, then follow our quick setup guide to deploy this infrastructure.
Try Pulumi Cloud for FREEFrequently Asked Questions
Common Issues & Gotchas
dependsOn to reference your aws.iam.RolePolicy resource to ensure proper deletion order.cluster, name, launchType, schedulingStrategy, and iamRole. Changing any of these requires replacing the service.orderedPlacementStrategies and placementConstraints take effect on the next task deployment. To apply them immediately, set forceNewDeployment=true.Deployment & Updates
forceNewDeployment=true and configure the triggers property with a map of values. When triggers values change, the service redeploys. For example, use triggers: { redeployment: "plantimestamp()" } to redeploy on every apply.sigintRollback=true and waitForSteadyState=true. This allows graceful cancellation with automatic rollback to the previous stable state. Only works with the ECS deployment controller.deploymentConfiguration. Each strategy supports different bake times and rollout percentages.ignoreChanges to set an initial desiredCount, then ignore external changes from Application Autoscaling or other tools.Scheduling & Capacity
desiredCount). DAEMON runs exactly one task per container instance. With DAEMON, don’t specify desiredCount.launchType (EC2, FARGATE, or EXTERNAL) and capacityProviderStrategy are mutually exclusive. Use one or the other, not both.Networking & Load Balancing
iamRole when using a load balancer, but only if your task definition doesn’t use awsvpc network mode. If using awsvpc, don’t specify this role.networkConfiguration is required for task definitions using awsvpc network mode to receive their own Elastic Network Interface. It’s not supported for other network modes.orderedPlacementStrategy blocks and up to 10 placementConstraints blocks per service.Using a different cloud?
Explore containers guides for other cloud providers: