Como Implementar Aplicações Web Balanceadas por Carga na DigitalOcean com CDK para Terraform e TypeScript

O autor selecionou a Fundação Wikimedia para receber uma doação como parte do programa Write for DOnations.

Introdução

A Infraestrutura como Código (IaC) é uma prática de automatizar a implantação e modificações da infraestrutura, definindo os estados dos recursos e suas relações em código. A execução desse código então cria ou modifica os recursos reais na nuvem. A IaC permite que os engenheiros usem uma ferramenta de IaC como o Terraform (da HashiCorp) para provisionamento de infraestrutura.

Com a IaC, as alterações na sua infraestrutura podem passar pelo mesmo processo de revisão de código que o seu código de aplicativo. Você pode armazenar o código em controle de versão (como o Git) para manter um histórico do estado de sua infraestrutura, e você pode automatizar ainda mais o processo de implantação com ferramentas de nível mais alto, como uma plataforma interna de desenvolvedores (IDP).

Terraform é uma ferramenta popular de IaC (Infrastructure as Code) agnóstica de plataforma devido ao seu amplo suporte para muitas plataformas, incluindo o GitHub, Cloudflare e DigitalOcean. A maioria das configurações do Terraform são escritas usando uma linguagem declarativa chamada de HashiCorp Configuration Language (HCL).

O Cloud Development Kit for Terraform (CDKTF) é uma ferramenta construída em cima do Terraform que permite definir infraestrutura usando uma linguagem de programação familiar (como TypeScript, Python ou Go) em vez de HCL. Esta ferramenta pode fornecer uma curva de aprendizado mais suave para desenvolvedores não familiarizados com HCL, enquanto permite que desenvolvedores usem recursos de programação nativos como loops, variáveis e funções. Neste tutorial, você começará instalando a ferramenta de linha de comando (CLI) cdktf. Em seguida, você irá criar um projeto CDKTF em TypeScript e definir o projeto com dois servidores NGINX balanceados por um balanceador de carga. Você então usará o cdktf para implantar a infraestrutura. No final deste tutorial, você terá um projeto CDKTF a partir do qual poderá expandir sua infraestrutura.

Neste tutorial, você começará instalando a ferramenta de interface de linha de comando (CLI) cdktf. Em seguida, você criará um projeto CDKTF em TypeScript e definirá o projeto com dois servidores NGINX que são balanceados por um balanceador de carga. Você usará então cdktf para implantar a infraestrutura. No final deste tutorial, você terá um projeto CDKTF a partir do qual poderá expandir sua infraestrutura.

Nota: Este tutorial foi testado com CDKTF 0.11.2 e Terraform 1.2.2.

Pré-requisitos

Para concluir este tutorial, você precisará:

Passo 1 — Instalando o CLI cdktf

Para começar, você instalará a ferramenta de linha de comando cdktf.

O CLI cdktf está disponível como um pacote NPM. Se você pesquisar por cdktf no npmjs.com, encontrará dois pacotes com nomes semelhantes: cdktf e cdktf-cli.

Conceitualmente, o CDKTF é uma camada de abstração em cima do Terraform. Consiste em duas partes:

  • uma biblioteca contendo um conjunto de construções nativas da linguagem (como funções e classes) para definir infraestrutura. Essa parte é encapsulada dentro do pacote npm cdktf. Por exemplo, você pode ver o uso das classes App e TerraformStack do pacote cdktf no seguinte projeto de exemplo do CDKTF:

    import { App, TerraformStack } from "cdktf";
    class APIStack extends TerraformStack {}
    const app = new App();
    new APIStack(app, "feature-x");
    app.synth();
    
  • Um adaptador que analisa os construtos dentro do projeto CDKTF e os reduz a um conjunto de documentos JSON, que são então ingeridos no Terraform da mesma forma que o HCL é ingerido. Este adaptador é encapsulado em uma ferramenta CLI chamada cdktf, fornecida pelo pacote cdktf-cli.

Para instalar a ferramenta CLI cdktf, você precisa do pacote cdktf-cli. Você pode instalar este pacote globalmente usando npm, yarn ou um gerenciador de pacotes de sua escolha.

Para instalar cdktf-cli com npm, execute o seguinte:

  1. npm install --global [email protected]

Observação: Provavelmente haverá uma versão mais recente do pacote cdktf-cli após a publicação deste artigo. Você pode tentar seguir o tutorial com a versão mais recente executando npm install --global cdktf-cli@latest em vez disso, mas esteja ciente de que algumas saídas podem diferir ligeiramente.

