Fargate with Load Balancer

Quickly set up an ALB usinc AWS CDK.

Fargate with Load Balancer
const { service, targetGroup } =
  new ecsPatterns.ApplicationLoadBalancedFargateService(
    this,
    "CoolAppLoadBalancer",
    {
      serviceName: `CoolApp${capitalize(props.environment)}`,
      cpu: 1024,
      taskDefinition,
      securityGroups: [this.securityGroup],
      desiredCount: props.instanceCount || 1,
      publicLoadBalancer: true,
      cluster,
      deploymentController: { type: ecs.DeploymentControllerType.ECS },
      certificate,
      domainName,
      domainZone: hostedZone,
      redirectHTTP: true,
      loadBalancerName: `CoolAppALB${capitalize(props.environment)}`,
    }
  );

new cdk.CfnOutput(this, "service", { value: service.serviceName });

targetGroup.setAttribute("deregistration_delay.timeout_seconds", "5");

targetGroup.configureHealthCheck({
  interval: Duration.seconds(10),
  timeout: Duration.seconds(5),
  healthyThresholdCount: 5,
});

new cdk.CfnOutput(this, "targetgrouparn", {
  value: targetGroup.targetGroupArn,
});

Usage

In order to set up the ALB, you are going to need references to the following resources:

  • Environment in use
  • Task definition (or at least an image)
  • Security group
  • Certificate
  • Domain name
  • Domain zone
1

Get references to domain & certificate

const props = {
  appName: "cool-app",
  domain: "awesomedomain.com",
};

const domainName = `${props.appName}.${props.domain}`;

const hostedZone = route53.HostedZone.fromLookup(this, "DnsZone", {
  domainName: props.domain,
});

const certificate = new acm.Certificate(this, `CoolAppCert`, {
  domainName,
  validation: acm.CertificateValidation.fromDns(hostedZone),
});
2

Retrieve references to the VPC, Security group & Cluster

const vpc = ec2.Vpc.fromLookup(this, "Vpc", { vpcId: props.vpcId });

this.securityGroup = new ec2.SecurityGroup(this, "CoolAppSecurityGroup", {
  vpc,
  securityGroupName: `CoolApp${capitalize(props.environment)}SecurityGroup`,
});

// Manipulate the securitygroup to suit your application here if needed
const cluster = ecs.Cluster.fromClusterAttributes(this, "ECSCluster", {
  vpc,
  securityGroups: [this.securityGroup],
  clusterName: props.environment,
});

// This outputs the cluster name to the configured output file (output.json)
new cdk.CfnOutput(this, "cluster", { value: cluster.clusterName });
3

Fetch secrets (database)

// Use the environment prop to fetch the right credentials for your environment
const verySecretSecrets = secretsmanager.Secret.fromSecretNameV2(
  this,
  `SecretSecrets`,
  `secret-database-${props.environment}`
);

// (Optional): Ouput the secret ARN for use by CI
new cdk.CfnOutput(this, "secretarn", {
  value: postgresSecrets.secretArn,
});

// This utility enables easy fetching of secrets by key
const getSecret = (key: string) => ecs.Secret.fromSecretsManager(postgresSecrets, key) // prettier-ignore
4

Create Log group & Task definition

// The App will use this log group when storing its messages.
// You can set the amount of time you want the logs to be stored,
// as well as what name they should be showing up under in CloudWatch
const logGroup = new logs.LogGroup(this, "CoolAppLogGroup", {
  logGroupName: `cool-app-${props.environment}`,
  removalPolicy: cdk.RemovalPolicy.DESTROY,
  retention: logs.RetentionDays.FIVE_DAYS,
});

// Create a task definition and assign a famility to it.
// This makes it easier to differentiate the created resources in the console.
const taskDefinition = new ecs.FargateTaskDefinition(this,'CoolAppTask',{family: 'weather-api'}) // prettier-ignore

