Skip to main content

Service

The Service construct is a higher level CDK construct that simplifies the deployment of containerized applications. It provides a simple way to build and deploy your app to AWS with these features:

  • Deployment to an ECS Fargate cluster with an Application Load Balancer as the front end.
  • Auto-scaling based on CPU and memory utilization and per-container request count.
  • Directly referencing other AWS resources in your app.
  • Configuring custom domains for your website URL.

Quick Start

To create a service, set path to the directory that contains the Dockerfile.

import { Service } from "sst/constructs";

new Service(stack, "MyService", {
path: "./service",
port: 3000,
});

Here's an example of the Dockerfile for a simple Express app.

./service/Dockerfile
FROM node:18-bullseye-slim

COPY . /app
WORKDIR /app/

RUN npm install

ENTRYPOINT ["node", "app.mjs"]

And the app.mjs would look like this:

./service/app.mjs
import express from "express";
const app = express();

app.get("/", (req, res) => {
res.send("Hello world");
});

app.listen(3000);

The Docker container uses the Node.js 18 slim image in this instance, installs the dependencies specified in the package.json, and then starts the Express server.

When you run sst deploy, SST does a couple things:

  • Runs docker build to build the image
  • Uploads the image to Elastic Container Registry (ECR)
  • Creates a VPC if one is not provided
  • Launches an Elastic Container Service (ECS) cluster in the VPC
  • Creates a Fargate service to run the container image
  • Creates an Auto Scaling Group to auto-scale the cluster
  • Creates an Application Load Balancer (ALB) to route traffic to the cluster
  • Creates a CloudFront Distribution to allow configuration of caching and custom domains

Working locally

To work on your app locally with SST:

  1. Start SST in your project root.

    npx sst dev
  2. Then start your app.

    Navigate to the service directory, and run your app wrapped inside sst bind. For instance, if you're working with an Express app:

    cd service
    npx sst bind node app.mjs

    The sst bind command loads service resources, environment variables, and the IAM permissions granted to the service. Read more about sst bind here.

  3. Staring your app inside Docker

    If you need to run the your app inside Docker locally, pass the environment variables set by sst bind into the docker container.

    sst bind "env | grep -E 'SST_|AWS_' > .env.tmp && docker run --env-file .env.tmp my-image"

    This sequence fetches variables starting with SST_, saving them to the .env.tmp file, which is then used in the Docker run.

note

When running sst dev, SST does not deploy your app. It's meant to be run locally.


Configuring containers

Fargate supports a variety of CPU and memory combinations for the containers. The default size used is 0.25 vCPU and 512 MB. To configure it, do the following:

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cpu: "2 vCPU",
memory: "8 GB",
});

You may also configure the amount of ephemeral storage allocated to the task. The default is 20 GB. To configure it, do the following:

new Service(stack, "MyService", {
path: "./service",
port: 3000,
storage: "100 GB",
});

Auto-scaling

Your cluster can auto-scale as the traffic increases or decreases based on several metrics:

  • CPU utilization (default 70%)
  • Memory utilization (default 70%)
  • Per-container request count (default 500)

You can also set the minimum and maximum number of containers to which the cluster can scale.

Auto-scaling is disabled by default as both the minimum and maximum are set to 1.

To configure it:

new Service(stack, "MyService", {
path: "./service",
port: 3000,
scaling: {
minContainers: 4,
maxContainers: 16,
cpuUtilization: 50,
memoryUtilization: 50,
requestsPerContainers: 1000,
}
});

Custom domains

You can configure the service with a custom domain hosted either on Route 53 or externally.

new Service(stack, "MyService", {
path: "./service",
port: 3000,
customDomain: "my-app.com",
});

Note that visitors to http:// will be redirected to https://.

You can also configure an alias domain to point to the main domain. For instance, to set up www.my-app.com to redirect to my-app.com:

new Service(stack, "MyServiceSite", {
path: "./service",
port: 3000,
customDomain: {
domainName: "my-app.com",
domainAlias: "www.my-app.com",
},
});

Using AWS services

