Demystifying AWS CDK’s ECS Pattern
Tags: aws, typescript, awscdk, devops
Published: 2023-12-12 15:36:40

# Demystifying AWS CDK’s ECS Pattern

AWS CDK is my first choice when choosing an IaC for AWS (actually, it's a lie; I prefer [SST](actually it's a lie, it's [SST](https://medium.com/@jolodev/sst-24fd4e2f4d7c)). Using the same programming language for IaC and application development is convenient, allowing me to use the same tools without switching contexts.

Another powerful asset is L3-Constructs or `patterns`. L3- constructs simplify multiple resources for common tasks. One of them is the `ECS-Pattern`.

However, they abstract away many resources, and in this blog post, I am going to demystify the `ApplicationLoadBalancedFargateService` construct.

## `ApplicationLoadBalancedFargateService`

Let’s break down the verbose construct `ApplicationLoadBalancedFargateService` into their resources and what code you may need.

### Minimal

Below is the minimal code that deploys an Application Load Balancer, a Fargate ECS Cluster and task, Target Groups, and a Listener on port 80.  Depending on the deployment region, the code deploys a VPC and at least two private and public subnets.

```typescript
const service = new ApplicationLoadBalancedFargateService(stack, "AlbFargateService", {
    // You have to define a task image option
    taskImageOptions: {
      image: ContainerImage.fromAsset("path/to/your/Dockerfile"),
    },
  },
);
```

Or if you want to use your own `taskDefinition`:

```typescript
const service = new ApplicationLoadBalancedFargateService(stack, 'AlbFargateService', {
  // The task definition will mostlikely be bigger as you need to pass props
  taskDefinition: new FargateTaskDefinition(stack, 'TaskDefinition', {
    // here you will need add props which looks similar to above `taskImageOptions`
  })
})
```

That is what you will get. Out of simplicity, I left out the IAM roles and policies.

![Minimal Code for ALB ECS pattern](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/dsD_5ZshvhWzRm4W6wbhX.png)

When using the `taskDefinition`you often need to configure the container, which usually ends up in more code.

### HTTPS

However, the minimal code approach only listens on port 80 (HTTP). If you want to create a serious application using AWS ECS Fargate, you want HTTPS (port 443).

As a pre-requisite, you must have a [Hosted Zone](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/hosted-zones-working-with.html) and a domain name in your Route53.

```typescript
new ApplicationLoadBalancedFargateService(stack, "AlbFargateServiceHttps", {
  taskImageOptions: {
    image: ContainerImage.fromAsset("path/to/your/Dockerfile"),
  },
  publicLoadBalancer: true,
  redirectHTTP: true, // Best practice to redirect if calling URL with `http`
  cpu: 512, // <-- Default: 0.25 Otherwise container dies to quickly
  memoryLimitMiB: 1024, // <-- Otherwise container dies to quickly
  domainName: "my.example.com",
  domainZone: HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
    hostedZoneId: props.hostedZoneId,
    zoneName: props.zoneName,
  }),
});
```

And this is what it looks like

![ALB ECS Pattern with HTTPs](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/5Fn6cXrKt4EKA7oID33ow.png)

7 extra lines create a second listener on port 443, and the Application Load Balancer (ALB) terminates HTTPS with an AWS Certificate Manager (ACM) certificate. This certificate will be automatically created when adding `domainName` and `domainZone`.

Now, you will be able to access your container with `https://my.example.com`.

### Authentication with Cognito

Cognito is a great option if we want to add an authentication layer. This means we must authenticate with Cognito before loading data into the Docker container. To achieve this, we add an action to our listener that tells the user to authenticate before being directed to the container. 

```typescript
// Define the Service first
const service = new ApplicationLoadBalancedFargateService(
  stack,
  "AlbFargateService",
  {
    taskImageOptions: {
      image: ContainerImage.fromAsset("path/to/your/Dockerfile"),
    },
    publicLoadBalancer: true,
    redirectHTTP: true, // Best practice to redirect if calling URL with `http`
    cpu: 512, // <-- Default: 0.25 Otherwise container dies to quickly
    memoryLimitMiB: 1024, // <-- Otherwise container dies to quickly
    domainName: "my.example.com",
    domainZone: HostedZone.fromHostedZoneAttributes(this, "HostedZone", {
      hostedZoneId: props.hostedZoneId,
      zoneName: props.zoneName,
    }),
  },
);

// Set Cognito
const userPool = new UserPool(this, "UserPool");
const userPoolClient = new UserPoolClient(this, "UserPoolClient", {
  userPool,
  generateSecret: true,
  authFlows: {
    userPassword: true,
  },
  oAuth: {
    flows: {
      authorizationCodeGrant: true,
    },
    callbackUrls: [
      `https://${service.loadBalancer.loadBalancerDnsName}/oauth2/idpresponse`,
    ],
  },
});
const userPoolDomain = new UserPoolDomain(this, "Domain", {
  userPool,
  cognitoDomain: {
    domainPrefix: "test-cdk-prefix",
  },
});