Alternativamente, você pode usar Homebrew no macOS ou Linux para instalar a CLI cdktf como a fórmula cdktf:Para verificar se a instalação foi bem-sucedida, execute o comando cdktf sem argumentos:

  1. brew install cdktf

Para verificar se a instalação foi bem-sucedida, execute o comando cdktf sem argumentos:

  1. cdktf

Você verá uma saída semelhante à seguinte:

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 Mostrar número da versão --disable-logging Não escrever arquivos de log. Suportado usando a variável de ambiente CDKTF_DISABLE_LOGGING. --disable-plugin-cache-env Não definir TF_PLUGIN_CACHE_DIR automaticamente. --log-level Qual nível de log deve ser escrito. -h, --help Show help Options can be specified via environment variables with the "CDKTF_" prefix (e.g. "CDKTF_OUTPUT")

A saída mostra as comandos disponíveis. Nas próximas etapas deste tutorial, você ganhará experiência usando cdktf init, cdktf get, cdktf deploy e cdktf destroy.

Agora que você instalou o CLI cdktf, você pode definir a infraestrutura escrevendo algum código TypeScript.

Passo 2 – Criando um Novo Projeto CDKTF

Neste passo, você usará o CLI cdktf que acabou de instalar para criar um projeto CDKTF padrão, no qual você construirá nas próximas etapas.

Crie um diretório que abrigará o projeto CDKTF executando o seguinte comando:

  1. mkdir infra

Em seguida, navegue até o diretório recém-criado:

  1. cd infra/

Use o comando cdktf init para criar um esqueleto de projeto CDKTF no qual você construirá:

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

O CDKTF permite aos desenvolvedores definir infraestrutura usando TypeScript, Python, Java, C# ou Go. A opção --template=typescript diz ao cdktf para criar este projeto CDKTF usando TypeScript.

O Terraform (e assim o CDKTF) mantém o controle dos recursos que está gerenciando registrando suas definições e estados em arquivos chamados arquivos de estado do Terraform. A opção --local diz ao CDKTF para manter esses arquivos de estado localmente na máquina que está executando o cdktf (cada arquivo segue a estrutura de nomeação terraform.<stack>.tfstate).

Após executar o comando, o CLI pode pedir sua permissão para enviar relatórios de falhas à equipe do CDKTF para ajudá-los a melhorar o produto:

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)

Digite Y se você deseja consentir ou N se discordar, depois pressione ENTER.

cdktf então criará o esqueleto do projeto e instalará os pacotes. Quando o projeto estiver esqueletizado, você verá uma saída semelhante ao seguinte:

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)

Você também verá alguns novos arquivos adicionados ao diretório infra. Os arquivos mais importantes são cdktf.json e main.ts.

cdktf.json é o arquivo de configuração do projeto CDKTF. Se você abrir o arquivo, ele exibirá algo como o seguinte:A propriedade app define o comando que será executado para sintetizar o código TypeScript em JSON compatível com o Terraform. Esta propriedade indica que main.ts é o ponto de entrada para o projeto CDKTF.

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

A propriedade app define o comando que será executado para sintetizar o código TypeScript em JSON compatível com o Terraform. Essa propriedade indica que main.ts é o ponto de entrada para o projeto CDKTF.

Se você abrir o arquivo main.ts, verá algo semelhante ao seguinte:

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

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

    // defina recursos aqui
  }
}

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

Na linguagem do CDKTF, um conjunto de recursos de infraestrutura relacionados pode ser agrupado em um pilha. Por exemplo, os recursos que compõem um aplicativo de API, como Droplets, balanceadores de carga e registros DNS, podem ser agrupados em uma única pilha chamada APIStack. Cada pilha mantém seu próprio estado e pode ser implantada, modificada ou destruída independentemente de outras pilhas. Um uso comum de pilhas é ter uma pilha para produção e uma pilha separada para desenvolvimento.

Um aplicativo é um contêiner para várias pilhas. Por exemplo, um aplicativo pode agrupar as pilhas de vários microsserviços.

O projeto CDKTF gerado no esqueleto de main.ts contém uma única classe de pilha chamada MyStack, atualmente definindo nenhum recurso. Uma instância de MyStack é criada com o nome infra, contida dentro de um aplicativo chamado app. Nas etapas subsequentes, você definirá recursos de infraestrutura dentro do construtor MyStack.

Após criar o projeto, o próximo passo é configurar o projeto CDKTF com provedores.

