Cómo desplegar aplicaciones web balanceadas en carga en DigitalOcean con CDK para Terraform y TypeScript

El autor seleccionó la Fundación Wikimedia para recibir una donación como parte del programa Write for Donations.

Introducción

La Infraestructura como Código (IaC) es una práctica de automatización de implementación y modificaciones de infraestructura al definir los estados de los recursos y sus relaciones en código. La ejecución de ese código luego crea o modifica los recursos reales en la nube. La IaC permite a los ingenieros utilizar una herramienta de IaC como Terraform (de HashiCorp) para el aprovisionamiento de infraestructura.

Con IaC, los cambios en su infraestructura pueden pasar por el mismo proceso de revisión de código que su código de aplicación. Puede almacenar el código en control de versiones (como Git) para mantener un historial del estado de su infraestructura, y puede automatizar aún más el proceso de implementación con herramientas de nivel superior como una plataforma de desarrollador interno (IDP) de autoservicio.

Terraform es una herramienta popular de Infraestructura como Código (IaC, por sus siglas en inglés) que es independiente de la plataforma debido a su amplio soporte para muchas plataformas, incluyendo GitHub, Cloudflare y DigitalOcean. La mayoría de las configuraciones de Terraform se escriben utilizando un lenguaje declarativo llamado HashiCorp Configuration Language (HCL).

El Kit de Desarrollo en la Nube para Terraform (CDKTF) es una herramienta construida sobre Terraform que te permite definir infraestructura utilizando un lenguaje de programación familiar (como TypeScript, Python o Go) en lugar de HCL. Esta herramienta puede proporcionar una curva de aprendizaje más suave para los desarrolladores que no están familiarizados con HCL, al tiempo que permite a los desarrolladores utilizar características de programación nativas como bucles, variables y funciones.

En este tutorial, comenzarás instalando la interfaz de línea de comandos (CLI, por sus siglas en inglés) cdktf. Luego, crearás un proyecto CDKTF en TypeScript y definirás el proyecto con dos servidores NGINX que serán balanceados por un balanceador de carga. Después, utilizarás cdktf para implementar la infraestructura. Al final de este tutorial, tendrás un proyecto CDKTF desde el cual podrás expandir tu infraestructura.

Nota: Este tutorial ha sido probado con CDKTF 0.11.2 y Terraform 1.2.2.

Prerrequisitos

Para completar este tutorial, necesitarás:

Paso 1 — Instalación de la CLI cdktf

Para comenzar, instalarás la herramienta de línea de comandos cdktf.

La CLI cdktf está disponible como un paquete NPM. Si buscas cdktf en npmjs.com, encontrarás dos paquetes con nombres similares: cdktf y cdktf-cli.

Conceptualmente, CDKTF es una capa de abstracción sobre Terraform. Consta de dos partes:

  • una biblioteca que contiene un conjunto de construcciones nativas del lenguaje (como funciones y clases) para definir infraestructura. Esta parte está encapsulada dentro del paquete npm cdktf. Por ejemplo, puedes ver el uso de las clases App y TerraformStack del paquete cdktf en el siguiente proyecto de muestra de CDKTF:

    importar { App, TerraformStack } desde "cdktf";
    clase APIStack extiende TerraformStack {}
    const app = nuevo App();
    nuevo APIStack(app, "feature-x");
    app.synth();
    
  • Un adaptador que analiza las construcciones dentro del proyecto CDKTF y las reduce a un conjunto de documentos JSON, que luego se ingieren en Terraform de la misma manera que se ingiere HCL. Este adaptador está encapsulado en una herramienta CLI llamada cdktf, proporcionada por el paquete cdktf-cli.

Para instalar la herramienta CLI cdktf, necesitas el paquete cdktf-cli. Puedes instalar este paquete globalmente usando npm, yarn, o un administrador de paquetes de tu elección.

Para instalar cdktf-cli con npm, ejecuta lo siguiente:

  1. npm install --global [email protected]

Nota: Es probable que haya una versión más nueva del paquete cdktf-cli después de la publicación de este artículo. Puedes intentar seguir el tutorial con la última versión ejecutando npm install --global cdktf-cli@latest en su lugar, pero ten en cuenta que algunas salidas pueden diferir ligeramente.

Alternativamente, puedes usar Homebrew en macOS o Linux para instalar la CLI cdktf como la fórmula cdktf:

  1. brew install cdktf

Para verificar que la instalación haya sido exitosa, ejecuta el comando cdktf sin argumentos:

  1. cdktf

Verás una salida similar a la siguiente:

Output
Please pass a command to cdktf, here are all available ones: cdktf Commands: cdktf init Create a new cdktf project from a template. cdktf get Generate CDK Constructs for Terraform providers and modules. cdktf convert Converts a single file of HCL configuration to CDK for Terraform. cdktf deploy [stacks...] Deploy the given stacks cdktf destroy [stacks..] Destroy the given stacks cdktf diff [stack] Perform a diff (terraform plan) for the given stack cdktf list List stacks in app. cdktf login Retrieves an API token to connect to Terraform Cloud. cdktf synth Synthesizes Terraform code for the given app in a directory. cdktf watch [stacks..] [experimental] Watch for file changes and automatically trigger a deploy cdktf output [stacks..] Prints the output of stacks cdktf debug Get debug information about the current project and environment cdktf completion generate completion script Options: --version Muestra el número de versión --disable-logging No escribe archivos de registro. Soportado usando la variable de entorno CDKTF_DISABLE_LOGGING. --disable-plugin-cache-env No establece automáticamente TF_PLUGIN_CACHE_DIR. --log-level Qué nivel de registro debe ser escrito. -h, --help Show help Options can be specified via environment variables with the "CDKTF_" prefix (e.g. "CDKTF_OUTPUT")

La salida muestra los comandos disponibles. En el resto de este tutorial, ganarás experiencia usando cdktf init, cdktf get, cdktf deploy y cdktf destroy.

Ahora que has instalado el CLI cdktf, puedes definir infraestructura escribiendo algo de código TypeScript.

Paso 2 — Creando un Nuevo Proyecto CDKTF

En este paso, utilizarás el CLI cdktf que acabas de instalar para crear un proyecto de CDKTF de plantilla, en el que trabajarás en pasos posteriores.

Crea un directorio que albergará el proyecto CDKTF ejecutando el siguiente comando:

  1. mkdir infra

Luego, navega al directorio recién creado:

  1. cd infra/

Usa el comando cdktf init para crear una estructura de proyecto CDKTF en la que trabajarás:

  1. cdktf init --template=typescript --project-name=base --project-description="Base architecture" --local

CDKTF permite a los desarrolladores definir infraestructura utilizando TypeScript, Python, Java, C# o Go. La opción --template=typescript le indica a cdktf que genere la estructura de este proyecto de CDKTF utilizando TypeScript.

Terraform (y por lo tanto CDKTF) realiza un seguimiento de los recursos que administra registrando sus definiciones y estados en archivos llamados archivos de estado de Terraform. La opción --local le indica a CDKTF que mantenga estos archivos de estado localmente en la máquina donde se ejecuta cdktf (cada archivo sigue la estructura de nombres terraform.<stack>.tfstate).

Después de ejecutar el comando, es posible que la interfaz de línea de comandos le pida permiso para enviar informes de fallos al equipo de CDKTF para ayudarlos a mejorar el producto:

Output
? Do you want to send crash reports to the CDKTF team? See https://www.terraform.io/cdktf/create-and-deploy/configuration-file for more information (Y/n)

Escriba Y si desea consentir o N si está en desacuerdo, luego presione ENTER.

cdktf luego creará la estructura del proyecto e instalará los paquetes. Cuando el proyecto esté estructurado, verá una salida similar a la siguiente:

Output
Your cdktf typescript project is ready! cat help Print this message Compile: npm run get Import/update Terraform providers and modules (you should check-in this directory) npm run compile Compile typescript code to javascript (or "npm run watch") npm run watch Watch for changes and compile typescript in the background npm run build Compile typescript Synthesize: cdktf synth [stack] Synthesize Terraform resources from stacks to cdktf.out/ (ready for 'terraform apply') Diff: cdktf diff [stack] Perform a diff (terraform plan) for the given stack Deploy: cdktf deploy [stack] Deploy the given stack Destroy: cdktf destroy [stack] Destroy the stack Test: npm run test Runs unit tests (edit __tests__/main-test.ts to add your own tests) npm run test:watch Watches the tests and reruns them on change Upgrades: npm run upgrade Upgrade cdktf modules to latest version npm run upgrade:next Upgrade cdktf modules to latest "@next" version (last commit)

También verá algunos archivos nuevos agregados al directorio infra. Los archivos más importantes son cdktf.json y main.ts.

cdktf.json es el archivo de configuración para el proyecto de CDKTF. Si abre el archivo, mostrará algo como lo siguiente:

cdktf.json
{
  "language": "typescript",
  "app": "npx ts-node main.ts",
  "projectId": "28c87598-4343-47a9-bb5d-8fb0e031c41b",
  "terraformProviders": [],
  "terraformModules": [],
  "context": {
    "excludeStackIdFromLogicalIds": "true",
    "allowSepCharsInLogicalIds": "true"
  }
}

La propiedad app define el comando que se ejecutará para sintetizar el código TypeScript a JSON compatible con Terraform. Esta propiedad indica que main.ts es el punto de entrada al proyecto de CDKTF.

Si abres el archivo main.ts, verás algo similar a lo siguiente:

main.ts
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";

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

    // define los recursos aquí
  }
}

const app = new App();
new MyStack(app, "infra");
app.synth();

En el lenguaje de CDKTF, una colección de recursos de infraestructura relacionados puede agruparse en un stack. Por ejemplo, los recursos que componen una aplicación de API, como Droplets, balanceadores de carga y registros DNS, pueden agruparse en un único stack llamado APIStack. Cada stack mantiene su propio estado y puede ser implementado, modificado o destruido de forma independiente de otros stacks. Un uso común de los stacks es tener un stack para producción y un stack separado para desarrollo.

Una aplicación es un contenedor para múltiples stacks. Por ejemplo, una aplicación puede agrupar los stacks de varios microservicios.

El proyecto CDKTF generado en el esquema de main.ts contiene una única clase de stack llamada MyStack, que actualmente no define ningún recurso. Se crea una instancia de MyStack con el nombre infra, contenido dentro de una aplicación llamada app. En pasos posteriores, definirás recursos de infraestructura dentro del constructor de MyStack.

Después de crear el proyecto, el siguiente paso es configurar el proyecto CDKTF con proveedores.

Paso 3 — Instalando el Proveedor DigitalOcean

Proveedores son bibliotecas que proporcionan instrucciones a Terraform (que se utiliza bajo el capó en cdktf) sobre cómo crear, actualizar y eliminar recursos en proveedores de nube, proveedores SaaS y otras plataformas que exponen interfaces de programación de aplicaciones (APIs). Los proveedores encapsulan la lógica de llamar a estas API upstream en funciones estándar que Terraform puede llamar.

Por ejemplo, si creara una nueva gota de DigitalOcean sin Terraform, tendría que enviar una solicitud POST al /v2/droplets del API de DigitalOcean. Con Terraform, en su lugar, instalaría el proveedor de DigitalOcean y definiría un recurso digitalocean_droplet, similar al siguiente fragmento de código de muestra:

new Droplet(this, 'web', {
  image: 'ubuntu-20-04-x64',
  name,
  region: 'lon1',
  size: 's-1vcpu-1gb',
}

A continuación, puede utilizar la herramienta de línea de comandos cdktf para traducir este código TypeScript en JSON compatible con Terraform y pasarlo al proveedor, que realizará las llamadas API adecuadas para crear la gota en su nombre.

Ahora que comprende qué es un proveedor, puede configurar el proveedor de DigitalOcean para su proyecto CDKTF.

Abre el archivo cdktf.json y agrega la cadena digitalocean/digitalocean al arreglo terraformProviders:

cdktf.json
{
  "language": "typescript",
  "app": "npx ts-node main.ts",
  "projectId": "28c87598-4343-47a9-bb5d-8fb0e031c41b",
  "terraformProviders": ["digitalocean/digitalocean"],
  "terraformModules": [],
  "context": {
    "excludeStackIdFromLogicalIds": "true",
    "allowSepCharsInLogicalIds": "true"
  }
}

digitalocean/digitalocean es el identificador del proveedor de DigitalOcean en el Registro de Terraform.

Guarda y cierra el archivo.

A continuación, ejecuta cdktf get para descargar e instalar el proveedor.

  1. cdktf get

cdktf get descargará el proveedor, extraerá el esquema, generará las clases TypeScript correspondientes y lo añadirá como un módulo TypeScript bajo .gen/providers/. Esta generación automática de código te permite utilizar cualquier proveedor de Terraform y módulos HCL con CDKTF, y es cómo CDKTF puede proporcionar completado de código en editores que lo admitan.

Una vez que cdktf get termine de ejecutarse, verás una salida similar a la siguiente:

Output
Generated typescript constructs in the output directory: .gen

También verás un nuevo directorio llamado .gen que contiene el código generado del proveedor.

En este paso, instalaste el proveedor digitalocean/digitalocean en el proyecto. En el siguiente paso, configurarás el proveedor de DigitalOcean con las credenciales necesarias para autenticar el proveedor con la API de DigitalOcean.

Paso 4 — Configurando el Proveedor de DigitalOcean

En este paso, configurarás el proveedor de DigitalOcean con tu Token de Acceso Personal de DigitalOcean, lo que permite al proveedor llamar a la API de DigitalOcean en tu nombre.

Diferentes proveedores requieren y admiten credenciales diferentes para autenticarse con la API correspondiente. Para el proveedor de DigitalOcean, debes proporcionar tu Token de Acceso Personal de DigitalOcean. Puedes especificar el token al proveedor configurándolo como las variables de entorno DIGITALOCEAN_TOKEN o DIGITALOCEAN_ACCESS_TOKEN.

Ejecuta el siguiente comando en tu terminal para establecer la variable de entorno para esa sesión de terminal.

  1. export DIGITALOCEAN_ACCESS_TOKEN="your_personal_access_token"

Nota: Al llamar a export, estás configurando la variable de entorno solo para esa sesión de terminal. Si cierras y vuelves a abrir la terminal o ejecutas los comandos de cdktf en una terminal diferente, deberás volver a ejecutar el comando export para que la variable de entorno surta efecto.

A continuación, especificarás el proveedor dentro de la clase MyStack, lo que te permitirá definir recursos proporcionados por el proveedor dentro de tu pila. Actualiza el archivo main.ts de la siguiente manera:

main.ts
import { Construct } from "constructs";
import { App, TerraformStack } from "cdktf";
import { DigitaloceanProvider } from "./.gen/providers/digitalocean"

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

    new DigitaloceanProvider(this, 'provider')
    
  }
}

const app = new App();
new MyStack(app, "infra");
app.synth();

El módulo para el proveedor se encuentra en ./.gen/providers/digitalocean, el cual se generó automáticamente cuando ejecutaste cdktf get.

Configuraste el proveedor digitalocean/digitalocean con credenciales en este paso. A continuación, comenzarás a definir la infraestructura que forma parte del objetivo de este tutorial.

Paso 5 — Definición de Aplicaciones Web en Droplets

En este paso, definirás dos servidores NGINX, cada uno sirviendo archivos diferentes, desplegados en dos Droplets Ubuntu 20.04 idénticos.

Comienzas con la definición de los dos Droplets. Modifica main.ts con los cambios resaltados:

main.ts
...
import { DigitaloceanProvider, Droplet } from "./.gen/providers/digitalocean"

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    ...
    const dropletNames = ['foo', 'bar']
    const droplets = dropletNames.map(name => new Droplet(this, name, {
        image: 'ubuntu-20-04-x64',
        name,
        region: 'lon1',
        size: 's-1vcpu-1gb',
      })
    )
  }
}

Utilizas un bucle nativo de JavaScript (Array.prototype.map()) para evitar duplicación en el código.

Al igual que si estuvieras creando el Droplet a través de la consola, hay varios parámetros que especificar:

  • image – la distribución de Linux y la versión en la que se ejecutará tu Droplet.
  • region – el centro de datos en el que se ejecutará el Droplet.
  • size – la cantidad de recursos de CPU y memoria que reservarás para el Droplet.
  • name – un nombre único utilizado para referirse al Droplet.

