> ## Documentation Index
> Fetch the complete documentation index at: https://cdktn.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploy an application with CDK Terrain

> Deploy a Kubernetes app with CDKTN on a local kind cluster, refactor into reusable constructs with tests, ship custom images, and run multiple stacks.

<Note>
  This guide was recovered from the [Wayback Machine snapshot of “Deploy an application with CDK for Terraform”](https://web.archive.org/web/20251112223320https://developer.hashicorp.com/terraform/tutorials/cdktf/cdktf-applications).
</Note>

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](https://opentofu.org/))
* CDKTN v0.15+
* Docker Desktop (or equivalent) installed and running
* Node.js (v18+) and npm (v8.19+)
* `kubectl`
* `kind`

<Tip>CDKTN works with both Terraform and OpenTofu. To run OpenTofu, set `TERRAFORM_BINARY_NAME=tofu` before using the CLI. Refer to [Environment Variables](/create-and-deploy/environment-variables) for details.</Tip>

## Clone example repository

```shell Shell theme={null}
git clone https://github.com/hashicorp-education/learn-terraform-cdktf-applications
```

```shell Shell theme={null}
cd learn-terraform-cdktf-applications
```

## Start a local registry

Start a local Docker registry on `127.0.0.1:5000`.

```shell-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
kind get clusters cdktf-app
```

```shell-session Shell theme={null}
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 Shell theme={null}
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 Shell theme={null}
docker network connect kind local-registry
```

```shell-session Shell theme={null}
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 Shell theme={null}
mkdir app
```

```shell Shell theme={null}
cd app
```

Initialize a TypeScript CDKTN project using the Kubernetes provider.

```shell Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
cdktn synth Generated Terraform code for the stacks: app
```

Deploy the stack.

```shell-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp 4/4 4 4 8m31s
```

## Refactor your deployment

Create a `constructs/` directory.

```shell Shell theme={null}
mkdir constructs
```

Create `constructs/kubernetes-web-app.ts`.

```ts TypeScript theme={null}
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`.

```ts TypeScript theme={null}
export * from "./kubernetes-web-app"
```

Update `app/main.ts` to use your new construct.

```ts TypeScript theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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`.

```js JavaScript theme={null}
const cdktn = require('cdktn')
cdktn.Testing.setupJest()
```

Create `app/__tests__/kubernetes-web-app-test.ts`.

```ts TypeScript theme={null}
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-session Shell theme={null}
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.

```ts TypeScript theme={null}
export interface KubernetesNodePortServiceConfig {
  readonly port: number
  readonly app: string
  readonly component: string
  readonly environment: string
}
```

Add the NodePort service construct.

```ts TypeScript theme={null}
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.

```ts TypeScript theme={null}
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-session Shell theme={null}
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`.

```ts TypeScript theme={null}
new KubernetesNodePortService(this, 'service', {
  port: 30001,
  app: 'myapp',
  component: 'frontend',
  environment: 'dev',
})
```

Deploy.

```shell-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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`.

```ts TypeScript theme={null}
import { TerraformOutput } from "cdktn"
```

Add a new construct that represents an application composed of a Deployment and NodePort Service.

```ts TypeScript theme={null}
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.

```ts TypeScript theme={null}
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-session Shell theme={null}
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.

```ts TypeScript theme={null}
import { SimpleKubernetesWebApp } from "./constructs"
```

Replace the previous deployment + service with a single construct.

```ts TypeScript theme={null}
new SimpleKubernetesWebApp(this, 'app_frontend', {
  image: 'nginx:latest',
  replicas: 3,
  port: 30001,
  app: 'myapp',
  component: 'frontend',
  environment: 'dev',
})
```

Deploy.

```shell-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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 Shell theme={null}
cd ../frontend
```

```shell-session Shell theme={null}
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 Shell theme={null}
docker tag nocorp-frontend:latest localhost:5000/nocorp-frontend:latest
```

```shell-session Shell theme={null}
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 Shell theme={null}
cd ../app
```

```shell-session Shell theme={null}
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-session Shell theme={null}
kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE myapp-frontend-dev 3/3 3 3 43s
```

Verify the response.

```shell-session Shell theme={null}
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 Shell theme={null}
cd ../backend
```

```shell-session Shell theme={null}
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.

```ts TypeScript theme={null}
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 Shell theme={null}
cd ../app
```

```shell-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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.

```ts TypeScript theme={null}
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-session Shell theme={null}
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.

```ts TypeScript theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
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-session Shell theme={null}
kind delete cluster --name=cdktf-app Deleting cluster "cdktf-app" ...
```

```shell-session Shell theme={null}
docker stop local-registry local-registry
```

```shell-session Shell theme={null}
docker rm local-registry local-registry
```

## Next steps

* Review how to structure CDKTN code with [Constructs](/concepts/constructs) and [Stacks](/concepts/stacks).
* If you want a smaller starting point, begin with the [Install tutorial](/tutorials/install) or [Build AWS infrastructure](/tutorials/build-aws).
