Deploy AWS ECS Services

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 FREE

Frequently Asked Questions

Common Issues & Gotchas
Why is my service stuck in DRAINING state during deletion?
This happens when the IAM role policy is destroyed before the ECS service. Set dependsOn to reference your aws.iam.RolePolicy resource to ensure proper deletion order.
What properties can't I change after creating a service?
The following properties are immutable: cluster, name, launchType, schedulingStrategy, and iamRole. Changing any of these requires replacing the service.
Why aren't my placement strategy or constraint changes taking effect immediately?
Updates to orderedPlacementStrategies and placementConstraints take effect on the next task deployment. To apply them immediately, set forceNewDeployment=true.
Deployment & Updates
How do I force a redeployment of my service?
Set 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.
How do I enable rollback when canceling a deployment?
Set sigintRollback=true and waitForSteadyState=true. This allows graceful cancellation with automatic rollback to the previous stable state. Only works with the ECS deployment controller.
What deployment strategies are available?
You can configure LINEAR, CANARY, or BLUE_GREEN deployment strategies using deploymentConfiguration. Each strategy supports different bake times and rollout percentages.
How do I ignore autoscaling changes to desiredCount?
Use ignoreChanges to set an initial desiredCount, then ignore external changes from Application Autoscaling or other tools.
Scheduling & Capacity
What's the difference between REPLICA and DAEMON scheduling?
REPLICA (default) runs a specified number of tasks (desiredCount). DAEMON runs exactly one task per container instance. With DAEMON, don’t specify desiredCount.
Can I use DAEMON scheduling with Fargate?
No. DAEMON scheduling is not supported with FARGATE launch type or with CODE_DEPLOY or EXTERNAL deployment controllers.
What's the difference between launchType and capacityProviderStrategy?
launchType (EC2, FARGATE, or EXTERNAL) and capacityProviderStrategy are mutually exclusive. Use one or the other, not both.
Networking & Load Balancing
When do I need to specify an iamRole?
You need 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.
When is networkConfiguration required?
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.
What are the limits for placement strategies and constraints?
You can configure up to 5 orderedPlacementStrategy blocks and up to 10 placementConstraints blocks per service.

Using a different cloud?

Explore containers guides for other cloud providers: