11

We use CDK and we are starting using service discovery within our ECS services (hence, server to server not client to server). This means that we don't need ALB (for the moment at least, maybe we can revisit this choice later).

Unfortunately the CDK pattern that exists for building ECS services (ecs_patterns.ApplicationLoadBalancedFargateService) also creates an ALB, so we cannot use it as it is and we need to create those CDK steps ourselves.

The idea is basically to "port" this tutorial from AWS from using AWS CLI to use CDK.

Question is: has anyone done that already and wants to share it or knows the reason why CDK Patterns doesn't have that option?

(If nobody is going to share it, we will do it and then share it of course; I think this option should be present in CDK straight away and it's just a matter of not wasting time with "just" a configuration issue – unless there is something we are not seeing here...).

Claudio
  • 5,740
  • 5
  • 33
  • 40

2 Answers2

21

OK, so at the end it was easier than expected. This is my final solution (and check the comments out, since I got stuck a couple of times):

/*
 * ECS Fargate with service discovery but without Load Balancing
 */
import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as ecs from "@aws-cdk/aws-ecs";
import * as servicediscovery from "@aws-cdk/aws-servicediscovery";
import * as iam from "@aws-cdk/aws-iam";
import * as logs from "@aws-cdk/aws-logs";
import { DnsRecordType } from "@aws-cdk/aws-servicediscovery";

export class EcsServiceDiscoveryStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const serviceName = "zambulo";
    const namespace = "caludio.magic";

    const vpc = ec2.Vpc.fromLookup(this, "VPC", {
      isDefault: true,
    });

    const cluster = new ecs.Cluster(this, "EcsServiceDiscovery", {
      vpc: vpc,
    });

    const dnsNamespace = new servicediscovery.PrivateDnsNamespace(
      this,
      "DnsNamespace",
      {
        name: namespace,
        vpc: vpc,
        description: "Private DnsNamespace for my Microservices",
      }
    );

    const taskrole = new iam.Role(this, "ecsTaskExecutionRole", {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
    });

    taskrole.addManagedPolicy(
      iam.ManagedPolicy.fromAwsManagedPolicyName(
        "service-role/AmazonECSTaskExecutionRolePolicy"
      )
    );

    /*
     * Check the doc for the allowed cpu/mem combiations:
     * https://docs.aws.amazon.com/cdk/api/latest/docs/@aws-cdk_aws-ecs.FargateTaskDefinition.html
     */
    const serviceTaskDefinition = new ecs.FargateTaskDefinition(
      this,
      `${serviceName}ServiceTaskDef`,
      {
        cpu: 256,
        memoryLimitMiB: 512,
        taskRole: taskrole,
      }
    );

    const serviceLogGroup = new logs.LogGroup(
      this,
      `${serviceName}ServiceLogGroup`,
      {
        logGroupName: `/ecs/${serviceName}Service`,
        removalPolicy: cdk.RemovalPolicy.DESTROY,
      }
    );

    /* Fargate only support awslog driver */
    const serviceLogDriver = new ecs.AwsLogDriver({
      logGroup: serviceLogGroup,
      streamPrefix: `${serviceName}Service`,
    });

    /*
     * If you chose a public image from the registry (like in this case),
     * the `assignPublicIp` in the Fargate definition (below) must be true
     */
    const serviceContainer = serviceTaskDefinition.addContainer(
      `${serviceName}ServiceContainer`,
      {
        image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
        logging: serviceLogDriver,
      }
    );

    serviceContainer.addPortMappings({
      containerPort: 80,
    });

    const serviceSecGrp = new ec2.SecurityGroup(
      this,
      `${serviceName}ServiceSecurityGroup`,
      {
        allowAllOutbound: true,
        securityGroupName: `${serviceName}ServiceSecurityGroup`,
        vpc: vpc,
      }
    );

    serviceSecGrp.connections.allowFromAnyIpv4(ec2.Port.tcp(80));

    new ecs.FargateService(this, `${serviceName}Service`, {
      cluster: cluster,
      taskDefinition: serviceTaskDefinition,
      // Must be `true` when using public images
      assignPublicIp: true,
      // If you set it to 0, the deployment will finish succesfully anyway
      desiredCount: 1,
      securityGroup: serviceSecGrp,
      cloudMapOptions: {
        // This will be your service_name.namespace
        name: serviceName,
        cloudMapNamespace: dnsNamespace,
        dnsRecordType: DnsRecordType.A,
      },
    });
  }
}
Claudio
  • 5,740
  • 5
  • 33
  • 40
  • 2
    I found an alternative approach to get a **PrivateDnsNamespace**: `const dnsNamespace = cluster.addDefaultCloudMapNamespace({ vpc, name: namespace, type: servicediscovery.NamespaceType.DNS_PRIVATE });` – Bryan Jyh Herng Chong Oct 05 '21 at 13:01
  • 2
    So how does one container in a task address another container in another task with the above DNS setup? – CpILL Aug 26 '22 at 01:31
  • @CpILL I think it would be zambulo.caludio.magic in that example. E.g. `http://zambulo.caludio.magic` – alayor Mar 02 '23 at 04:20
0

CDK Patterns is a great resource, however it is limited to only a subset of all of the possible use cases for deploying infrastructure on AWS, hence 'patterns'.

I have no personal experience with AWS Service Discovery but it appears the CDK does offer L2 constructs for it. aws-servicediscovery

In addition to service discovery, you can also create a L2 FargateService construct using the aws-ecs library.

nsquires
  • 899
  • 2
  • 8
  • 20
  • 1
    Yeah, at the end I ended up writing my own script, thanks. I will post it here as the accepted answer since it also contains a couple of hints maybe useful for others. Thanks! – Claudio Mar 27 '21 at 10:36