// Add actions to Target Groups
service.listener.addAction("CognitoListener", {
  action: new AuthenticateCognitoAction({
    userPool,
    userPoolClient,
    userPoolDomain,
    next: ListenerAction.forward([service.targetGroup]),
  }),
});
```

Line 34 is crucial for redirecting back to the load balancer domain. Lines 46 - 53 describe the new action for the listener, where we add the Cognito Suite from lines 22-43.

The “simplified” diagram

![Using ALB ECS pattern with Cognito](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/p-RZtd9Ii__5nhozMVURM.png)

### Cognito with Custom Domain and Identity Federation

If we want to use an Identity Federation like GitHub, we can utilize an L2-Construct `UserPoolIdentityProviderOidc`. Moreover, if we aim to establish a personalized domain for our login page, we need to generate a certificate initially and then add it to Route 53's Hosted Zone.

```typescript
declare const service: ApplicationLoadBalancedFargateService // like in the previous code
declare const userPool: UserPool(this, 'UserPool') // like in the previous code
declare const userPoolClient: UserPoolClient(this, 'UserPoolClient') // like in the previous code
const userPoolIdentityProviderOidc = new UserPoolIdentityProviderOidc(this, 'GitHubOidc', {
	clientId: 'register-the-app-on-github',
	clientSecret: 'get-it-on-github',
	issuerUrl: 'https://github.com',
	userPool,
	name: 'GitHub',
});

const hostedZone = HostedZone.fromHostedZoneAttributes(this, 'HostedZone', {
	hostedZoneId: 'my-id',
	zoneName: 'example.com',
})
const certificate = new CdkCertificate(this, 'Certificate', {
	domainName: 'github.example.com',
	validation: CertificateValidation.fromDns(hostedZone),
}); // ❗ You need a new one
const userPoolDomain = new UserPoolDomain(this, 'UserPoolDomain', {
	userPool,
	customDomain: {
		certificate,
		domainName: 'github.example.com',
	}
});

service.listener.addAction('CognitoListener', {
	action: new AuthenticateCognitoAction({
		userPool,
		userPoolClient,
		userPoolDomain,
		next: ListenerAction.forward([service.targetGroup]),
	}),
});
```

**Note: **Lines 1-3 have the same implementation as in the previous section.

![](https://assets.vrite.io/646ce5fc07ef9ba4530fd23b/xPj7v9VU8PHtGe61mLJh8.png)

It is possible to use Cognito with Identity Federation and a custom domain. However, you may not require a Cognito Custom Domain if you already use ID Federation with services like GitHub, Google, or Azure AD. A custom domain is available for completeness but may not be necessary in certain cases.

## Conclusion

The ECS pattern is very powerful and provides many services with less code. They still provide properties for customization. However, you most likely end up with more code (at least because of having HTTPs). What we haven’t seen are roles and policies. Do not worry the community won’t let you down and will try to reach the least privileged. And even if you have doubts, you could easily add your roles or policies.

In this blog post, we have demystified the `ApplicationLoadBalancedFargateService` pattern in their services, and I hope you understand this pattern a bit better. I recommend using them. However, don’t blindly trust them; check their underlying resources.