Skip to main content
In this tutorial, you use CDK Terrain (CDKTN) to deploy an example web application on Kubernetes. You will:
  • Create a local Kubernetes cluster with kind.
  • Convert an existing Terraform configuration into TypeScript.
  • Refactor into reusable constructs and add tests.
  • Deploy custom frontend and backend images via a local registry.
  • Deploy a second stack representing a test environment.

Prerequisites

  • Terraform v1.2+ (or OpenTofu)
  • CDKTN v0.15+
  • Docker Desktop (or equivalent) installed and running
  • Node.js (v18+) and npm (v8.19+)
  • kubectl
  • kind
CDKTN works with both Terraform and OpenTofu. To run OpenTofu, set TERRAFORM_BINARY_NAME=tofu before using the CLI. Refer to Environment Variables for details.

Clone example repository

Shell
git clone https://github.com/hashicorp-education/learn-terraform-cdktf-applications
Shell
cd learn-terraform-cdktf-applications

Start a local registry

Start a local Docker registry on 127.0.0.1:5000.
Shell
docker run -d --restart always -p "127.0.0.1:5000:5000" --name local-registry registry:2 Unable to find image 'registry:2' locally 2: Pulling from library/registry 530afca65e2e: Already exists d450d4da0343: Pull complete 96277bea17b6: Pull complete 470ad04e03fb: Pull complete bd3d4dc6e66f: Pull complete Digest: sha256:c631a581c6152f5a4a141a974b74cf308ab2ee660287a3c749d88e0b536c0c20 Status: Downloaded newer image for registry:2 2d9c6166d2ea3b1f6ef9d933afa6069eef5e2dbaece27ce1b235b89b7c5d374b

Create a kind cluster

Create the cluster.
Shell
kind create cluster --name=cdktf-app --config kind-config.yaml Creating cluster "cdktf-app" ... ✓ Ensuring node image (kindest/node:v1.25.0) 🖼 ✓ Preparing nodes 📦 ✓ Writing configuration 📜 ✓ Starting control-plane 🕹️ ✓ Installing CNI 🔌 ✓ Installing StorageClass 💾 Set kubectl context to "kind-cdktf-app" You can now use your cluster with: kubectl cluster-info --context kind-cdktf-app Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/user/quick-start/
Verify the cluster.
Shell
kind get clusters cdktf-app
Shell
kubectl cluster-info --context=kind-cdktf-app Kubernetes control plane is running at https://127.0.0.1:56821 CoreDNS is running at https://127.0.0.1:56821/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
Write a kubeconfig for this cluster that you will reference from CDKTN.
Shell
kubectl config view --raw --context kind-cdktf-app > kubeconfig.yaml
Connect the local registry to the kind network and apply the registry hosting config.
Shell
docker network connect kind local-registry
Shell
kubectl apply -f local-registry-configmap.yaml --kubeconfig kubeconfig.yaml configmap/local-registry-hosting created

Initialize your CDKTN application

Create the app/ directory and move into it.
Shell
mkdir app
Shell
cd app
Initialize a TypeScript CDKTN project using the Kubernetes provider.
Shell
cdktn init --template=typescript \
  --project-name=learn-terraform-cdktf-applications \
  --project-description="Learn how to develop CDKTN applications" \
  --providers="kubernetes@~>2.14" \
  --local

Create a Kubernetes Deployment