SST makes it very easy for your Service construct to access other resources in your AWS account. If you have an S3 bucket created using the Bucket construct, you can bind it to your app.

const bucket = new Bucket(stack, "Uploads");

new Service(stack, "MyService", {
path: "./service",
port: 3000,
bind: [bucket],
});

This will attach the necessary IAM permissions and allow your app to access the bucket via the typesafe sst/node client.

import { Bucket } from "sst/node/bucket";

console.log(Bucket.Uploads.bucketName);

Read more about this in the Resource Binding doc.


Private services

If you don't want your service to be publicly accessible, create a private service by disabling the Application Load Balancer and CloudFront distribution.

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
applicationLoadBalancer: false,
cloudfrontDistribution: false,
},
});

Using Nixpacks

If a Dockerfile is not found in the service's path, Nixpacks will be used to analyze the service code, and then generate a Dockerfile within .nixpacks. This file will build and run your application. Read more about customizing the Nixpacks builds.

note

The generated .nixpacks directory should be added to your .gitignore file.


Examples

Creating a Service

new Service(stack, "MyService", {
path: "./service",
port: 3000,
});

Using custom Dockerfile

new Service(stack, "MyService", {
path: "./service",
port: 3000,
file: "path/to/Dockerfile.prod",
});

Using existing ECR image

import { ContainerImage } from "aws-cdk-lib/aws-ecs";

const service = new Service(stack, "MyService", {
port: 3000,
cdk: {
container: {
image: ContainerImage.fromRegistry(
"public.ecr.aws/amazonlinux/amazonlinux:latest"
),
},
},
});

// Grants permissions to pull private images from the ECR registry
service.cdk?.taskDefinition.executionRole?.addManagedPolicy({
managedPolicyArn: "arn:aws:iam::aws:policy/AmazonECSTaskExecutionRolePolicy",
});

Configuring docker build

Here's an example of passing build args to the docker build command.

new Service(stack, "MyService", {
path: "./service",
port: 3000,
build: {
buildArgs: {
FOO: "bar"
}
}
});

Configuring log retention

The Service construct creates a CloudWatch log group to store the logs. By default, the logs are retained indefinitely. You can configure the log retention period like this:

new Service(stack, "MyService", {
path: "./service",
port: 3000,
logRetention: "one_week",
});

Configuring additional props

new Service(stack, "MyService", {
path: "./service",
port: 8080,
cpu: "2 vCPU",
memory: "8 GB",
scaling: {
minContainers: 4,
maxContainers: 16,
cpuUtilization: 50,
memoryUtilization: 50,
requestsPerContainers: 1000,
},
config: [STRIPE_KEY, API_URL],
permissions: ["ses", bucket],
});

Advanced examples

Configuring Fargate Service

Here's an example of configuring the circuit breaker for the Fargate service.

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
fargateService: {
circuitBreaker: { rollback: true },
},
},
});

Configuring Service Container

Here's an example of configuring the Fargate container health check. Make sure the curl command exists inside the container.

import { Duration } from "aws-cdk-lib/core";

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
container: {
healthCheck: {
command: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"],
interval: Duration.minutes(30),
retries: 20,
startPeriod: Duration.minutes(30),
timeout: Duration.minutes(30),
},
},
},
});

Configuring Application Load Balancer

Here's an example of configuring the Application Load Balancer subnets.

import { SubnetType, Vpc } from "aws-cdk-lib/aws-ec2";

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
applicationLoadBalancer: {
vpcSubnets: { subnetType: SubnetType.PUBLIC }
},
vpc: Vpc.fromLookup(stack, "VPC", {
vpcId: "vpc-xxxxxxxxxx",
}),
},
});

Configuring Application Load Balancer Target

Here's an example of configuring the Application Load Balancer health check.

import { Duration } from "aws-cdk-lib/core";

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
applicationLoadBalancerTargetGroup: {
healthCheck: {
healthyHttpCodes: "200, 302",
path: "/health",
},
},
},
});

Configuring Application Load Balancer HTTP to HTTPS redirect