Passo 3 — Instalação do Provedor DigitalOcean

Neste passo, você instalará o Provedor DigitalOcean no projeto CDKTF.

Provedores são bibliotecas que fornecem instruções para o Terraform (que é usado pelo cdktf por baixo dos panos) sobre como criar, atualizar e excluir recursos em provedores de nuvem, provedores SaaS e outras plataformas que expõem interfaces de programação de aplicativos (APIs). Os provedores encapsulam a lógica de chamada dessas APIs de upstream em funções padrão que o Terraform pode chamar.

Por exemplo, se você fosse criar um novo Droplet DigitalOcean sem o Terraform, você teria que enviar uma solicitação POST para o endpoint /v2/droplets do API DigitalOcean. Com o Terraform, você instalaria o provedor DigitalOcean e definiria um recurso digitalocean_droplet, semelhante ao seguinte trecho de amostra:

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

Em seguida, você pode usar a ferramenta CLI cdktf para traduzir esse código TypeScript em JSON compatível com o Terraform e passá-lo para o provedor, que fará as chamadas de API apropriadas para criar o Droplet em seu nome.

Agora que você entende o que é um provedor, pode configurar o provedor DigitalOcean para seu projeto CDKTF.

Abra o arquivo cdktf.json e adicione a string digitalocean/digitalocean à matriz 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 é o identificador para o provedor DigitalOcean no Terraform Registry.

Salve e feche o arquivo.

Em seguida, execute cdktf get para baixar e instalar o provedor.

  1. cdktf get

cdktf get irá baixar o provedor, extrair o esquema, gerar as classes TypeScript correspondentes e adicioná-lo como um módulo TypeScript sob .gen/providers/. Essa geração automática de código permite que você use quaisquer provedores Terraform e módulos HCL com CDKTF, e é assim que CDKTF pode fornecer a conclusão de código em editores que oferecem suporte a ele.

Depois que cdktf get terminar de ser executado, você verá uma saída semelhante à seguinte:

Output
Generated typescript constructs in the output directory: .gen

Você também verá um novo diretório chamado .gen contendo o código gerado do provedor.

Nesta etapa, você instalou o provedor digitalocean/digitalocean no projeto. Na próxima etapa, você configurará o provedor DigitalOcean com as credenciais necessárias para autenticar o provedor com a API DigitalOcean.

Etapa 4 — Configurando o Provedor DigitalOcean

Nesta etapa, você configurará o provedor DigitalOcean com seu Token de Acesso Pessoal DigitalOcean, o que permite que o provedor chame a API DigitalOcean em seu nome.

Provedores diferentes requerem e suportam diferentes credenciais para autenticar com a API upstream. Para o provedor DigitalOcean, você precisa fornecer seu Token de Acesso Pessoal DigitalOcean. Você pode especificar o token para o provedor definindo-o como as variáveis de ambiente DIGITALOCEAN_TOKEN ou DIGITALOCEAN_ACCESS_TOKEN.

Execute o seguinte comando no seu terminal para definir a variável de ambiente para essa sessão de terminal.

  1. export DIGITALOCEAN_ACCESS_TOKEN="your_personal_access_token"

Nota: Ao chamar export, você está definindo a variável de ambiente apenas para essa sessão de terminal. Se você fechar e reabrir o terminal ou executar os comandos cdktf em um terminal diferente, precisará executar o comando export novamente para que a variável de ambiente entre em vigor.

Em seguida, você especificará o provedor dentro da classe MyStack, o que permitirá definir recursos fornecidos pelo provedor dentro de sua pilha. Atualize o arquivo main.ts para o seguinte:

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();

O módulo para o provedor está localizado em ./.gen/providers/digitalocean, que foi gerado automaticamente quando você executou cdktf get.

Você configurou o provedor digitalocean/digitalocean com credenciais nesta etapa. Em seguida, você começará a definir a infraestrutura que faz parte do objetivo deste tutorial.

Passo 5 — Definindo Aplicações Web em Droplets

Nesta etapa, você definirá dois servidores NGINX, cada um servindo arquivos diferentes, implantados em dois Droplets Ubuntu 20.04 idênticos.

Você começa com a definição dos dois Droplets. Modifique o main.ts com as alterações realçadas:

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',
      })
    )
  }
}

Você usa um loop nativo em JavaScript (Array.prototype.map()) para evitar duplicação no código.