Los valores para imagen, región y tamaño deben ser cosas que DigitalOcean admita. Puede encontrar los valores válidos (llamados slugs) para todas las imágenes de distribución de Linux admitidas, tamaños de Droplet y regiones en la página Slugs de la API de DigitalOcean. Puede encontrar una lista completa de atributos requeridos y opcionales en la página de documentación de digitalocean_droplet.

Agregando una Clave SSH

Como parte de los requisitos previos, cargó una clave pública SSH sin contraseña en su cuenta de DigitalOcean y anotó su nombre. Ahora usará ese nombre para recuperar el ID de la clave SSH y pasarlo a la definición de su Droplet.

Dado que la clave SSH se agregó manualmente a su cuenta de DigitalOcean, no es un recurso gestionado por su configuración actual de Terraform. Si intentara definir un nuevo recurso digitalocean_ssh_key, creará una nueva clave SSH en lugar de usar la existente.

En su lugar, definirá un nuevo origen de datos digitalocean_ssh_key . En Terraform, los origenes de datos se utilizan para recuperar información sobre la infraestructura que no está gestionada por la configuración actual de Terraform. En otras palabras, proporcionan una vista de solo lectura del estado de la infraestructura externa preexistente. Una vez que se define un origen de datos, puede utilizar los datos en otro lugar de su configuración de Terraform.

Todavía en main.ts y dentro del constructor de MyStack, defina un nuevo origen de datos DataDigitaloceanSshKey, y pase el nombre que asignó a su clave SSH (aquí, el nombre es do_cdktf):

main.ts
...
import { DataDigitaloceanSshKey, DigitaloceanProvider, Droplet } from "./.gen/providers/digitalocean"

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    ...
    const dropletNames = ['foo', 'bar']
    const sshKey = new DataDigitaloceanSshKey(this, 'sshKey', {
      name: 'do_cdktf',
    })
    const droplets = dropletNames.map(name => new Droplet(this, name, {
    ...
  }
}
...

Luego, actualice la definición del Droplet para incluir la clave SSH:

main.ts
...
const droplets = dropletNames.map(name => new Droplet(this, name, {
  image: 'ubuntu-20-04-x64',
  name,
  region: 'lon1',
  size: 's-1vcpu-1gb',
  sshKeys: [sshKey.id.toString()]
}))
...

Una vez provisionado, puede acceder al Droplet utilizando una clave SSH privada en lugar de una contraseña.

Especificar el script de datos de usuario para instalar NGINX

Ahora ha definido dos Droplets idénticos que ejecutan Ubuntu, configurados con acceso SSH. La siguiente tarea es instalar NGINX en cada Droplet.

Cuando se crea un Droplet, una herramienta llamada CloudInit iniciará el servidor. CloudInit puede aceptar un archivo llamado datos de usuario, que puede modificar cómo se inicializa el servidor. Los datos de usuario pueden ser cualquier archivo cloud-config o scripts que el servidor pueda interpretar, como scripts Bash.

En el resto de este paso, creará un script Bash y lo especificará como datos de usuario del Droplet. El script instalará NGINX como parte del proceso de inicialización. Además, el script también reemplazará el contenido del archivo /var/www/html/index.html (el archivo predeterminado servido por NGINX) con el nombre de host y la dirección IP del Droplet, lo que hará que los dos servidores NGINX sirvan archivos diferentes. En el siguiente paso, colocará ambos servidores NGINX detrás de un balanceador de carga; al servir archivos diferentes, será evidente si el balanceador de carga está distribuyendo correctamente las solicitudes o no.

Todavía en main.ts, agregue una nueva propiedad userData al objeto de configuración del Droplet:

main.ts
...
class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    ...
    const droplets = dropletNames.map(name => new Droplet(this, name, {
      image: 'ubuntu-20-04-x64',
      name,
      region: 'lon1',
      size: 's-1vcpu-1gb',
      sshKeys: [sshKey.id.toString()],
      userData: `#!/bin/bash

apt-get -y update
apt-get -y install nginx
export HOSTNAME=$(curl -s http://169.254.169.254/metadata/v1/hostname)
export PUBLIC_IPV4=$(curl -s http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)
echo Droplet: $HOSTNAME, IP Address: $PUBLIC_IPV4 > /var/www/html/index.html
`
    }))
  }
}

Advertencia: Asegúrese de que no haya nuevas líneas antes del shebang (#!); de lo contrario, es posible que el script no se ejecute.

Cuando se provisiona el Droplet por primera vez, el script se ejecutará como usuario root. Utilizará el gestor de paquetes de Ubuntu, APT, para instalar el paquete nginx. Luego, utilizará el Servicio de Metadatos de DigitalOcean para recuperar información sobre sí mismo y escribirá el nombre de host y la dirección IP en index.html, el cual es servido por NGINX.

En este paso, se definieron dos Droplets que ejecutan Ubuntu, configurando cada uno con acceso SSH e instalando NGINX mediante la función de datos de usuario. En el siguiente paso, se definirá un balanceador de carga que se situará frente a estos servidores NGINX y se configurará para equilibrar la carga de manera circular.

Paso 6 — Definir un Balanceador de Carga

En este paso, se definirá un Balanceador de Carga de DigitalOcean mediante la definición de una instancia del recurso digitalocean_loadbalancer.

Todavía en main.ts, añade la siguiente definición para un balanceador de carga al final del constructor de MyStack:

main.ts
...
import { App, Fn, TerraformStack } from "cdktf";
import { DataDigitaloceanSshKey, DigitaloceanProvider, Droplet, Loadbalancer } from "./.gen/providers/digitalocean"

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    ...
    new Loadbalancer(this, 'lb', {
      name: 'default',
      region: 'lon1',
      algorithm: 'round_robin',
      forwardingRule: [{
        entryProtocol: 'http',
        entryPort: 80,
        targetProtocol: 'http',
        targetPort: 80,
      }],
      dropletIds: droplets.map((droplet) => Fn.tonumber(droplet.id))
    })
  }
}
...