Here's an example of redirecting HTTP requests to HTTPS.

import { Certificate } from "aws-cdk-lib/aws-certificatemanager";
import {
ApplicationProtocol,
ListenerAction,
ListenerCertificate,
} from "aws-cdk-lib/aws-elasticloadbalancingv2";

const service = new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
// Set default listener to be HTTPS
applicationLoadBalancerListener: {
protocol: ApplicationProtocol.HTTPS,
port: 443,
certificates: [ListenerCertificate.fromArn("arn:xxxxxxxxxx")],
},
},
});

// Add redirect listener
service.applicationLoadBalancer.addListener("HttpListener", {
protocol: ApplicationProtocol.HTTP,
defaultAction: ListenerAction.redirect({
protocol: "HTTPS",
host: "#{host}",
path: "/#{path}",
query: "#{query}",
port: "443",
statusCode: "HTTP_301",
}),
})

Using an existing VPC

import { Vpc } from "aws-cdk-lib/aws-ec2";

new Service(stack, "MyService", {
path: "./service",
port: 3000,
cdk: {
vpc: Vpc.fromLookup(stack, "VPC", {
vpcId: "vpc-xxxxxxxxxx",
}),
},
});

Sharing a Cluster

import { Cluster } from "aws-cdk-lib/aws-ecs";

const cluster = new Cluster(stack, "SharedCluster");

new Service(stack, "MyServiceA", {
path: "./service-a",
port: 3000,
cdk: { cluster },
});

new Service(stack, "MyServiceB", {
path: "./service-b",
port: 3000,
cdk: { cluster },
});

Constructor

new Service(scope, id, props)

Parameters

ServiceProps

architecture?

Type : "arm64" | "x86_64"

Default : "x86_64"

The CPU architecture of the container.

{
architecture: "arm64",
}

bind?

Type : Array<BindingResource>

Bind resources for the function

{
bind: [STRIPE_KEY, bucket],
}

build?

Type :

build.buildArgs?

Type : Record<string, string>

Default : No build args

Build args to pass to the docker build command.

{
build: {
buildArgs: {
FOO: "bar"
}
}
}

build.buildSsh?

Type : string

Default : No --ssh flag is passed to the build command

SSH agent socket or keys to pass to the docker build command. Docker BuildKit must be enabled to use the ssh flag

container: {
buildSsh: "default"
}

build.cacheFrom?

Type : Array<ServiceContainerCacheProps>

Default : No cache from options are passed to the build command

Cache from options to pass to the docker build command. DockerCacheOption[].

container: {
cacheFrom: [{ type: 'registry', params: { ref: 'ghcr.io/myorg/myimage:cache' }}],
}

build.cacheTo?

Type : ServiceContainerCacheProps

Default : No cache to options are passed to the build command

Cache to options to pass to the docker build command. DockerCacheOption[].

container: {
cacheTo: { type: 'registry', params: { ref: 'ghcr.io/myorg/myimage:cache', mode: 'max', compression: 'zstd' }},
}

cpu?

Type : "0.25 vCPU" | "0.5 vCPU" | "1 vCPU" | "2 vCPU" | "4 vCPU" | "8 vCPU" | "16 vCPU"

Default : "0.25 vCPU"

The amount of CPU allocated.

{
cpu: "1 vCPU",
}

customDomain?

Type : string | ServiceDomainProps

The customDomain for this service. SST supports domains that are hosted either on Route 53 or externally.

Note that you can also migrate externally hosted domains to Route 53 by following this guide.

{
customDomain: "domain.com",
}
{
customDomain: {
domainName: "domain.com",
domainAlias: "www.domain.com",
hostedZone: "domain.com"
}
}

dev?

Type :

dev.deploy?

Type : boolean

Default : false