taskDefinition.addContainer("CoolAppContainer", {
  image: ecs.ContainerImage.fromEcrRepository(props.ecrRepo, "latest"),
  logging: ecs.LogDriver.awsLogs({
    logGroup,
    streamPrefix: "/ecs/CoolApp",
  }),

  secrets: {
    DB_USER: getSecret("username"),
    DB_PASSWORD: getSecret("password"),
    DB_HOST: getSecret("host"),
    DB_PORT: getSecret("port"),
  },

  portMappings: [
    {
      containerPort: 3000,
      protocol: ecs.Protocol.TCP,
    },
  ],
});
5

Create ALB using ecs-patterns

const { service, targetGroup } =
  new ecsPatterns.ApplicationLoadBalancedFargateService(
    this,
    "CoolAppLoadBalancer",
    {
      serviceName: `CoolApp${capitalize(props.environment)}`,
      cpu: 1024,
      taskDefinition,
      securityGroups: [this.securityGroup],
      desiredCount: props.instanceCount || 1,
      publicLoadBalancer: true,
      cluster,
      deploymentController: { type: ecs.DeploymentControllerType.ECS },
      certificate,
      domainName,
      domainZone: hostedZone,
      redirectHTTP: true,
      loadBalancerName: `CoolAppALB${capitalize(props.environment)}`,
    }
  );

// Write service name to output for use in CI (to force redeployment for example)
new cdk.CfnOutput(this, "service", { value: service.serviceName });

// (Optional) Reduce the default deregistration delay from 300 seconds to a mere 5.
// This makes the deployment process quicker, since the time that the container
// waits before going into a DRAINING state is greatly reduced.
targetGroup.setAttribute("deregistration_delay.timeout_seconds", "5");

// (Optional) Reduce the interval, timeout and successful
//attempts needed to perform health checks
targetGroup.configureHealthCheck({
  interval: Duration.seconds(10),
  timeout: Duration.seconds(5),
  healthyThresholdCount: 5,
});

// Write target-group to output in order for the CI to
//be able to listen for successful deployments
new cdk.CfnOutput(this, "targetgrouparn", {
  value: targetGroup.targetGroupArn,
});

Final file

stacks/cool-app-stack.ts

import * as cdk from '@aws-cdk/core'
import * as ecsPatterns from '@aws-cdk/aws-ecs-patterns'
import * as secretsmanager from '@aws-cdk/aws-secretsmanager'
import * as ecs from '@aws-cdk/aws-ecs'
import * as ecr from '@aws-cdk/aws-ecr'
import * as acm from '@aws-cdk/aws-certificatemanager'
import * as route53 from '@aws-cdk/aws-route53'
import * as ec2 from '@aws-cdk/aws-ec2'
import * as logs from '@aws-cdk/aws-logs'
import { Duration } from '@aws-cdk/core'



export interface CoolAppStackProps extends cdk.StackProps {
 domain: string
 /** Used as part of the domain ({appname}.{domainName}) */
 appName: string
 environment: 'development' | 'staging' | 'production'
 ecrRepo: ecr.Repository
 vpcId: string
 /** Maximum amount of instances created by the ALB. @default 1 */
 instanceCount?: number
}