Convert the provided Terraform configuration into TypeScript.
Shell
cat ../k8s_deployment.tf | cdktn convert --provider=kubernetes /*Provider bindings are generated by running cdktn get. See https://cdk.tf/provider-generation for more details.*/ import * as kubernetes from "./.gen/providers/kubernetes"; new kubernetes.deployment.Deployment(this, "myapp", { metadata: { labels: { app: "myapp", component: "frontend", environment: "dev", }, name: "myapp-frontend-dev", }, spec: { replicas: "1", selector: { matchLabels: { app: "myapp", component: "frontend", environment: "dev", }, }, template: { metadata: { labels: { app: "myapp", component: "frontend", environment: "dev", }, }, spec: {
Install dependencies.
Shell
npm install path added 299 packages, and audited 357 packages in 3s 33 packages are looking for funding run `npm fund` for details found 0 vulnerabilities
Synthesize the application.
Shell
cdktn synth Generated Terraform code for the stacks: app
Deploy the stack.
Shell
cdktn deploy app Initializing the backend... app Successfully configured the backend "local"! Terraform will automatically use this backend unless the backend configuration changes. app Initializing provider plugins... app - Finding hashicorp/kubernetes versions matching "2.14.0"... app - Using hashicorp/kubernetes v2.14.0 from the shared cache directory ##... Plan: 1 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop ##... app kubernetes_deployment.myapp (myapp): Creating... app kubernetes_deployment.myapp (myapp): Creation complete after 8s [id=default/myapp] app Apply complete! Resources: 1 added, 0 changed, 0 destroyed. No outputs found.
Verify the deployment.
Shell
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp 1/1 1 1 117s

Scale the deployment

After you update the deployment to use 4 replicas, deploy again.
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... app - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 app Terraform has been successfully initialized! ##... Terraform will perform the following actions: app # kubernetes_deployment.myapp (myapp) will be updated in-place ~ resource "kubernetes_deployment" "myapp" { id = "default/myapp" # (1 unchanged attribute hidden) ~ spec { ~ replicas = "1" -> "4" # (4 unchanged attributes hidden) # (3 unchanged blocks hidden) } # (1 unchanged block hidden) } Plan: 0 to add, 1 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan.
Verify the updated replica count.
Shell
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp 4/4 4 4 8m31s

Refactor your deployment

Create a constructs/ directory.
Shell
mkdir constructs
Create constructs/kubernetes-web-app.ts.
TypeScript
import { Construct } from "constructs"
import * as kubernetes from "@cdktn/provider-kubernetes"

export interface KubernetesWebAppDeploymentConfig {
  readonly image: string
  readonly replicas: number
  readonly app: string
  readonly component: string
  readonly environment: string
  readonly env?: Record<string, string>
}

export class KubernetesWebAppDeployment extends Construct {
  public readonly resource: kubernetes.deployment.Deployment

  constructor(
    scope: Construct,
    name: string,
    config: KubernetesWebAppDeploymentConfig
  ) {
    super(scope, name)

    this.resource = new kubernetes.deployment.Deployment(this, name, {
      metadata: {
        labels: {
          app: config.app,
          component: config.component,
          environment: config.environment,
        },
        name: `${config.app}-${config.component}-${config.environment}`,
      },
      spec: {
        replicas: config.replicas.toString(),
        selector: {
          matchLabels: {
            app: config.app,
            component: config.component,
            environment: config.environment,
          },
        },
        template: {
          metadata: {
            labels: {
              app: config.app,
              component: config.component,
              environment: config.environment,
            },
          },
          spec: {
            container: [
              {
                image: config.image,
                name: `${config.app}-${config.component}-${config.environment}`,
                env: Object.entries(config.env || {}).map(([name, value]) => ({
                  name,
                  value,
                })),
              },
            ],
          },
        },
      },
    })
  }
}
Export the construct from constructs/index.ts.
TypeScript
export * from "./kubernetes-web-app"
Update app/main.ts to use your new construct.
TypeScript
import { Construct } from "constructs"
import { App, TerraformStack } from "cdktn"
import * as kubernetes from "@cdktn/provider-kubernetes"
import * as path from "path"

import { KubernetesWebAppDeployment } from "./constructs"

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    super(scope, name)

    new kubernetes.provider.KubernetesProvider(this, 'kind', {
      configPath: path.join(__dirname, '../kubeconfig.yaml'),
    })

    new KubernetesWebAppDeployment(this, 'deployment', {
      image: 'nginx:latest',
      replicas: 2,
      app: 'myapp',
      component: 'frontend',
      environment: 'dev',
    })
  }
}

const app = new App()
new MyStack(app, 'app')
app.synth()
Deploy the refactored application.
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... app - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 app Terraform has been successfully initialized! ##... Plan: 1 to add, 0 to change, 1 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop ##... app kubernetes_deployment.myapp: Destroying... [id=default/myapp] app kubernetes_deployment.deployment_7B0B4E40 (deployment/deployment): Creating... app kubernetes_deployment.myapp: Destruction complete after 0s app kubernetes_deployment.deployment_7B0B4E40 (deployment/deployment): Creation complete after 8s [id=default/myapp-frontend-dev] app Apply complete! Resources: 1 added, 0 changed, 1 destroyed. No outputs found.
Verify the new deployment.
Shell
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp-frontend-dev 2/2 2 2 5m14s

Add tests

Create app/jest.setup.js.
JavaScript
const cdktn = require('cdktn')
cdktn.Testing.setupJest()
Create app/__tests__/kubernetes-web-app-test.ts.
TypeScript
import "cdktn/lib/testing/adapters/jest"
import { Testing } from "cdktn"
import * as kubernetes from "@cdktn/provider-kubernetes"
import { KubernetesWebAppDeployment } from "../constructs"

describe('Our CDKTN Constructs', () => {
  describe('KubernetesWebAppDeployment', () => {
    it('should contain a deployment resource', () => {
      expect(
        Testing.synthScope((scope) => {
          new KubernetesWebAppDeployment(scope, 'myapp-frontend-dev', {
            image: 'nginx:latest',
            replicas: 4,
            app: 'myapp',
            component: 'frontend',
            environment: 'dev',
          })
        })
      ).toHaveResource(kubernetes.deployment.Deployment)
    })
  })
})
Run tests.
Shell
npm run test > app@1.0.0 test > jest PASS __tests__/main-test.ts PASS __tests__/kubernetes-web-app-test.ts Test Suites: 2 passed, 2 total Tests: 1 todo, 1 passed, 2 total Snapshots: 0 total Time: 5.109 s Ran all test suites.

Add a NodePort Service

Add a NodePort service interface.
TypeScript
export interface KubernetesNodePortServiceConfig {
  readonly port: number
  readonly app: string
  readonly component: string
  readonly environment: string
}
Add the NodePort service construct.
TypeScript
export class KubernetesNodePortService extends Construct {
  public readonly resource: kubernetes.service.Service

  constructor(
    scope: Construct,
    name: string,
    config: KubernetesNodePortServiceConfig
  ) {
    super(scope, name)

    this.resource = new kubernetes.service.Service(this, name, {
      metadata: {
        name: `${config.app}-${config.component}-${config.environment}`,
      },
      spec: {
        type: 'NodePort',
        port: [
          {
            port: 80,
            targetPort: '80',
            nodePort: config.port,
            protocol: 'TCP',
          },
        ],
        selector: {
          app: config.app,
          component: config.component,
          environment: config.environment,
        },
      },
    })
  }
}
Add a test for the new construct.
TypeScript
describe('KubernetesNodePortService', () => {
  it('should contain a Service resource', () => {
    expect(
      Testing.synthScope((scope) => {
        new KubernetesNodePortService(scope, 'myapp-frontend-dev', {
          app: 'myapp',
          component: 'frontend',
          environment: 'dev',
          port: 30001,
        })
      })
    ).toHaveResource(kubernetes.service.Service)
  })
})
Run tests.
Shell
npm run test > app@1.0.0 test > jest PASS __tests__/main-test.ts PASS __tests__/kubernetes-web-app-test.ts Test Suites: 2 passed, 2 total Tests: 1 todo, 2 passed, 3 total Snapshots: 0 total Time: 5.094 s Ran all test suites.

Add a NodePortService to your application

Add the NodePort service in main.ts.
TypeScript
new KubernetesNodePortService(this, 'service', {
  port: 30001,
  app: 'myapp',
  component: 'frontend',
  environment: 'dev',
})
Deploy.
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... app - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 app Terraform has been successfully initialized! ##... Plan: 1 to add, 0 to change, 0 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop ##... app kubernetes_service.service_E7C408F2 (service/service): Creating... app kubernetes_service.service_E7C408F2 (service/service): Creation complete after 0s [id=default/myapp-frontend-dev] app Apply complete! Resources: 1 added, 0 changed, 0 destroyed. No outputs found.
Verify the service and request it.
Shell
kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 101m myapp-frontend-dev NodePort 10.96.186.212 <none> 80:30001/TCP 22s
Shell
curl http://localhost:30001 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>

Refactor constructs

Destroy the stack before continuing.
Shell
cdktn destroy app Initializing the backend... app Initializing provider plugins... app - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 app Terraform has been successfully initialized! ##... Plan: 0 to add, 0 to change, 2 to destroy. ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop app kubernetes_service.service_E7C408F2 (service/service): Destroying... [id=default/myapp-frontend-dev] app kubernetes_deployment.deployment_7B0B4E40 (deployment/deployment): Destroying... [id=default/myapp-frontend-dev] app kubernetes_deployment.deployment_7B0B4E40 (deployment/deployment): Destruction complete after 0s app kubernetes_service.service_E7C408F2 (service/service): Destruction complete after 0s app Destroy complete! Resources: 2 destroyed.
Add a Terraform output import to constructs/kubernetes-web-app.ts.
TypeScript
import { TerraformOutput } from "cdktn"
Add a new construct that represents an application composed of a Deployment and NodePort Service.
TypeScript
export type SimpleKubernetesWebAppConfig = KubernetesWebAppDeploymentConfig &
  KubernetesNodePortServiceConfig

export class SimpleKubernetesWebApp extends Construct {
  public readonly deployment: KubernetesWebAppDeployment
  public readonly service: KubernetesNodePortService
  public readonly config: SimpleKubernetesWebAppConfig

  constructor(
    scope: Construct,
    name: string,
    config: SimpleKubernetesWebAppConfig
  ) {
    super(scope, name)

    this.config = config
    this.deployment = new KubernetesWebAppDeployment(this, 'deployment', {
      image: config.image,
      replicas: config.replicas,
      app: config.app,
      component: config.component,
      environment: config.environment,
      env: config.env,
    })

    this.service = new KubernetesNodePortService(this, 'service', {
      port: config.port,
      app: config.app,
      component: config.component,
      environment: config.environment,
    })

    new TerraformOutput(this, 'url', {
      value: `http://localhost:${config.port}`,
    })
  }
}
Add tests for the new construct.
TypeScript
describe('SimpleKubernetesWebApp', () => {
  it('should contain a Service resource', () => {
    expect(
      Testing.synthScope((scope) => {
        new SimpleKubernetesWebApp(scope, 'myapp-frontend-dev', {
          image: 'nginx:latest',
          replicas: 4,
          app: 'myapp',
          component: 'frontent',
          environment: 'dev',
          port: 30001,
        })
      })
    ).toHaveResource(kubernetes.service.Service)
  })
  it('should contain a Deployment resource', () => {
    expect(
      Testing.synthScope((scope) => {
        new SimpleKubernetesWebApp(scope, 'myapp-frontend-dev', {
          image: 'nginx:latest',
          replicas: 4,
          app: 'myapp',
          component: 'frontent',
          environment: 'dev',
          port: 30001,
        })
      })
    ).toHaveResource(kubernetes.deployment.Deployment)
  })
})
Run tests.
Shell
npm run test > app@1.0.0 test > jest PASS __tests__/main-test.ts (6.019 s) PASS __tests__/kubernetes-web-app-test.ts (8.059 s) Test Suites: 2 passed, 2 total Tests: 1 todo, 4 passed, 5 total Snapshots: 0 total Time: 8.556 s Ran all test suites.

Use the SimpleKubernetesWebApp construct

Update your imports.
TypeScript
import { SimpleKubernetesWebApp } from "./constructs"
Replace the previous deployment + service with a single construct.
TypeScript
new SimpleKubernetesWebApp(this, 'app_frontend', {
  image: 'nginx:latest',
  replicas: 3,
  port: 30001,
  app: 'myapp',
  component: 'frontend',
  environment: 'dev',
})
Deploy.
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... app - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 app Terraform has been successfully initialized! ##... Plan: 2 to add, 0 to change, 0 to destroy. Changes to Outputs: + app_frontend_url_5DD99814 = "http://localhost:30001" ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop app kubernetes_service.app_frontend_service_C2863249 (app_frontend/service/service): Creating... app kubernetes_deployment.app_frontend_deployment_0EE98C72 (app_frontend/deployment/deployment): Creating... app kubernetes_service.app_frontend_service_C2863249 (app_frontend/service/service): Creation complete after 0s [id=default/myapp-frontend-dev] app kubernetes_deployment.app_frontend_deployment_0EE98C72 (app_frontend/deployment/deployment): Creation complete after 4s [id=default/myapp-frontend-dev] app Apply complete! Resources: 2 added, 0 changed, 0 destroyed. app Outputs: app_frontend_url_5DD99814 = "http://localhost:30001"
Verify and request it.
Shell
kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 142m myapp-frontend-dev NodePort 10.96.216.88 <none> 80:30001/TCP 44s
Shell
curl http://localhost:30001 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>

Deploy a custom image

Build and push the frontend image.
Shell
cd ../frontend
Shell
docker build . -t nocorp-frontend [+] Building 33.0s (12/12) FINISHED => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 295B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 67B 0.0s => [internal] load metadata for docker.io/library/node:14 1.3s => [1/7] FROM docker.io/library/node:14@sha256:109b118e0d49dd12ca6f5b84a7a9a9c8a147f75567b3ad50620bdacaf5e6320d 26.2s ## ... => [internal] load build context 0.0s => => transferring context: 718.44kB 0.0s => [2/7] WORKDIR /usr/share/app 0.2s => [3/7] COPY package*.json ./ 0.0s => [4/7] COPY frontend.js ./ 0.0s => [5/7] COPY public/ ./public/ 0.0s => [6/7] COPY views/ ./views/ 0.0s => [7/7] RUN npm install 4.8s => exporting to image 0.2s => => exporting layers 0.2s => => writing image sha256:9dc1ce3668a79770f694ddeae6a5c2236527c381cd429d850eb4a37a8c565ce1 0.0s => => naming to docker.io/library/nocorp-frontend 0.0s Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Shell
docker tag nocorp-frontend:latest localhost:5000/nocorp-frontend:latest
Shell
docker push localhost:5000/nocorp-frontend:latest The push refers to repository [localhost:5000/nocorp-frontend] 72d21ab0ed02: Pushed a831dca05db6: Pushed 66018b4c7669: Pushed 65f4bcfe9daa: Pushed add6fa0cc975: Pushed 8628cf05347a: Pushed 85bfe2c7cf32: Pushed 28e2f0d3695c: Pushed b892edc3d92e: Pushed f1486e967e48: Pushed 5750262417ad: Pushed 9ed3c35b4335: Pushed 6f7f3f280040: Pushed d6e0d602719c: Pushed 73c3e7ef7bc6: Pushed latest: digest: sha256:f56babe32077523e891b24af0e38fe00026c7f8ed38b89d90a102aaeeb3d40b8 size: 3465
Deploy the updated frontend image.
Shell
cd ../app
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... app - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 Terraform has been successfully initialized! ##... app Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: ~ update in-place Terraform will perform the following actions: app # kubernetes_deployment.app_frontend_deployment_0EE98C72 (app_frontend/deployment/deployment) will be updated in-place ~ resource "kubernetes_deployment" "app_frontend_deployment_0EE98C72" { id = "default/myapp-frontend-dev" # (1 unchanged attribute hidden) ~ spec { # (5 unchanged attributes hidden) ~ template { ~ spec { # (11 unchanged attributes hidden) ~ container { ~ image = "nginx:latest" -> "localhost:5000/nocorp-frontend:latest" name = "myapp-frontend-dev" # (8 unchanged attributes hidden) ##... Plan: 0 to add, 1 to change, 0 to destroy. ─────────────────────────────────────────────────────────────────────────────
Verify deployment.
Shell
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp-frontend-dev 3/3 3 3 43s
Verify the response.
Shell
curl http://localhost:30001 <!DOCTYPE html><html><head><title>Terranimo</title></head><link rel="stylesheet" href="/styles/terramino.css"></html><body><div class="score" id="score"></div><div class="container"><div class="content"><h1>Terramino</h1><p class="error" id="errorMessage">Could not connect to server! <br/> Reload to try again.</p><p>Move: ← → Rotate: ↑ Drop: ↓</p></div><div class="content"><canvas width="320" height="640" id="game"></canvas></div></div><script src="/scripts/terramino.js"></script><script>start('http://localhost:30002')</script></body>

Add a backend service

Build and push the backend image.
Shell
cd ../backend
Shell
npm run deploy > nocorp-backend-app@1.0.0 deploy /Users/<YOU>/code/learn-terraform-cdktf-applications/backend > npm run build && npm run tag && npm run push > nocorp-backend-app@1.0.0 build /Users/<YOU>/code/learn-terraform-cdktf-applications/backend > docker build . -t nocorp-backend ## ... The push refers to repository [localhost:5000/nocorp-backend] 676744aedcf3: Pushed e64b93b2c9bd: Pushed 8628cf05347a: Mounted from nocorp-frontend 85bfe2c7cf32: Mounted from nocorp-frontend 28e2f0d3695c: Mounted from nocorp-frontend b892edc3d92e: Mounted from nocorp-frontend f1486e967e48: Mounted from nocorp-frontend 5750262417ad: Mounted from nocorp-frontend 9ed3c35b4335: Mounted from nocorp-frontend 6f7f3f280040: Mounted from nocorp-frontend d6e0d602719c: Mounted from nocorp-frontend 73c3e7ef7bc6: Mounted from nocorp-frontend latest: digest: sha256:845e60dd8350066a89757f1bdb200584f317e5b15d68cc9609a3c359f3736676 size: 2841
Update main.ts to add a backend application.
TypeScript
const app_backend = new SimpleKubernetesWebApp(this, 'app_backend', {
  image: 'localhost:5000/nocorp-backend:latest',
  replicas: 1,
  port: 30002,
  app: 'myapp',
  component: 'backend',
  environment: 'dev',
})
Deploy.
Shell
cd ../app
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 Terraform has been successfully initialized! ##... app Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create ~ update in-place Terraform will perform the following actions: app # kubernetes_deployment.app_backend_deployment_1A8B5520 (app_backend/deployment/deployment) will be created + resource "kubernetes_deployment" "app_backend_deployment_1A8B5520" { ##... # kubernetes_deployment.app_frontend_deployment_0EE98C72 (app_frontend/deployment/deployment) will be updated in-place ~ resource "kubernetes_deployment" "app_frontend_deployment_0EE98C72" { id = "default/myapp-frontend-dev" ##... + env { + name = "BACKEND_APP_URL" + value = "http://localhost:30002" } ##... # kubernetes_service.app_backend_service_EAD583EF (app_backend/service/service) will be created + resource "kubernetes_service" "app_backend_service_EAD583EF" { ##... Plan: 2 to add, 1 to change, 0 to destroy. Changes to Outputs: + app_backend_url_CAA2B50B = "http://localhost:30002"
Verify deployments.
Shell
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp-backend-dev 1/1 1 1 57s myapp-frontend-dev 3/3 3 3 15m
Verify the backend.
Shell
curl http://localhost:30002/new {"game":{"id":"pdSW6Cymw","state":"running","score":0,"tetrominoSequence":["I","J","Z","O","T","S"]},"tetromino":"L"}

Deploy another application stack

Refactor the stack to accept frontend and backend configuration.
TypeScript
class MyStack extends TerraformStack {
  constructor(
    scope: Construct,
    name: string,
    config: {
      frontend: SimpleKubernetesWebAppConfig
      backend: SimpleKubernetesWebAppConfig
    }
  ) {
    super(scope, name)

    new kubernetes.provider.KubernetesProvider(this, 'kind', {
      configPath: path.join(__dirname, '../kubeconfig.yaml'),
    })

    const app_backend = new SimpleKubernetesWebApp(
      this,
      'app_backend',
      config.backend
    )
    new SimpleKubernetesWebApp(this, 'app_frontend', {
      ...config.frontend,
      env: { BACKEND_APP_URL: `http://localhost:${app_backend.config.port}` },
    })
  }
}
Deploy.
Shell
cdktn deploy app Initializing the backend... app Initializing provider plugins... - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.14.0 app Terraform has been successfully initialized! ##... app No changes. Your infrastructure matches the configuration. app Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed. app app_frontend url = http://localhost:30001 app_backend url = http://localhost:30002
Add a second stack to represent a test environment.
TypeScript
new MyStack(app, 'app-test', {
  frontend: {
    image: 'localhost:5000/nocorp-frontend:latest',
    replicas: 4,
    port: 30003,
    app: 'myapp',
    component: 'frontend',
    environment: 'test',
  },
  backend: {
    image: 'localhost:5000/nocorp-backend:latest',
    replicas: 2,
    port: 30004,
    app: 'myapp',
    component: 'backend',
    environment: 'test',
  },
})
Deploy the new stack.
Shell
cdktn deploy app-test app-test Initializing the backend... app-test Successfully configured the backend "local"! Terraform will automatically use this backend unless the backend configuration changes. app-test Initializing provider plugins... - Finding hashicorp/kubernetes versions matching "2.14.0"... app-test - Using hashicorp/kubernetes v2.14.0 from the shared cache directory app-test Terraform has created a lock file .terraform.lock.hcl to record the provider selections it made above. Include this file in your version control repository so that Terraform can guarantee to make the same selections by default when you run "terraform init" in the future. ##... Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop ##... app-test kubernetes_service.app_frontend_service_C2863249 (app_frontend/service/service): Creating... kubernetes_service.app_backend_service_EAD583EF (app_backend/service/service): Creating... app-test kubernetes_deployment.app_backend_deployment_1A8B5520 (app_backend/deployment/deployment): Creating... app-test kubernetes_deployment.app_frontend_deployment_0EE98C72 (app_frontend/deployment/deployment): Creating... app-test kubernetes_service.app_frontend_service_C2863249 (app_frontend/service/service): Creation complete after 0s [id=default/myapp-frontend-test] app-test kubernetes_service.app_backend_service_EAD583EF (app_backend/service/service): Creation complete after 0s [id=default/myapp-backend-test] app-test kubernetes_deployment.app_frontend_deployment_0EE98C72 (app_frontend/deployment/deployment): Creation complete after 8s [id=default/myapp-frontend-test] app-test kubernetes_deployment.app_backend_deployment_1A8B5520 (app_backend/deployment/deployment): Creation complete after 8s [id=default/myapp-backend-test] app-test Apply complete! Resources: 4 added, 0 changed, 0 destroyed. app-test Outputs:
Verify both environments.
Shell
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp-backend-dev 1/1 1 1 46m myapp-backend-test 2/2 2 2 71s myapp-frontend-dev 3/3 3 3 60m myapp-frontend-test 4/4 4 4 71s

Clean up resources

Destroy the test stack.
Shell
cdktn destroy app-test app-test Initializing the backend... app-test Initializing provider plugins... - Reusing previous version of hashicorp/kubernetes from the dependency lock file app-test - Using previously-installed hashicorp/kubernetes v2.14.0 app-test Terraform has been successfully initialized! ##... Plan: 0 to add, 0 to change, 4 to destroy. app-test Changes to Outputs: - app_backend_url_CAA2B50B = "http://localhost:30004" -> null - app_frontend_url_5DD99814 = "http://localhost:30003" -> null ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app-test ❯ Approve Applies the changes outlined in the plan. Dismiss Stop ##... app-test kubernetes_service.app_backend_service_EAD583EF (app_backend/service/service): Destruction complete after 0s app-test kubernetes_service.app_frontend_service_C2863249 (app_frontend/service/service): Destruction complete after 0s app-test Destroy complete! Resources: 4 destroyed.
Destroy the dev stack.
Shell
cdktn destroy app app Initializing the backend... app Initializing provider plugins... - Reusing previous version of hashicorp/kubernetes from the dependency lock file app - Using previously-installed hashicorp/kubernetes v2.12.1 app Terraform has been successfully initialized! ##... Plan: 0 to add, 0 to change, 4 to destroy. Changes to Outputs: - app_frontend_url_FE3D723A = "http://localhost:30001" -> null - app_backend_url_91B41C22 = "http://localhost:30002" -> null ───────────────────────────────────────────────────────────────────────────── Saved the plan to: plan To perform exactly these actions, run the following command to apply: terraform apply "plan" Please review the diff output above for app ❯ Approve Applies the changes outlined in the plan. Dismiss Stop ##... app kubernetes_service.app_backend_service_EAD583EF (app_backend/service/service): Destruction complete after 0s app kubernetes_service.app_frontend_service_C2863249 (app_frontend/service/service): Destruction complete after 0s app Destroy complete! Resources: 4 destroyed.
Delete the kind cluster and remove the local registry.
Shell
kind delete cluster --name=cdktf-app Deleting cluster "cdktf-app" ...
Shell
docker stop local-registry local-registry
Shell
docker rm local-registry local-registry

Next steps