Assim como se estivesse criando o Droplet através do console, existem vários parâmetros a serem especificados:

  • image – a distribuição e versão do Linux que o Droplet executará.
  • region – o datacenter onde o Droplet será executado.
  • size – a quantidade de recursos de CPU e memória a serem reservados para o Droplet.
  • name – um nome único usado para se referir ao Droplet.

Os valores para image, region e size devem ser coisas que o DigitalOcean suporta. Você pode encontrar os valores válidos (chamados de slugs) para todas as distribuições do Linux, tamanhos de Droplet e regiões suportadas na página DigitalOcean API Slugs. Você pode encontrar uma lista completa de atributos obrigatórios e opcionais na página de documentação do digitalocean_droplet.

Adicionando uma Chave SSH

Como parte dos pré-requisitos, você carregou uma chave pública SSH sem senha em sua conta do DigitalOcean e anotou seu nome. Agora, você usará esse nome para recuperar o ID da chave SSH e passá-lo na definição do seu Droplet.

Como a chave SSH foi adicionada manualmente à sua conta do DigitalOcean, ela não é um recurso gerenciado pela sua configuração atual do Terraform. Se você tentasse definir um novo recurso digitalocean_ssh_key, ele criaria uma nova chave SSH em vez de usar a existente.

Em vez disso, você definirá uma nova digitalocean_ssh_key fonte de dados. No Terraform, fontes de dados são usadas para recuperar informações sobre infraestrutura que não são gerenciadas pela configuração atual do Terraform. Em outras palavras, elas fornecem uma visão somente leitura do estado de infraestrutura externa existente. Uma vez definida uma fonte de dados, você pode usar os dados em outro lugar em sua configuração do Terraform.

Ainda no main.ts e dentro do construtor de MyStack, defina uma nova fonte de dados DataDigitaloceanSshKey e passe o nome que você atribuiu à sua chave SSH (aqui, o nome é 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, {
    ...
  }
}
...

Em seguida, atualize a definição do Droplet para incluir a chave 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()]
}))
...

Ao ser provisionado, você pode acessar o Droplet usando uma chave SSH privada em vez de uma senha.

Especificando Script de Dados do Usuário para Instalar o NGINX

Agora você definiu dois Droplets idênticos rodando Ubuntu, configurados com acesso SSH. A próxima tarefa é instalar o NGINX em cada Droplet.

Quando um Droplet está sendo criado, uma ferramenta chamada CloudInit fará o bootstrap do servidor. O CloudInit pode aceitar um arquivo chamado dados do usuário, que pode modificar como o servidor é bootstrapped. Os dados do usuário podem ser quaisquer arquivos cloud-config ou scripts que o servidor possa interpretar, como scripts Bash.

No restante deste passo, você criará um script Bash e o especificará como os dados do usuário do Droplet. O script instalará o NGINX como parte do processo de bootstrap. Além disso, o script também substituirá o conteúdo do arquivo /var/www/html/index.html (o arquivo padrão servido pelo NGINX) com o nome de host e o endereço IP do Droplet, o que fará com que os dois servidores NGINX sirvam arquivos diferentes. No próximo passo, você colocará ambos esses servidores NGINX por trás de um balanceador de carga; ao servir arquivos diferentes, ficará evidente se o balanceador de carga está distribuindo solicitações corretamente ou não.