export class CoolAppStack extends cdk.Stack {
  securityGroup: ec2.SecurityGroup
  constructor(scope: cdk.Construct, id: string, props: CoolAppStackProps) {
    super(scope, id, props)

    const domainName = `${props.appName}.${props.domain}`

    const hostedZone = route53.HostedZone.fromLookup(this, 'DnsZone', {
      domainName: props.domain
    })

    const certificate = new acm.Certificate(this, `CoolAppCert`, {
      domainName,
      validation: acm.CertificateValidation.fromDns(hostedZone)
    })

    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcId: props.vpcId })

    this.securityGroup = new ec2.SecurityGroup(this, 'CoolAppSecurityGroup', {
      vpc,
      securityGroupName: `CoolApp${capitalize(props.environment)}SecurityGroup`
    })

    // Manipulate the securitygroup to suit your application here if needed
    const cluster = ecs.Cluster.fromClusterAttributes(this, 'ECSCluster', {
      vpc,
      securityGroups: [this.securityGroup],
      clusterName: props.environment
    })

    // This outputs the cluster name to the configured output file (output.json)
    new cdk.CfnOutput(this, 'cluster', { value: cluster.clusterName })

    // Use the environment prop to fetch the right credentials for your environment
    const verySecretSecrets = secretsmanager.Secret.fromSecretNameV2(
      this,
      `SecretSecrets`,
      `secret-database-${props.environment}`
    )

    // (Optional): Ouput the secret ARN for use by CI
    new cdk.CfnOutput(this, 'secretarn', {
      value: postgresSecrets.secretArn
    })

    // This utility enables easy fetching of secrets by key
    const getSecret = (key: string) => ecs.Secret.fromSecretsManager(postgresSecrets, key) // prettier-ignore

    // The App will use this log group when storing its messages.
    // You can set the amount of time you want the logs to be stored,
    // as well as what name they should be showing up under in CloudWatch
    const logGroup = new logs.LogGroup(this, 'CoolAppLogGroup', {
      logGroupName: `cool-app-${props.environment}`,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      retention: logs.RetentionDays.FIVE_DAYS
    })

    // Create a task definition and assign a famility to it.
    // This makes it easier to differentiate the created resources in the console.
    const taskDefinition = new ecs.FargateTaskDefinition(this,'CoolAppTask',{family: 'weather-api'}) // prettier-ignore

    taskDefinition.addContainer('CoolAppContainer', {
      image: ecs.ContainerImage.fromEcrRepository(props.ecrRepo, 'latest'),
      logging: ecs.LogDriver.awsLogs({
        logGroup,
        streamPrefix: '/ecs/CoolApp'
      }),
      secrets: {
        DB_USER: getSecret('username'),
        DB_PASSWORD: getSecret('password'),
        DB_HOST: getSecret('host'),
        DB_PORT: getSecret('port')
      },
      portMappings: [
        {
          containerPort: 3000,
          protocol: ecs.Protocol.TCP
        }
      ]
    })

    const { service, targetGroup } = new ecsPatterns.ApplicationLoadBalancedFargateService(
      this,
      'CoolAppLoadBalancer',
      {
        serviceName: `CoolApp${capitalize(props.environment)}`,
        cpu: 1024,
        taskDefinition,
        securityGroups: [this.securityGroup],
        desiredCount: props.instanceCount || 1,
        publicLoadBalancer: true,
        cluster,
        deploymentController: { type: ecs.DeploymentControllerType.ECS },
        certificate,
        domainName,
        domainZone: hostedZone,
        redirectHTTP: true,
        loadBalancerName: `CoolAppALB${capitalize(props.environment)}`
      }
    )

    // Write service name to output for use in CI (to force redeployment for example)
    new cdk.CfnOutput(this, 'service', { value: service.serviceName })

    // (Optional) Reduce the default deregistration delay from 300 seconds to a mere 5.
    // This makes the deployment process quicker, since the time that the container
    // waits before going into a DRAINING state is greatly reduced.
    targetGroup.setAttribute('deregistration_delay.timeout_seconds', '5')

    // (Optional) Reduce the interval, timeout and successful
    //attempts needed to perform health checks
    targetGroup.configureHealthCheck({
      interval: Duration.seconds(10),
      timeout: Duration.seconds(5),
      healthyThresholdCount: 5
    })

    // Write target-group to output in order for the CI to
    //be able to listen for successful deployments
    new cdk.CfnOutput(this, 'targetgrouparn', {
      value: targetGroup.targetGroupArn
    })
  }
}

Fore completeness, here's a cdk-app file to tie it together and make it deployable.

cdk-app.ts

#!/usr/bin/env node

import 'source-map-support/register'
import * as cdk from '@aws-cdk/core'
import { CoolAppStack } from './stacks/cool-app-stack'
import { EcrRepoStack } from './stacks/repo-stack'

new CoolAppStack(app, 'CoolAppStackDevelopment', {
 appName: 'cool-app',
 domain: 'coolcompany.com',
 env: {
   region: 'eu-west-1',
   account: '12345678987',
 },
 vpcId: 'vpc-12ab34cd',
 environment: 'development',
 ecrRepo: ecrCoolAppRepo.repo,
 tags: {
   application: 'cool-app',
   realm: 'snippet-showoffs',
   environment: 'development',
 }
})