El argumento forwardingRule indica al balanceador de carga que escuche las solicitudes HTTP en el puerto 80 y las reenvíe a cada uno de los Droplets en el puerto 80.

Los dropletIds especifican los Droplets a los que el balanceador de carga pasará las solicitudes. Toma un número, pero el valor de droplet.id es una cadena. Por lo tanto, has utilizado la función de Terraform Fn.tonumber para convertir el valor de la identificación de Droplet de cadena a número.

Nota: Has utilizado la función de Terraform Fn.tonumber aquí en lugar de parseInt nativo de JavaScript porque el valor de droplet.id es desconocido hasta que se provisiona el Droplet. Las funciones de Terraform están diseñadas para operar en valores de tiempo de ejecución desconocidos antes de que Terraform aplique una configuración.

Guarda y cierra el archivo.

Ahora has definido dos Droplets y un balanceador de carga que está delante de ellos. Tu archivo main.ts debería lucir similar a esto:

main.ts
import { Construct } from "constructs";
import { App, Fn, TerraformStack } from "cdktf";
import { DataDigitaloceanSshKey, DigitaloceanProvider, Droplet, Loadbalancer } from "./.gen/providers/digitalocean"

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

    new DigitaloceanProvider(this, 'provider')

    const dropletNames = ['foo', 'bar']
    const sshKey = new DataDigitaloceanSshKey(this, 'sshKey', {
      name: 'do_cdktf',
    })
    const droplets = dropletNames.map(name => new Droplet(this, name, {
        image: 'ubuntu-20-04-x64',
        name,
        region: 'lon1',
        size: 's-1vcpu-1gb',
        sshKeys: [sshKey.id.toString()],
        userData: `#!/bin/bash

apt-get -y update
apt-get -y install nginx
export HOSTNAME=$(curl -s http://169.254.169.254/metadata/v1/hostname)
export PUBLIC_IPV4=$(curl -s http://169.254.169.254/metadata/v1/interfaces/public/0/ipv4/address)
echo Droplet: $HOSTNAME, IP Address: $PUBLIC_IPV4 > /var/www/html/index.html
`
      })
    )

    new Loadbalancer(this, 'lb', {
      name: 'default',
      region: 'lon1',
      algorithm: 'round_robin',
      forwardingRule: [{
        entryProtocol: 'http',
        entryPort: 80,
        targetProtocol: 'http',
        targetPort: 80,
      }],
      dropletIds: droplets.map((droplet) => Fn.tonumber(droplet.id))
    })
  }
}

const app = new App();
new MyStack(app, "infra");
app.synth();

En el siguiente paso, utilizarás la herramienta CLI cdktf para actualizar todo tu proyecto CDKTF.

Paso 7 — Provisionamiento de tu Infraestructura

En este paso, utilizarás la herramienta cdktf CLI para aprovisionar los Droplets y balanceadores de carga que definiste en los pasos anteriores.

Asegúrate de estar en el directorio infra/ y haber establecido la variable de entorno DIGITALOCEAN_ACCESS_TOKEN para tu sesión de terminal, luego ejecuta el comando cdktf deploy:

  1. cdktf deploy

Deberías ver una salida similar a la siguiente:

Output
infra Initializing the backend... infra Initializing provider plugins... infra - Reusing previous version of digitalocean/digitalocean from the dependency lock file infra - Using previously-installed digitalocean/digitalocean v2.19.0 infra Terraform has been successfully initialized! infra Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: infra # digitalocean_droplet.bar (bar) will be created + resource "digitalocean_droplet" "bar" { + backups = false + created_at = (known after apply) + disk = (known after apply) + graceful_shutdown = false + id = (known after apply) + image = "ubuntu-20-04-x64" + ipv4_address = (known after apply) + ipv4_address_private = (known after apply) + ipv6 = false + ipv6_address = (known after apply) + locked = (known after apply) + memory = (known after apply) + monitoring = false + name = "bar" + price_hourly = (known after apply) + price_monthly = (known after apply) + private_networking = (known after apply) + region = "lon1" + resize_disk = true + size = "s-1vcpu-1gb" + ssh_keys = [ + "34377800", ] + status = (known after apply) + urn = (known after apply) + user_data = "f9b1d9796d069fe504ce0d89439b6b664b14b1a1" + vcpus = (known after apply) + volume_ids = (known after apply) + vpc_uuid = (known after apply) } # digitalocean_droplet.foo (foo) será creado + resource "digitalocean_droplet" "foo" { + backups = false + created_at = (known after apply) + disk = (known after apply) + graceful_shutdown = false + id = (known after apply) + image = "ubuntu-20-04-x64" + ipv4_address = (known after apply) + ipv4_address_private = (known after apply) + ipv6 = false + ipv6_address = (known after apply) + locked = (known after apply) + memory = (known after apply) + monitoring = false + name = "foo" + price_hourly = (known after apply) + price_monthly = (known after apply) + private_networking = (known after apply) + region = "lon1" + resize_disk = true + size = "s-1vcpu-1gb" + ssh_keys = [ + "34377800", ] + status = (known after apply) + urn = (known after apply) + user_data = "f9b1d9796d069fe504ce0d89439b6b664b14b1a1" + vcpus = (known after apply) + volume_ids = (known after apply) + vpc_uuid = (known after apply) } # digitalocean_loadbalancer.lb (lb) será creado + resource "digitalocean_loadbalancer" "lb" { + algorithm = "round_robin" + disable_lets_encrypt_dns_records = false + droplet_ids = (known after apply) + enable_backend_keepalive = false + enable_proxy_protocol = false + id = (known after apply) + ip = (known after apply) + name = "default" + redirect_http_to_https = false + region = "lon1" + size_unit = (known after apply) + status = (known after apply) + urn = (known after apply) + vpc_uuid = (known after apply) + forwarding_rule { + certificate_id = (known after apply) + certificate_name = (known after apply) + entry_port = 80 + entry_protocol = "http" + target_port = 80 + target_protocol = "http" + tls_passthrough = false } + healthcheck { + check_interval_seconds = (known after apply) + healthy_threshold = (known after apply) + path = (known after apply) + port = (known after apply) + protocol = (known after apply) + response_timeout_seconds = (known after apply) + unhealthy_threshold = (known after apply) } + sticky_sessions { + cookie_name = (known after apply) + cookie_ttl_seconds = (known after apply) + type = (known after apply) } } Plan: 3 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 infra ❯ Approve Applies the changes outlined in the plan. Dismiss Stop

