Cdktf with docker containers waiting on other constructs

Hi all,

Trying to understand how to create a dependency with this docker provider and by going through other documentation & tutorials, I hit a snag and just want some assistance.

I want to create a dependency tree between the network and the container (I want the network defined first) instead of creating two constructs and hoping that the network construct gets called first. I can define two different constructs working with a main.ts:

import { Construct } from "constructs";
import { config } from "./config";
import { App, TerraformStack } from "cdktf";
import { DockerProvider } from "@cdktf/provider-docker/lib/provider";
import { LocalNetwork } from "./network";
import { LocalDatabase } from "./database";

class localPortalStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name);
    new DockerProvider(this, "docker", {});
    new LocalNetwork(this, 'internalnetwork');
    new LocalDatabase(this, 'dbcontainer');
  }
}

const app = new App();

new localPortalStack(app, config.name);
app.synth();

with a network.ts:

import { Construct } from "constructs";
import { config } from './config';
import { Network } from "@cdktf/provider-docker/lib/network";

export class LocalNetwork extends Construct {
  constructor(scope: Construct, name: string) {
    super(scope, name);
    new Network(this, "main", {name: config.networkName});
  }
}

and a database container

import { Construct } from "constructs";
import {config} from './config';
import { Container } from "@cdktf/provider-docker/lib/container";


export class LocalDatabase extends Construct {
  constructor(scope: Construct, name: string ) {
    super(scope, name);
    new Container(this, "postgresContainer", {
      name: config.postgresContainerName,
      image: config.postgresImage,
      networksAdvanced: [{name:config.networkName}]
    });

  }
}

and using a config

const name = 'portalapi-cdktf';
const networkName = 'app-company-network'
const postgresContainerName = 'postgres_cdktf'
const postgresImage = 'postgres:14.2'

export const config = {
  name,
  networkName,
  postgresContainerName,
  postgresImage,
};

This works… but it’s two constructs both being ran at the same time. I wanna inherit the network name and put it into the container instead of using a config file on the top level. I wanna use this idea/structure to have containers wait for each other (the idea is that the db container needs to be constructed first before the application container starts)

I’ve dug through a lot of tutorials and references that hashicorp has provided but still haven’t found something that concrete. I don’t feel it’s too difficult to do something like this, I just haven’t worked with node/typescript/cdktf that much but I do know terraform dependencies and I’ve known a docker compose can use a depends_on field and I feel like constructs are able to do so as well.

Hi @scotlyt :wave:

Thank you for the detailed write-up. Terraform does handle dependencies automatically if it can detect that one resource (in your case the Docker container) depends on another one. As you are using the same static network name string in both resources, this hides this dependency from Terraform.

You have two options to let Terraform know of this dependency:

1. Make it explicit

container.dependsOn = [network.fqn];
// or
new Container(this, "postgresContainer", {
      // ...
      dependsOn: [network],
});

This would require you to pass the Network to your LocalDatabase construct.

2. Use a reference

Reference the network name from the Network resource instead of passing the same static value. Terraform will then be able to automatically detect that your container depends on the network resource.

new Container(this, "postgresContainer", {
      // ...
      networksAdvanced: [{name: network.name}]
});

This would also require you to pass the Network (or at least the network.name string) to your LocalDatabase construct.
The reason why this works, is because the network.name string is actually a Token that will be resolved to a Terraform reference when your application is synthesized.

Hope this helped!

– Ansgar

1 Like

Hey Ansgar,

I got half it done on my own, and I was able to have the references work so the containers had to wait for the network. Since containers have to wait for a network reference, that works great.

Reference for those interested as just one main.ts file + config (please note I’m still somewhat new and I think I can get it working much better/cleaner just still learning typescript & cdktf):

import { Construct } from "constructs";
import { config } from "./config";
import { App, TerraformStack } from "cdktf";
import { DockerProvider } from "@cdktf/provider-docker/lib/provider";
import { Network } from "@cdktf/provider-docker/lib/network";
import { Image } from "@cdktf/provider-docker/lib/image";
import { Container } from "@cdktf/provider-docker/lib/container";
import { Volume } from "@cdktf/provider-docker/lib/volume";

class localPortalStack extends TerraformStack {
  public readonly network: Network
  public readonly postgresContainer: Container
  public readonly postgresImage: Image
  public readonly postgresVolume: Volume

  constructor(scope: Construct, name: string) {
    super(scope, name);
    new DockerProvider(this, "docker", {});

    this.network = new Network(this, "dockerNetwork",{
      name: config.networkName,
    });

    this.postgresImage = new Image(this, "dockerPostgresImage",{
      name: config.postgresImage,
    });

    this.postgresVolume = new Volume(this, "dockerPostgresvolume", {
      name: config.postgresVolumeName,
      driver: "local",
      driverOpts: {type : "none",
                   device: config.postgresHostPath,
                   o : "bind"},
    });

    this.postgresContainer = new Container(this, "dockerPostgresContainer",{
      name: config.postgresContainerName,
      image: this.postgresImage.imageId,
      ports: [
        {
          internal: config.postgresPortInternal,
          external: config.postgresPortExeternal,
        },
      ],
      env: ["POSTGRES_HOST=postgres",
            "POSTGRES_PORT=5432",
            "POSTGRES_USER=postgres",
            "POSTGRES_PASSWORD=somepass",
            ],
      volumes: [{volumeName: this.postgresVolume.name,
                 hostPath: config.postgresHostPath,
                 containerPath: config.postgresContainerPath }],
      networksAdvanced: [{name: this.network.name}]
    });

  }
}


const app = new App();

new localPortalStack(app, config.name);
app.synth();

and the config

const name = 'portal-cdktf';
const networkName = 'app-company-network'
const postgresContainerName = 'postgres_cdktf'
const postgresVolumeName = 'database-data-company'
const postgresImage = 'postgres:14.2'
const postgresHostPath = '/PATHTODATALOCALLY'
const postgresContainerPath = '/var/lib/postgresql/data/'
const postgresPortInternal = 5432
const postgresPortExeternal = 5432

export const config = {
  name,
  networkName,
  postgresContainerName,
  postgresVolumeName,
  postgresImage, 
  postgresHostPath,
  postgresContainerPath,
  postgresPortInternal,
  postgresPortExeternal,
};

However, the container waiting on another container was more tricky. The issue is that containers don’t have to necessarily wait on another container and there’s nothing in the provider that covers it because its based off the Docker API. It’s mainly because terraform has to have references to something on when it does an apply and since the container name hasn’t been made.

I’m gonna try the first method on making it explicit waiting on certain containers to be up. I have an outdated application that has a container requiring another container to be active first as I’m trying to remove the technical debt around it.

Thank you very much for your reply and info!

That worked exactly how I wanted to, thanks!