DEV Community

Cover image for Demystifying AWS CDK’s ECS Pattern
JoLo
JoLo

Posted on

Demystifying AWS CDK’s ECS Pattern

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. 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.

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

Or if you want to use your own taskDefinition:

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`
  })
})
Enter fullscreen mode Exit fullscreen mode

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

Minimal Code for ALB ECS pattern

When using the taskDefinitionyou 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 and a domain name in your Route53.

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,
  }),
});
Enter fullscreen mode Exit fullscreen mode

And this is what it looks like

ALB ECS Pattern with HTTPs

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.

// 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]),
  }),
});
Enter fullscreen mode Exit fullscreen mode

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

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.

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]),
    }),
});
Enter fullscreen mode Exit fullscreen mode

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

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.

Top comments (0)