Nota: CDKTF aún está en desarrollo y la salida puede diferir de la que se muestra arriba.

Esta pantalla lista todos los recursos y propiedades que cdktf planea crear, actualizar y destruir. Algunos valores, como el ID de un Droplet, solo se conocen después de que el recurso se aprovisiona. Para esos, verás (conocido después de aplicar) como el valor de propiedad en la salida.

Revisa la lista de recursos para asegurarte de que sea lo que esperas. Luego, usa las teclas de flecha para seleccionar la opción Aprobar y presiona ENTER.

Verás una salida similar a la siguiente:

Output
infra digitalocean_droplet.foo (foo): Creating... digitalocean_droplet.bar (bar): Creating... infra digitalocean_droplet.bar (bar): Still creating... [10s elapsed] infra digitalocean_droplet.foo (foo): Still creating... [10s elapsed] 1 Stack deploying 0 Stacks done 0 Stacks waiting

Esta salida te indica que cdktf está comunicándose con la API de DigitalOcean para crear el Droplet. cdktf está creando los Droplets primero porque el balanceador de carga depende del ID del Droplet, que es desconocido hasta que los Droplets se aprovisionan.

La creación del Droplet generalmente demora menos de un minuto. Una vez que los Droplets se han aprovisionado, cdktf pasa a crear el balanceador de carga.

Output
infra digitalocean_droplet.bar (bar): Creation complete after 54s [id=298041598] infra digitalocean_droplet.foo (foo): Creation complete after 55s [id=298041600] infra digitalocean_loadbalancer.lb (lb): Creating... infra digitalocean_loadbalancer.lb (lb): Still creating... [10s elapsed]

El balanceador de carga puede tardar más. Después de que se haya creado el balanceador de carga, verás un resumen que muestra que la pila se ha implementado correctamente.

Output
infra digitalocean_loadbalancer.lb (lb): Still creating... [1m30s elapsed] infra digitalocean_loadbalancer.lb (lb): Creation complete after 1m32s [id=4f9ae2b7-b649-4fb4-beed-96b95bb72dd1] infra Apply complete! Resources: 3 added, 0 changed, 0 destroyed. No outputs found.

Ahora puedes visitar la consola de DigitalOcean, donde verás un balanceador de carga llamado default y dos Droplets saludables llamados foo y bar, cada uno sirviendo como objetivo para el balanceador de carga.

Puedes comprobar que NGINX está en funcionamiento y sirviendo contenido correctamente visitando la dirección IP de cada Droplet. Deberías ver un texto similar al siguiente:

Droplet: bar, IP Address: droplet_ip

