How to handover your public endpoint with docker-compose and ECS

Sometimes the local name resolution from docker-compose or cloud map is not enough. Sometimes you need the public FQDN, like when you startup a Kafka cluster. I’ll show you how you can handover the public DNS name of your loadbalancer to the container with an environment variable.

Let’s start a little bit earlier. My setup is nearly like described on the Docker Documentation. I need to handover the public DNS name as an environment variable to all of my 3 Kafka containers. It doesn’t matter which image you use or why you need the DNS name. Kafka needs the name as it is the base of the connection string given back to the end-user, which is not inside the docker-compose network and can therefore not resolve the container names like Kafka-1.

The solution is described in the Docker documentation, but you need some cloud formation know-how and some magic tricks to get it running. The first thing you need is the command to translate the docker-compose.yml to cloud formation:

docker-compose convert > cf_template.yml

The further points are more related to the overlay implementation. It helped me to convert and compare the docker-compose file with and without the overlay. Note that the overlay has “x-aws-cloudformation” at the highest level, so the cloud formation template, starting with resource, must be indented once more.

Overlay Example:

x-aws-cloudformation:
  Resources:
    Kafka1TaskDefinition:
      Properties:
        ContainerDefinitions:
        - Environment:
        ...

4 indents for “ContainerDefinitions” Cloud formation Example:

Resources:
  Kafka1TaskDefinition:
    Properties:
      ContainerDefinitions:
      - Environment:
      ...

3 indents for “ContainerDefinitions”

How do we get the DNS name now? The architecture of the docker compose with ecs is a network loadbalancer in front of ECS Fargate, so the single, public entrypoint is the DNS name of the loadbalancer. The resource ist named “LoadBalancer” and from the type “AWS::ElasticLoadBalancingV2::LoadBalancer”. As stated in to Documentation there is a property called “DNSName” which can be called by GetAtt. So a first draft could look like this:

x-aws-cloudformation:
  Resources:
    Kafka1TaskDefinition:
      Properties:
        ContainerDefinitions:
        - Environment:
          - Name: KAFKA_ADVERTISED_LISTENERS
            Value: !Join
              - ""
              - - "LISTENER_INTERNAL://kafka-1:19092,LISTENER_DOCKERHOST://localhost:29092,LISTENER_EXTERNAL://"
                - !GetAtt LoadBalancer.DNSName
                - ":9092"

converted:

  Kafka1TaskDefinition:
    Properties:
      ContainerDefinitions:
      - Environment:
        - Name: KAFKA_ADVERTISED_LISTENERS
          Value:
          - ""
          - - LISTENER_INTERNAL://kafka-1:19092,LISTENER_DOCKERHOST://localhost:29092,LISTENER_EXTERNAL://
            - LoadBalancer.DNSName
            - :9092
      Cpu: "256"
      ExecutionRoleArn:
        Ref: Kafka1TaskExecutionRole
      Family: platys-demo-platform-kafka-1
      Memory: "512"
      NetworkMode: awsvpc
      RequiresCompatibilities:
      - FARGATE
    Type: AWS::ECS::TaskDefinition

we see two problems here, the functions are replaced by nothing and the whole configuration around is missing in the task definition. Playing around with cloud formation syntax showed me that the long hand syntax and a newline solve this problem with the replaced functions:

x-aws-cloudformation:
  Resources:
    Kafka1TaskDefinition:
      Properties:
        ContainerDefinitions:
        - Environment:
          - Name: KAFKA_ADVERTISED_LISTENERS
            Value: 
              Fn::Join:
                - ""
                - - "LISTENER_INTERNAL://kafka-1:19092,LISTENER_DOCKERHOST://localhost:29092,LISTENER_EXTERNAL://"
                  - Fn::GetAtt:
                    - LoadBalancer
                    - DNSName
                  - ":9092"

The problem with the missing configuration, can be explained very simply. A list with x entries is replaced with a list with only one entry. My intentional hope was that only this one entry would be modified. So we need to add the rest of the definition from the cloud formation template to the overlay as well. So here is the config which is working for me:

x-aws-cloudformation:
  Resources:
    Kafka1TaskDefinition:
      Properties:
        ContainerDefinitions:
        - Command:
          - eu-central-1.compute.internal
          - platys-demo-platform.local
          Essential: false
          Image: docker/ecs-searchdomain-sidecar:1.0
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:
                Ref: LogGroup
              awslogs-region:
                Ref: AWS::Region
              awslogs-stream-prefix: platys-demo-platform
          Name: Kafka1_ResolvConf_InitContainer
        - DependsOn:
          - Condition: SUCCESS
            ContainerName: Kafka1_ResolvConf_InitContainer
          Environment:
          - Name: KAFKA_ADVERTISED_LISTENERS
            Value: 
              Fn::Join:
                - ""
                - - "LISTENER_INTERNAL://kafka-1:19092,LISTENER_DOCKERHOST://localhost:29092,LISTENER_EXTERNAL://"
                  - Fn::GetAtt: 
                    - LoadBalancer 
                    - DNSName
                  - ":9092"
          - Name: KAFKA_AUTO_CREATE_TOPICS_ENABLE
            Value: "False"
          - ...
          Essential: true
          Image: docker.io/confluentinc/cp-kafka:6.0.1@sha256:b834319bd56bae6fbe2f74a7feb9c2e16312cbdf00fb9cac49f54185211afb35
          LinuxParameters: {}
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group:
                Ref: LogGroup
              awslogs-region:
                Ref: AWS::Region
              awslogs-stream-prefix: platys-demo-platform
          Name: kafka-1
          PortMappings:
          - ContainerPort: 9092
            HostPort: 9092
            Protocol: tcp
          - ContainerPort: 29092
            HostPort: 29092
            Protocol: tcp
          - ContainerPort: 9992
            HostPort: 9992
            Protocol: tcp

I did the same for the two other Kafka containers and it’s working fine. But for future adjustments, I have to be a bit more careful as the overlay covers a wide range and this might override changes done in the docker-compose part of the file.