When running sst dev, site is not deployed. This is to ensure sst dev` can start up quickly.

{
dev: {
deploy: true
}
}

dev.url?

Type : string

The local site URL when running sst dev .

{
dev: {
url: "http://localhost:3000"
}
}

environment?

Type : Record<string, string>

An object with the key being the environment variable name.

{
environment: {
API_URL: api.url,
USER_POOL_CLIENT: auth.cognitoUserPoolClient.userPoolClientId,
},
}

file?

Type : string

Default : "Dockerfile"

Path to Dockerfile relative to the defined "path".

logRetention?

Type : "one_day" | "three_days" | "five_days" | "one_week" | "two_weeks" | "one_month" | "two_months" | "three_months" | "four_months" | "five_months" | "six_months" | "one_year" | "thirteen_months" | "eighteen_months" | "two_years" | "three_years" | "five_years" | "six_years" | "seven_years" | "eight_years" | "nine_years" | "ten_years" | "infinite"

Default : Logs retained indefinitely

The duration logs are kept in CloudWatch Logs.

{
logRetention: "one_week"
}

memory?

Type : ${number} GB

Default : "0.5 GB"

The amount of memory allocated.

{
memory: "2 GB",
}

path?

Type : string

Default : "."

Path to the directory where the app is located.

permissions?

Type : Permissions

Attaches the given list of permissions to the SSR function. Configuring this property is equivalent to calling attachPermissions() after the site is created.

{
permissions: ["ses"]
}

port

Type : number

The port number on the container.

{
port: 8000,
}

scaling?

Type :

scaling.cpuUtilization?

Type : number

Default : 70

Scales in or out to achieve a target cpu utilization.

{
scaling: {
cpuUtilization: 50,
memoryUtilization: 50,
},
}

scaling.maxContainers?

Type : number

Default : 1

The maximum capacity for the cluster.

{
scaling: {
minContainers: 4,
maxContainers: 16,
},
}

scaling.memoryUtilization?

Type : number

Default : 70

Scales in or out to achieve a target memory utilization.

{
scaling: {
cpuUtilization: 50,
memoryUtilization: 50,
},
}

scaling.minContainers?

Type : number

Default : 1

The minimum capacity for the cluster.

{
scaling: {
minContainers: 4,
maxContainers: 16,
},
}

scaling.requestsPerContainer?

Type : number

Default : 500

Scales in or out to achieve a target request count per container.

{
scaling: {
requestsPerContainer: 1000,
},
}

storage?

Type : ${number} GB

Default : "20 GB"

The amount of ephemeral storage allocated, in GB.

{
storage: "100 GB",
}

waitForInvalidation?

Type : boolean

Default : false

While deploying, SST waits for the CloudFront cache invalidation process to finish. This ensures that the new content will be served once the deploy command finishes. However, this process can sometimes take more than 5 mins. For non-prod environments it might make sense to pass in false . That'll skip waiting for the cache to invalidate and speed up the deploy process.

{
waitForInvalidation: true
}

cdk?

Type :

cdk.applicationLoadBalancer?

Type : boolean | Omit<ApplicationLoadBalancerProps, "vpc">

Default : true

By default, SST creates an Application Load Balancer to distribute requests across containers. Set this to false to skip creating the load balancer.

{
cdk: {
applicationLoadBalancer: false
}
}

cdk.applicationLoadBalancerListener?

Type : BaseApplicationListenerProps

Customize the Application Load Balancer's target group.

{
cdk: {
applicationLoadBalancerListener: {
port: 8080
}
}
}

cdk.applicationLoadBalancerTargetGroup?

Type : ApplicationTargetGroupProps

Customize the Application Load Balancer's target group.

{
cdk: {
applicationLoadBalancerTargetGroup: {
healthCheck: {
path: "/health"
}
}
}
}

cdk.cachePolicy?

Type : ICachePolicy

By default, SST creates a CloudFront cache policy. Pass in a value to override the default policy.

import { CachePolicy } from "aws-cdk-lib/aws-cloudfront";

{
cdk: {
cachePolicy: CachePolicy.fromCachePolicyId(stack, "CachePolicy", "83da9c7e-98b4-4e11-a168-04f0df8e2c65"),
}
}

cdk.cloudfrontDistribution?

Type : boolean | ServiceCdkDistributionProps

Default : true

By default, SST creates a CloudFront distribution. Pass in a value to override the default settings this construct uses to create the CDK Distribution internally. Alternatively, set this to false to skip creating the distribution.

{
cdk: {
cloudfrontDistribution: false
}
}

cdk.cluster?

Type : ICluster

Create the service in an existing ECS cluster.

import { Cluster } from "aws-cdk-lib/aws-ecs";

{
cdk: {
cluster: Cluster.fromClusterArn(stack, "Cluster", "arn:aws:ecs:us-east-1:123456789012:cluster/my-cluster"),
}
}

cdk.container?

Type :

Customizing the container definition for the ECS task.

{
cdk: {
container: {
healthCheck: {
command: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
}
}
}
}

cdk.fargateService?

Type : Omit<FargateServiceProps, "cluster" | "taskDefinition">

Customize the Fargate Service.

{
cdk: {
fargateService: {
circuitBreaker: { rollback: true }
}
}
}

cdk.vpc?

Type : IVpc

Create the service in the specified VPC. Note this will only work once deployed.

import { Vpc } from "aws-cdk-lib/aws-ec2";

{
cdk: {
vpc: Vpc.fromLookup(stack, "VPC", {
vpcId: "vpc-xxxxxxxxxx",
}),
}
}

Properties

An instance of Service has the following properties.

customDomainUrl

Type : undefined | string

If the custom domain is enabled, this is the URL of the website with the custom domain.

id

Type : string

url

Type : undefined | string

The CloudFront URL of the website.

cdk

Type : undefined |

cdk.applicationLoadBalancer

Type : undefined | ApplicationLoadBalancer

cdk.certificate

Type : undefined | ICertificate

cdk.cluster

Type : undefined | ICluster

cdk.distribution

Type : undefined | IDistribution

cdk.fargateService

Type : undefined | FargateService

cdk.hostedZone

Type : undefined | IHostedZone

cdk.taskDefinition

Type : undefined | FargateTaskDefinition

cdk.vpc

Type : undefined | IVpc

The internally created CDK resources.

Methods

An instance of Service has the following methods.

addEnvironment

addEnvironment(name, value)

Parameters

  • name string
  • value string

Attaches additional environment variable to the service.

service.addEnvironment({
DEBUG: "*"
});

attachPermissions

attachPermissions(permissions)

Parameters

Attaches the given list of permissions to allow the service to access other AWS resources.

service.attachPermissions(["sns"]);

bind

bind(constructs)

Parameters

  • constructs Array<BindingResource>

Binds additional resources to service.

service.bind([STRIPE_KEY, bucket]);

ServiceDomainProps

alternateNames?

Type : Array<string>

Default : []

Specify additional names that should route to the Cloudfront Distribution. Note, certificates for these names will not be automatically generated so the certificate option must be specified.

domainAlias?

Type : string

Default : no alias configured

An alternative domain to be assigned to the website URL. Visitors to the alias will be redirected to the main domain. (ie. www.domain.com ).

Use this to create a www. version of your domain and redirect visitors to the root domain.

domainName

Type : string

The domain to be assigned to the website URL (ie. domain.com).

Supports domains that are hosted either on Route 53 or externally.

hostedZone?

Type : string

Default : same as the

The hosted zone in Route 53 that contains the domain. By default, SST will look for a hosted zone matching the domainName that's passed in.

Set this option if SST cannot find the hosted zone in Route 53.

isExternalDomain?

Type : boolean

Default : false

Set this option if the domain is not hosted on Amazon Route 53.

cdk?

Type :

cdk.certificate?

Type : ICertificate

Import the certificate for the domain. By default, SST will create a certificate with the domain name. The certificate will be created in the us-east-1 (N. Virginia) region as required by AWS CloudFront.

Set this option if you have an existing certificate in the us-east-1 region in AWS Certificate Manager you want to use.

cdk.hostedZone?

Type : IHostedZone

Import the underlying Route 53 hosted zone.

ServiceContainerCacheProps

ServiceCdkDistributionProps