Si no ves esa cadena de texto o el servidor no responde, verifica que los datos de usuario que especificaste sean correctos y que no haya caracteres (incluyendo saltos de línea) antes del shebang (#!). También puedes hacer SSH en el Droplet usando tu clave privada SSH y revisar los registros de salida generados por CloudInit en /var/log/cloud-init-output.log:

  1. ssh -i path_to_ssh_private_key root@droplet_ip

Una vez que hayas confirmado que los Droplets están activos y sirviendo contenido, puedes comenzar a probar el balanceador de carga. Lo haces enviando algunas solicitudes.

Ejecuta el siguiente comando desde tu terminal para enviar diez solicitudes al balanceador de carga:

  1. for run in {1..10}; do curl http://load_balancer_ip/; done

Deberías ver una salida similar a la siguiente, aunque las direcciones IP mostradas serán diferentes:

Output
Droplet: foo, IP Address: droplet_foo_ip Droplet: bar, IP Address: droplet_bar_ip Droplet: foo, IP Address: droplet_foo_ip Droplet: bar, IP Address: droplet_bar_ip Droplet: bar, IP Address: droplet_bar_ip Droplet: foo, IP Address: droplet_foo_ip Droplet: bar, IP Address: droplet_bar_ip Droplet: foo, IP Address: droplet_foo_ip Droplet: bar, IP Address: droplet_bar_ip Droplet: foo, IP Address: droplet_foo_ip

Muestra que las solicitudes al balanceador de carga fueron reenviadas a cada Droplet cinco veces, lo que indica que el balanceador de carga está funcionando.

Nota: El balanceador de carga no siempre distribuirá equitativamente entre los dos Droplets; es posible que encuentres que cuatro solicitudes fueron enviadas a un Droplet y seis a otro. Este comportamiento es normal.

En este paso, utilizaste cdktf para aprovisionar tus recursos, y luego utilizaste la consola de DigitalOcean para descubrir las direcciones IP de tus Droplets y balanceador de carga. Luego enviaste solicitudes a cada Droplet y balanceador de carga para confirmar que funcionan.

En el próximo paso, obtendrás las direcciones IP de los Droplets y el balanceador de carga sin iniciar sesión en la consola de DigitalOcean.

Paso 8 — Información de salida

En el paso anterior, tuviste que iniciar sesión en la Consola de DigitalOcean para obtener las direcciones IP de tu Droplet y balanceador de carga. En este paso, modificarás tu código ligeramente para que esta información se imprima en la salida del comando cdktf deploy, ahorrándote un viaje a la consola.

Terraform registra la configuración y el estado de sus recursos administrados en archivos de estado. Para tu pila infra, el archivo de estado se puede encontrar en infra/terraform.infra.tfstate. Podrás encontrar las direcciones IP de los Droplets y el balanceador de carga dentro de este archivo de estado.

Sin embargo, ordenar un archivo grande puede ser inconveniente. CDKTF proporciona el constructor TerraformOutput, que puedes usar para emitir variables y hacerlas disponibles fuera de la pila. Cualquier salida se imprime en stdout después de que se ejecute cdktf deploy. Ejecutar cdktf output también puede imprimir salidas en cualquier momento.

Nota: Aunque solo utilice salidas para imprimir información en la consola en este tutorial, su verdadero poder proviene de las pilas que utilizan salidas de otras pilas como entrada, una característica conocida como referencias entre pilas.

Actualice el archivo main.ts para incluir las salidas de las direcciones IP del balanceador de carga y los Droplets:

main.ts
import { Construct } from "constructs";
import { App, Fn, TerraformOutput, TerraformStack } from "cdktf";
import { DataDigitaloceanSshKey, DigitaloceanProvider, Droplet, Loadbalancer } from "./.gen/providers/digitalocean"

class MyStack extends TerraformStack {
  constructor(scope: Construct, name: string) {
    ...
    const lb = new Loadbalancer(this, 'lb', {
      ...
    })

    new TerraformOutput(this, "loadBalancerIP", {
      value: lb.ip,
    });

    droplets.forEach((droplet, index) => new TerraformOutput(this, `droplet${index}IP`, {
      value: droplet.ipv4Address
    }))
  }
}
...

Guarde y cierre el archivo.

Ejecute cdktf deploy para actualizar el cambio:

  1. cdktf deploy

Dentro de la salida, debería ver algo similar a lo siguiente:

Output
───────────────────────────────────────────────────────────────────────────── Changes to Outputs: + droplet0IP = "droplet_foo_ip" + droplet1IP = "droplet_bar_ip" + loadBalancerIP = "load_balancer_ip" You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure. ─────────────────────────────────────────────────────────────────────────────

Esta salida le indica que no se realizarán cambios en la infraestructura, solo lo que se produce como salida de la pila.

Use las teclas de flecha para seleccionar Aprobar, y luego presione ENTER. Al final de la salida del terminal, debería ver algo similar a:

Output
infra droplet0IP = droplet_foo_ip droplet1IP = droplet_bar_ip loadBalancerIP = load_balancer_ip

Ahora, cada vez que ejecute cdktf deploy o cdktf output, la dirección IP de los Droplets y los balanceadores de carga se imprimirán en la salida del terminal, eliminando la necesidad de acceder a esa información desde la consola de DigitalOcean.

Ahora ha provisionado dos Droplets y un balanceador de carga y ha confirmado que están funcionando. Puede utilizar el proyecto CDKTF que ha desarrollado como base para definir infraestructuras más sofisticadas (puede encontrar una implementación de referencia en do-community / digitalocean-cdktf-typescript).

Los recursos provisionados en este tutorial incurrirán en un cargo. Si no tienes la intención de utilizar la infraestructura creada, debes destruirla. En el siguiente y último paso, limpiarás el proyecto destruyendo los recursos creados en este tutorial.

Paso 9 — Destruyendo tu Infraestructura

En este paso, eliminarás todos los recursos creados en este tutorial.

Todavía dentro del directorio infra/, ejecuta cdktf destroy:

  1. cdktf destroy

Deberías ver una salida similar a la siguiente:

Output
infra Initializing the backend... infra Initializing provider plugins... infra - Reusing previous version of digitalocean/digitalocean from the dependency lock file infra - Using previously-installed digitalocean/digitalocean v2.19.0 infra Terraform has been successfully initialized! infra digitalocean_droplet.bar (bar): Refreshing state... [id=298041598] digitalocean_droplet.foo (foo): Refreshing state... [id=298041600] infra digitalocean_loadbalancer.lb (lb): Refreshing state... [id=4f9ae2b7-b649-4fb4-beed-96b95bb72dd1] infra Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: - destroy Terraform will perform the following actions: infra # digitalocean_droplet.bar (bar) will be destroyed - resource "digitalocean_droplet" "bar" { - backups = false -> null - created_at = "2022-05-02T10:04:16Z" -> null - disk = 25 -> null - graceful_shutdown = false -> null - id = "298041598" -> null - image = "ubuntu-20-04-x64" -> null - ipv4_address = "droplet_bar_public_ip" -> null - ipv4_address_private = "droplet_bar_private_ip" -> null - ipv6 = false -> null - locked = false -> null - memory = 1024 -> null - monitoring = false -> null - name = "bar" -> null - price_hourly = 0.00744 -> null - price_monthly = 5 -> null - private_networking = true -> null - region = "lon1" -> null - resize_disk = true -> null - size = "s-1vcpu-1gb" -> null - ssh_keys = [ - "34377800", ] -> null - status = "active" -> null - tags = [] -> null - urn = "do:droplet:298041598" -> null - user_data = "f9b1d9796d069fe504ce0d89439b6b664b14b1a1" -> null - vcpus = 1 -> null - volume_ids = [] -> null - vpc_uuid = "bed80b32-dc82-11e8-83ec-3cfdfea9f3f0" -> null } # digitalocean_droplet.foo (foo) será destruido - resource "digitalocean_droplet" "foo" { - backups = false -> null - created_at = "2022-05-02T10:04:16Z" -> null - disk = 25 -> null - graceful_shutdown = false -> null - id = "298041600" -> null - image = "ubuntu-20-04-x64" -> null - ipv4_address = "droplet_foo_public_ip" -> null - ipv4_address_private = "droplet_foo_private_ip" -> null - ipv6 = false -> null - locked = false -> null - memory = 1024 -> null - monitoring = false -> null - name = "foo" -> null - price_hourly = 0.00744 -> null - price_monthly = 5 -> null - private_networking = true -> null - region = "lon1" -> null - resize_disk = true -> null - size = "s-1vcpu-1gb" -> null - ssh_keys = [ - "34377800", ] -> null - status = "active" -> null - tags = [] -> null - urn = "do:droplet:298041600" -> null - user_data = "f9b1d9796d069fe504ce0d89439b6b664b14b1a1" -> null - vcpus = 1 -> null - volume_ids = [] -> null - vpc_uuid = "bed80b32-dc82-11e8-83ec-3cfdfea9f3f0" -> null } # digitalocean_loadbalancer.lb (lb) será destruido - resource "digitalocean_loadbalancer" "lb" { - algorithm = "round_robin" -> null - disable_lets_encrypt_dns_records = false -> null - droplet_ids = [ - 298041598, - 298041600, ] -> null - enable_backend_keepalive = false -> null - enable_proxy_protocol = false -> null - id = "4f9ae2b7-b649-4fb4-beed-96b95bb72dd1" -> null - ip = "load_balancer_ip" -> null - name = "default" -> null - redirect_http_to_https = false -> null - region = "lon1" -> null - size_unit = 1 -> null - status = "active" -> null - urn = "do:loadbalancer:4f9ae2b7-b649-4fb4-beed-96b95bb72dd1" -> null - vpc_uuid = "bed80b32-dc82-11e8-83ec-3cfdfea9f3f0" -> null - forwarding_rule { - entry_port = 80 -> null - entry_protocol = "http" -> nul infra l - target_port = 80 -> null - target_protocol = "http" -> null - tls_passthrough = false -> null } - healthcheck { - check_interval_seconds = 10 -> null - healthy_threshold = 5 -> null - path = "/" -> null - port = 80 -> null - protocol = "http" -> null - response_timeout_seconds = 5 -> null - unhealthy_threshold = 3 -> null } - sticky_sessions { - cookie_ttl_seconds = 0 -> null - type = "none" -> null } } Plan: 0 to add, 0 to change, 3 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 infra ❯ Approve Applies the changes outlined in the plan. Dismiss Stop

Esta vez, en lugar de mostrar + junto a cada recurso, muestra -, lo que indica que CDKTF planea destruir el recurso. Revisa los cambios propuestos, luego usa las teclas de flecha para seleccionar Aprobar y presiona ENTER. El proveedor de DigitalOcean ahora se comunicará con la API de DigitalOcean para destruir los recursos.

Output
infra digitalocean_loadbalancer.lb (lb): Destroying... [id=4f9ae2b7-b649-4fb4-beed-96b95bb72dd1] infra digitalocean_loadbalancer.lb (lb): Destruction complete after 1s infra digitalocean_droplet.bar (bar): Destroying... [id=298041598] digitalocean_droplet.foo (foo): Destroying... [id=298041600]

El balanceador de carga fue eliminado primero porque no tiene dependencias (ningún otro recurso hace referencia al balanceador de carga en sus entradas). Debido a que los Droplets hacen referencia al balanceador de carga, solo pueden ser destruidos después de que el balanceador de carga sea destruido.

Después de que los recursos hayan sido destruidos, verás la siguiente línea impresa en la salida:

Output
Destroy complete! Resources: 3 destroyed.

Conclusión

En este tutorial, utilizaste CDKTF para aprovisionar y destruir una página web balanceada de carga, que consta de dos Droplets de DigitalOcean ejecutando servidores NGINX, servidos detrás de un balanceador de carga. También generaste información sobre los recursos en la terminal.

CDKTF es una capa de abstracción sobre Terraform. Comprender Terraform es útil para entender CDKTF. Si deseas aprender más sobre Terraform, puedes leer la serie Cómo Administrar Infraestructura con Terraform, que cubre Terraform en profundidad.

También puedes consultar la documentación oficial de CDK para Terraform y los tutoriales para aprender más sobre CDKTF.

Source:
https://www.digitalocean.com/community/tutorials/how-to-deploy-load-balanced-web-applications-on-digitalocean-with-cdk-for-terraform-and-typescript