Ainda no main.ts, adicione uma nova propriedade userData ao objeto de configuração do 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
`
    }))
  }
}

Aviso: Certifique-se de que não haja novas linhas antes do shebang (#!); caso contrário, o script pode não ser executado.

Quando o Droplet for provisionado pela primeira vez, o script será executado como o usuário root. Ele usará o gerenciador de pacotes do Ubuntu, APT, para instalar o pacote nginx. Em seguida, usará o Metadata Service da DigitalOcean para recuperar informações sobre si mesmo e gravar o nome de host e o endereço IP em index.html, que é servido pelo NGINX.

Neste passo, você definiu os dois Droplets executando o Ubuntu, configurou cada um com acesso SSH e instalou o NGINX usando o recurso de dados do usuário. No próximo passo, você definirá um balanceador de carga que ficará na frente desses servidores NGINX e configurará-o para balancear a carga de forma round-robin.

Passo 6 — Definindo um Balanceador de Carga

Neste passo, você definirá um Balanceador de Carga DigitalOcean ao definir uma instância do recurso digitalocean_loadbalancer.

Ainda no main.ts, adicione a seguinte definição para um balanceador de carga no final do construtor 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))
    })
  }
}
...

O argumento forwardingRule informa ao balanceador de carga para escutar solicitações HTTP na porta 80 e encaminhá-las para cada um dos Droplets na porta 80.

Os dropletIds especificam os Droplets para os quais o balanceador de carga encaminhará solicitações. Ele aceita um número, mas o valor de droplet.id é uma string. Portanto, você usou a Fn.tonumber função Terraform para converter o valor de ID de Droplet de string para um número.

Nota: Você usou a função Terraform Fn.tonumber aqui em vez da parseInt nativa do JavaScript porque o valor de droplet.id é desconhecido até que o Droplet seja provisionado. As funções Terraform são projetadas para operar em valores de tempo de execução desconhecidos antes de o Terraform aplicar uma configuração.

Salve e feche o arquivo.

Você agora definiu dois Droplets e um balanceador de carga que fica na frente deles. Seu main.ts deve ser semelhante a isto:

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();

Na próxima etapa, você usará a ferramenta CLI cdktf para materializar todo o seu projeto CDKTF.

Etapa 7 – Provisionamento da sua Infraestrutura

Nesta etapa, você usará a ferramenta CLI cdktf para provisionar os Droplets e os balanceadores de carga que você definiu nas etapas anteriores.

Certifique-se de que você está na pasta infra/ e definiu a variável de ambiente DIGITALOCEAN_ACCESS_TOKEN para a sessão do terminal, então execute o comando cdktf deploy:

  1. cdktf deploy

Você deve ver uma saída semelhante ao seguinte:

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á criado + 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á criado + 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 ainda está sendo desenvolvido e a saída pode diferir da mostrada acima.

Esta exibição lista todos os recursos e propriedades que cdktf planeja criar, atualizar e destruir. Alguns valores, como o ID de um Droplet, só são conhecidos após o provisionamento do recurso. Para esses, você verá (conhecido após aplicar) como o valor da propriedade na saída.

Revise a lista de recursos para garantir que seja o que você espera. Em seguida, use as teclas de seta para selecionar a opção Aprovar e pressione ENTER.

Você verá uma saída semelhante à seguinte:

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 saída informa que cdktf está se comunicando com a API do DigitalOcean para criar o Droplet. cdktf está criando os Droplets primeiro porque o balanceador de carga depende do ID do Droplet, que é desconhecido até que os Droplets sejam provisionados.

A criação do Droplet geralmente leva menos de um minuto. Depois que os Droplets são provisionados, cdktf passa a criar o 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]

O balanceador de carga pode demorar mais. Após a criação do balanceador de carga, você verá um resumo que mostra que a pilha foi implantada com sucesso.

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.

Agora você pode visitar o console do DigitalOcean, onde pode ver um balanceador de carga chamado default e dois Droplets saudáveis chamados foo e bar, cada um servindo como um alvo para o balanceador de carga.

Você pode testar se o NGINX está funcionando e servindo conteúdo corretamente visitando o endereço IP de cada Droplet. Você deve ver um texto semelhante ao seguinte:

Droplet: bar, IP Address: droplet_ip

Se você não vir essa string de texto ou o servidor não estiver respondendo, verifique se os dados do usuário que você especificou estão corretos e que nenhum caractere (incluindo novas linhas) antecede o shebang (#!). Você também pode fazer SSH no Droplet usando sua chave privada SSH e revisar os logs de saída gerados por CloudInit em /var/log/cloud-init-output.log:

  1. ssh -i path_to_ssh_private_key root@droplet_ip

Uma vez que você tenha confirmado que os Droplets estão ativos e servindo conteúdo, você pode começar a testar o balanceador de carga. Você faz isso enviando alguns pedidos.

Execute o seguinte comando a partir do seu terminal para enviar dez pedidos ao balanceador de carga:

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

Você deve ver uma saída semelhante ao seguinte, embora os endereços IP mostrados possam ser 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

Isso mostra que os pedidos ao balanceador de carga foram encaminhados para cada Droplet cinco vezes, indicando que o balanceador de carga está funcionando.

Nota: O balanceador de carga pode não sempre equilibrar entre os dois Droplets perfeitamente; você pode descobrir que quatro pedidos foram enviados para um Droplet e seis para o outro. Esse comportamento é normal.

Neste passo, você usou cdktf para provisionar seus recursos e depois usou o console da DigitalOcean para descobrir os endereços IP de seus Droplets e balanceador de carga. Em seguida, você enviou pedidos para cada Droplet e balanceador de carga para confirmar que eles funcionam.

No próximo passo, você obterá os endereços IP dos Droplets e do balanceador de carga sem fazer login no console da DigitalOcean.

Passo 8 — Saída de Informações

No passo anterior, você teve que fazer login no Console da DigitalOcean para obter os endereços IP do seu Droplet e do balanceador de carga. Neste passo, você modificará seu código ligeiramente para que essas informações sejam impressas na saída do comando cdktf deploy, economizando uma viagem ao console.

O Terraform registra a configuração e o estado de seus recursos gerenciados em arquivos de estado. Para sua pilha infra, o arquivo de estado pode ser encontrado em infra/terraform.infra.tfstate. Você poderá encontrar os endereços IP dos Droplets e do balanceador de carga dentro deste arquivo de estado.

No entanto, percorrer um arquivo grande pode ser inconveniente. O CDKTF fornece o construtor TerraformOutput, que você pode usar para produzir variáveis e torná-las disponíveis fora da pilha. Quaisquer saídas são impressas em stdout após a execução de cdktf deploy. A execução de cdktf output também pode imprimir saídas a qualquer momento.

Nota: Embora você use apenas saídas para imprimir informações no console neste tutorial, seu verdadeiro poder vem de pilhas usando saídas de outras pilhas como entrada, um recurso conhecido como referências entre pilhas.

Atualize o arquivo main.ts para incluir saídas dos endereços IP do balanceador de carga e dos 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
    }))
  }
}
...

Salve e feche o arquivo.

Execute cdktf deploy para concretizar a mudança:

  1. cdktf deploy

Dentro da saída, você deve ver algo semelhante ao seguinte:

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. ─────────────────────────────────────────────────────────────────────────────

Esse resultado informa que não serão feitas alterações na infraestrutura, apenas o que é produzido pela pilha.

Use as setas para selecionar Aprovar e, em seguida, pressione ENTER. No final da saída do terminal, você deve ver algo semelhante a:

Output
infra droplet0IP = droplet_foo_ip droplet1IP = droplet_bar_ip loadBalancerIP = load_balancer_ip

Agora, cada vez que você executar cdktf deploy ou cdktf output, o endereço IP dos Droplets e dos balanceadores de carga são impressos na saída do terminal, eliminando a necessidade de acessar essas informações pelo console da DigitalOcean.

Você agora provisionou dois Droplets e um balanceador de carga e confirmou que eles estão funcionando. Você pode usar o projeto CDKTF que desenvolveu como base para definir uma infraestrutura mais sofisticada (você pode encontrar uma implementação de referência em do-community / digitalocean-cdktf-typescript).

Os recursos provisionados neste tutorial incorrerão em cobrança. Se você não pretende usar a infraestrutura criada, deve destruí-la. No próximo e último passo, você limpará o projeto destruindo os recursos criados neste tutorial.

Passo 9 — Destruindo Sua Infraestrutura

Neste passo, você removerá todos os recursos criados neste tutorial.

Ainda dentro da pasta infra/, execute cdktf destroy:

  1. cdktf destroy

Você deve ver uma saída semelhante à seguinte:

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á destruído - 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á destruído - 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

Desta vez, em vez de mostrar + ao lado de cada recurso, ele mostra -, indicando que o CDKTF planeja destruir o recurso. Examine as alterações propostas e, em seguida, use as teclas de seta para selecionar Aprovar e pressione ENTER. O provedor DigitalOcean agora se comunicará com a API do DigitalOcean para destruir os 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]

O balanceador de carga foi excluído primeiro porque não possui dependências (nenhum outro recurso referencia o balanceador de carga em suas entradas). Como o balanceador de carga referencia os Droplets, eles só podem ser destruídos após o balanceador de carga ser destruído.

Após os recursos terem sido destruídos, você verá a seguinte linha impressa na saída:

Output
Destroy complete! Resources: 3 destroyed.

Conclusão

Neste tutorial, você usou o CDKTF para provisionar e destruir uma página web com balanceamento de carga, consistindo em dois Droplets do DigitalOcean executando servidores NGINX, servidos atrás de um balanceador de carga. Você também exibiu informações sobre os recursos no terminal.

CDKTF é uma camada de abstração acima do Terraform. Uma boa compreensão do Terraform é útil para entender o CDKTF. Se você gostaria de aprender mais sobre o Terraform, você pode ler a série Como Gerenciar Infraestrutura com Terraform, que cobre o Terraform em profundidade.

Você também pode conferir a documentação oficial do CDK for Terraform e os tutoriais para aprender mais sobre o CDKTF.

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