CDK for TerraformとTypeScriptを使用してDigitalOceanで負荷分散されたWebアプリケーションをデプロイする方法

著者は、寄付の一環としてウィキメディア財団を選択しました。

はじめに

インフラストラクチャーのコード化(IaC)は、コードでリソースの状態とそれらの関係を定義することによって、インフラストラクチャーの展開と変更を自動化する実践です。そのコードを実行すると、クラウド内の実際のリソースが作成または変更されます。IaCを使用すると、エンジニアはTerraformHashiCorpによる)などのIaCツールを使用してインフラストラクチャーをプロビジョニングできます。

IaCを使用すると、インフラストラクチャーの変更をアプリケーションコードと同じコードレビュープロセスを経ることができます。コードをバージョン管理(Gitなど)に格納して、インフラストラクチャーの状態の履歴を保持し、さらに高度なツール(自己サービス内部開発者プラットフォーム(IDP)など)を使用してデプロイメントプロセスを自動化できます。

Terraformは、その多くのプラットフォームに対する幅広いサポートにより、人気のあるプラットフォームに依存しないIaCツールです。これには、GitHub、Cloudflare、およびDigitalOceanなどの多くのプラットフォームが含まれます。ほとんどのTerraformの設定は、HashiCorp Configuration Language(HCL)と呼ばれる宣言型の言語を使用して書かれています。

TerraformのCloud Development Kit(CDKTF)は、Terraformの上に構築されたツールであり、HCLの代わりにTypeScript、Python、またはGoなどのなじみのあるプログラミング言語を使用してインフラストラクチャを定義することができます。このツールは、HCLに不慣れな開発者にとってより浅い学習曲線を提供しつつ、ループ、変数、および関数などのネイティブプログラミング機能を使用できるようにします。

このチュートリアルでは、まずcdktfコマンドラインインターフェース(CLI)ツールをインストールします。次に、TypeScriptでCDKTFプロジェクトを作成し、2つのNGINXサーバーを定義し、load balancerによって負荷分散されるプロジェクトを定義します。その後、cdktfを使用してインフラストラクチャをデプロイします。このチュートリアルの最後には、インフラストラクチャを拡張するためのCDKTFプロジェクトが得られます。

注意: このチュートリアルは CDKTF 0.11.2 および Terraform 1.2.2 でテストされています。

前提条件

このチュートリアルを完了するには、以下が必要です:

ステップ1 — cdktf CLIのインストール

まず、cdktfコマンドラインツールをインストールします。

cdktf CLIは、NPMパッケージとして利用できます。npmjs.comcdktfを検索すると、似た名前の2つのパッケージが見つかります:cdktfcdktf-cli

概念的には、CDKTFはTerraformの上にある抽象化レイヤーです。それは2つの部分から成り立っています。

  • 言語固有の構成要素(関数やクラスなど)を含むライブラリは、インフラストラクチャを定義するために使用されます。この部分は、cdktf npm パッケージ内にカプセル化されています。たとえば、次のサンプル CDKTF プロジェクトで、cdktf パッケージからの AppTerraformStack クラスの使用を見ることができます:

    import { App, TerraformStack } from "cdktf";
    class APIStack extends TerraformStack {}
    const app = new App();
    new APIStack(app, "feature-x");
    app.synth();
    
  • CDKTFプロジェクト内の構成要素を解析し、それらを一連のJSONドキュメントに変換し、それらをHCLと同じ方法でTerraformに取り込むアダプターです。このアダプターはcdktf-cliパッケージによって提供されるcdktfというCLIツールにカプセル化されています。

cdktf CLIツールをインストールするには、cdktf-cliパッケージが必要です。このパッケージは、npmyarn、または選択したパッケージマネージャーを使用してグローバルにインストールできます。

cdktf-clinpmでインストールするには、次のコマンドを実行します:

  1. npm install --global [email protected]

注意: この記事の公開後には、おそらくより新しいバージョンのcdktf-cliパッケージがあります。最新バージョンでチュートリアルを試すには、代わりにnpm install --global cdktf-cli@latestを実行することができますが、一部の出力が若干異なる可能性があることに注意してください。

また、macOSまたはLinuxでHomebrewを使用してcdktf CLIをインストールすることもできます。その場合、cdktfのフォーミュラを使用します:

  1. brew install cdktf

インストールが成功したことを確認するには、引数なしでcdktfコマンドを実行します:

  1. cdktf

次のような出力が表示されます:

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 バージョン番号を表示します --disable-logging ログファイルを書き込みません。CDKTF_DISABLE_LOGGING環境を使用してサポートされています。 --disable-plugin-cache-env TF_PLUGIN_CACHE_DIRを自動的に設定しません。 --log-level 書き込まれるログレベル。 -h, --help Show help Options can be specified via environment variables with the "CDKTF_" prefix (e.g. "CDKTF_OUTPUT")

出力には利用可能なコマンドが表示されます。このチュートリアルの残りでは、cdktf initcdktf getcdktf deploy、およびcdktf destroyを使用して経験を積むことになります。

今、cdktf CLIをインストールしたので、いくつかのTypeScriptコードを書くことでインフラストラクチャを定義できます。

ステップ2 — 新しいCDKTFプロジェクトの作成

このステップでは、後続の手順で構築するCDKTFプロジェクトの土台となる、さっきインストールしたcdktf CLIを使用します。

以下のコマンドを実行して、CDKTFプロジェクトを配置するディレクトリを作成します:

  1. mkdir infra

次に、新しく作成したディレクトリに移動します:

  1. cd infra/

cdktf initコマンドを使用して、構築するCDKTFプロジェクトの枠組みを作成します:

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

CDKTFは、TypeScript、Python、Java、C#、またはGoを使用してインフラストラクチャを定義することを開発者に許可します。 --template=typescriptオプションは、cdktfに対してこのCDKTFプロジェクトをTypeScriptを使用してスキャフォールドするよう指示します。

Terraform(およびそれによってCDKTF)は、管理しているリソースを追跡するために、Terraformステートファイルと呼ばれるファイルにその定義と状態を記録します。 --localオプションは、CDKTFにこれらのステートファイルをcdktfを実行しているマシン上のローカルに保持するよう指示します(各ファイルはterraform.<stack>.tfstateの命名構造に従います)。

コマンドを実行した後、CLIは製品を改善するためにCDKTFチームにクラッシュレポートを送信する許可を求める場合があります:

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)

同意する場合はYを入力し、同意しない場合はNを入力し、次にENTERキーを押します。

cdktfは、プロジェクトのスキャフォールドを作成し、パッケージをインストールします。プロジェクトがスキャフォールドされると、次のような出力が表示されます:

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)

infraディレクトリにもいくつかの新しいファイルが追加されます。最も重要なファイルはcdktf.jsonmain.tsです。

cdktf.jsonは、CDKTFプロジェクトの構成ファイルです。ファイルを開くと、次のようなものが表示されます:

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

appプロパティは、TypeScriptコードをTerraform互換のJSONに合成するために実行されるコマンドを定義します。このプロパティは、main.tsがCDKTFプロジェクトのエントリポイントであることを示しています。

main.tsファイルを開くと、次のようなものが表示されます:

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

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

    // ここでリソースを定義する
  }
}

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

CDKTFの言語では、関連するインフラストラクチャリソースのコレクションをスタックにグループ化できます。たとえば、Droplets、ロードバランサー、およびDNSレコードからなるAPIアプリケーションのリソースは、APIStackという単一のスタックにグループ化できます。各スタックは独自の状態を保持し、他のスタックとは独立して展開、変更、または破棄できます。スタックの一般的な使用法は、本番用のスタックと開発用のスタックを別々に持つことです。

アプリケーションは複数のスタックのコンテナです。たとえば、アプリケーションはさまざまなマイクロサービスのスタックをグループ化できます。

main.tsで生成されたCDKTFプロジェクトのスキャフォールドには、現在リソースを定義していないMyStackという単一のスタッククラスが含まれています。MyStackのインスタンスはinfraという名前で作成され、appというアプリケーション内に含まれます。後続の手順では、MyStackのコンストラクタ内でインフラストラクチャリソースを定義します。

プロジェクトを作成した後の次のステップは、CDKTFプロジェクトをプロバイダで構成することです。

ステップ3 — DigitalOceanプロバイダのインストール

このステップでは、CDKTF プロジェクトに DigitalOcean プロバイダーをインストールします。

プロバイダーは、Terraform(cdktf の下で使用される)にクラウドプロバイダー、SaaS プロバイダー、およびその他のアプリケーションプログラミングインターフェイス(API)を公開するプラットフォーム上でリソースを作成、更新、削除する方法に関する指示を提供するライブラリです。プロバイダーは、これらの上流APIを呼び出すロジックを標準の関数にカプセル化し、Terraformが呼び出せるようにします。

たとえば、Terraformを使用せずに新しい DigitalOcean ドロップレットを作成する場合、/v2/dropletsエンドポイントの DigitalOcean APIPOST リクエストを送信する必要があります。 代わりに、Terraformでは、DigitalOcean プロバイダー をインストールし、次のサンプルスニペットに示すように digitalocean_droplet リソースを定義します:

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

次に、この TypeScript コードを Terraform 互換の JSON に変換し、プロバイダーに渡して、そのプロバイダーがあなたの代わりにドロップレットを作成するための適切な API 呼び出しを行うために、cdktf CLI ツールを使用できます。

これで、プロバイダーが何であるかが理解できたので、CDKTF プロジェクトに DigitalOcean プロバイダーを設定できます。

cdktf.json ファイルを開いて、terraformProviders 配列に文字列 digitalocean/digitalocean を追加してください。

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 は、Terraform Registry 上の DigitalOcean プロバイダーの識別子です。

ファイルを保存して閉じます。

次に、cdktf get を実行してプロバイダーをダウンロードしてインストールします。

  1. cdktf get

cdktf get は、プロバイダーをダウンロードし、スキーマを抽出し、対応する TypeScript クラスを生成し、それを .gen/providers/ の下に TypeScript モジュールとして追加します。この自動コード生成により、CDKTF で任意の Terraform プロバイダーや HCL モジュールを使用し、それをサポートするエディターでコード補完を提供することができます。

cdktf get の実行が完了すると、以下のような出力が表示されます:

Output
Generated typescript constructs in the output directory: .gen

また、生成されたプロバイダーのコードが含まれる .gen という新しいディレクトリも表示されます。

このステップでは、プロジェクトに digitalocean/digitalocean プロバイダーをインストールしました。次のステップでは、DigitalOcean プロバイダーを DigitalOcean API で認証するために必要な資格情報で構成します。

ステップ 4 — DigitalOcean プロバイダーの構成

このステップでは、DigitalOceanプロバイダーを、あなたのDigitalOceanパーソナルアクセス トークンで構成します。これにより、プロバイダーはあなたの代わりにDigitalOcean APIを呼び出すことができます。

異なるプロバイダーは、上流のAPIと認証するために異なる資格情報を必要とし、サポートしています。DigitalOceanプロバイダーでは、DigitalOceanパーソナルアクセス トークンを提供する必要があります。このトークンをプロバイダーに指定するには、DIGITALOCEAN_TOKENまたはDIGITALOCEAN_ACCESS_TOKEN環境変数として設定します。

次のコマンドをターミナルで実行して、そのターミナルセッションの環境変数を設定します。

  1. export DIGITALOCEAN_ACCESS_TOKEN="your_personal_access_token"

注意: exportを呼び出すことで、そのターミナルセッションの環境変数のみが設定されます。ターミナルを閉じて再度開いた場合や、別のターミナルでcdktfコマンドを実行した場合は、環境変数が有効になるように再度exportコマンドを実行する必要があります。

次に、プロバイダーをMyStackクラス内に指定します。これにより、スタック内でプロバイダーが提供するリソースを定義できます。次のようにmain.tsファイルを更新します。

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

プロバイダーのモジュールは、./.gen/providers/digitaloceanにあります。これは、cdktf getを実行したときに自動生成されました。

このステップでは、認証情報を含むdigitalocean/digitaloceanプロバイダーを構成しました。次に、このチュートリアルの目標の一部を形成するインフラストラクチャの定義を開始します。

ステップ5 — ドロップレット上のウェブアプリケーションの定義

このステップでは、2つの異なるファイルを提供する2つのNGINXサーバーを、2つの同一のUbuntu 20.04ドロップレットに展開します。

まず、2つのドロップレットの定義から始めます。ハイライトされた変更を使用してmain.tsを修正します。

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

コード内の重複を避けるために、JavaScriptネイティブのループ(Array.prototype.map())を使用します。

コンソールを介してドロップレットを作成するかのように、指定する必要があるいくつかのパラメータがあります:

  • image – ドロップレットが実行されるLinuxディストリビューションとバージョン。
  • region – ドロップレットが実行されるデータセンター。
  • size – ドロップレットに予約するCPUとメモリリソースの量。
  • name – ドロップレットを参照するために使用される一意の名前。

imageregion、およびsizeの値は、DigitalOceanがサポートしているものでなければなりません。すべてのサポートされているLinuxディストリビューションイメージ、Dropletサイズ、およびリージョンの有効な値(スラッグと呼ばれる)は、DigitalOcean APIスラッグページで見つけることができます。必須およびオプションの属性の完全なリストは、digitalocean_dropletのドキュメントページで見つけることができます。

SSHキーの追加

前提条件の一環として、DigitalOceanアカウントにパスワードなしのSSH公開キーをアップロードし、その名前をメモしました。それを使用してSSHキーのIDを取得し、Dropletの定義に渡します。

SSHキーは手動でDigitalOceanアカウントに追加されたため、現在のTerraform構成で管理されているリソースではありません。新しいdigitalocean_ssh_keyリソースを定義しようとすると、既存のものを使用せずに新しいSSHキーが作成されます。

新しいdigitalocean_ssh_key データソースを定義します。Terraformでは、データソースは、現在のTerraform構成で管理されていないインフラストラクチャに関する情報を取得するために使用されます。言い換えると、これらは既存の外部インフラストラクチャの状態への読み取り専用ビューを提供します。データソースが定義されると、データをTerraform構成の他の場所で使用できます。

main.tsの中で、MyStackのコンストラクタ内で新しいDataDigitaloceanSshKeyデータソースを定義し、SSHキーに割り当てた名前(ここでは名前は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, {
    ...
  }
}
...

次に、Dropletの定義を更新して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()]
}))
...

プロビジョニングされると、パスワードの代わりにプライベートSSHキーを使用してDropletにアクセスできます。

NGINXのインストール用のユーザーデータスクリプトの指定

これで、Ubuntuを実行し、SSHアクセスが構成された2つの同一のDropletが定義されました。次のタスクは、それぞれのDropletにNGINXをインストールすることです。

Dropletが作成される際に、CloudInitというツールがサーバーをブートストラップします。CloudInitはユーザーデータと呼ばれるファイルを受け入れることができ、これによりサーバーのブートストラップ方法を変更できます。ユーザーデータには、サーバーが解釈できるcloud-configファイルやBashスクリプトなどが含まれる可能性があります。

このステップの残りでは、Bashスクリプトを作成し、それをDropletのユーザーデータとして指定します。このスクリプトでは、ブートストラッププロセスの一部としてNGINXをインストールします。さらに、スクリプトは/var/www/html/index.htmlファイル(NGINXが提供するデフォルトのファイル)の内容を、Dropletのホスト名とIPアドレスで置き換えます。これにより、2つのNGINXサーバーが異なるファイルを提供することになります。次のステップでは、これらのNGINXサーバーの両方をロードバランサーの背後に配置します。異なるファイルを提供することで、ロードバランサーがリクエストを適切に配信しているかどうかが明らかになります。

main.tsのまま、Dropletの構成オブジェクトにuserDataプロパティを追加します:

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
`
    }))
  }
}

警告:シェバン(#!)の前に新しい行がないことを確認してください。そうでないと、スクリプトが実行されない場合があります。

最初にDropletがプロビジョニングされると、スクリプトはrootユーザーとして実行されます。UbuntuのパッケージマネージャーであるAPTを使用してnginxパッケージをインストールします。次に、DigitalOceanのMetadata Serviceを使用して自身に関する情報を取得し、ホスト名とIPアドレスをindex.htmlに書き込みます。このindex.htmlはNGINXによって提供されます。

このステップでは、Ubuntuを実行する2つのDropletを定義し、それぞれにSSHアクセスを構成し、ユーザーデータ機能を使用してNGINXをインストールしました。次のステップでは、これらのNGINXサーバーの前に配置されるロードバランサーを定義し、ラウンドロビン方式でロードバランスするように構成します。

ステップ6 — ロードバランサーの定義

このステップでは、DigitalOceanロードバランサーを定義し、digitalocean_loadbalancerリソースのインスタンスを定義します。

main.tsMyStackコンストラクターの最後に、以下のロードバランサーの定義を追加してください。

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))
    })
  }
}
...

forwardingRule引数は、ロードバランサーにポート80でのHTTPリクエストを受け付け、それらを各Dropletのポート80に転送するように指示します。

dropletIdsは、ロードバランサーがリクエストを渡すDropletを指定します。これは数字を取りますが、droplet.idの値は文字列です。したがって、文字列のDroplet ID値を数字に変換するために、Fn.tonumber Terraform関数を使用しました。

注意:droplet.idの値はDropletがプロビジョニングされるまで不明です。そのため、ここではJavaScriptネイティブのparseIntではなく、Fn.tonumber Terraform関数を使用しました。Terraform関数は、Terraformが構成を適用する前に不明なランタイム値で動作するように設計されています。

ファイルを保存して閉じます。

これで、2つのDropletとそれらの前に配置されたロードバランサーが定義されました。あなたのmain.tsは、次のように見えるはずです:

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

次のステップでは、cdktf CLIツールを使用してCDKTFプロジェクト全体を実現します。

ステップ7 — インフラストラクチャのプロビジョニング

このステップでは、前のステップで定義したDropletsとロードバランサーをプロビジョニングするために、cdktf CLIツールを使用します。

infra/ディレクトリにいて、ターミナルセッションのDIGITALOCEAN_ACCESS_TOKEN環境変数を設定していることを確認し、cdktf deployコマンドを実行してください:

  1. cdktf deploy

次のような出力が表示されるはずです:

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)が作成されます + 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)が作成されます + 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

注意: CDKTFはまだ開発中ですので、上記の表示と異なる場合があります。

この表示には、cdktfが作成、更新、および破棄するすべてのリソースとプロパティがリストされています。ドロップレットのIDなど、一部の値はリソースがプロビジョニングされた後にのみわかります。その場合、出力のプロパティ値として(known after apply)が表示されます。

リソースのリストを確認して、期待どおりのものかどうかを確認してください。その後、矢印キーを使用して承認オプションを選択し、ENTERを押します。

次のような出力が表示されます:

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

この出力は、cdktfがDigitalOcean APIと通信してDropletを作成していることを示しています。 DropletのIDは、Dropletがプロビジョニングされるまでわからないため、cdktfは最初にDropletsを作成します。

Dropletの作成には通常1分未満かかります。 Dropletsがプロビジョニングされると、cdktfはロードバランサーの作成に移行します。

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]

ロードバランサーの作成には時間がかかる場合があります。ロードバランサーが作成された後、スタックが正常に展開されたことを示す要約が表示されます。

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.

今すぐ、DigitalOceanコンソールを訪れることができます。そこで、defaultという名前の1つのロードバランサーと、それぞれがロードバランサーのターゲットとして機能する2つの健全なDroplet (foobar) を見ることができます。

NGINXが実行され、コンテンツが正しく提供されていることをテストするには、各DropletのIPアドレスにアクセスしてください。次のようなテキストが表示されるはずです:

Droplet: bar, IP Address: droplet_ip

その文字列のテキストが表示されない場合やサーバーが応答しない場合は、指定したユーザーデータが正しいかどうか、およびシェバン (#!) の前に文字 (改行を含む) がないかどうかを確認してください。また、SSHプライベートキーを使用してDropletにSSHでき、CloudInitが生成した出力ログを/var/log/cloud-init-output.logで確認できます:

  1. ssh -i path_to_ssh_private_key root@droplet_ip

ドロップレットが起動し、コンテンツを提供していることを確認したら、ロードバランサーのテストを開始できます。これを行うには、いくつかのリクエストを送信します。

次のコマンドをターミナルから実行して、ロードバランサーに10件のリクエストを送信します:

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

以下のような出力が表示されるはずですが、表示されるIPアドレスは異なります:

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

これにより、ロードバランサーへのリクエストが各Dropletに5回転送されたことが示され、ロードバランサーが機能していることがわかります。

注意: ロードバランサーが常に2つのDroplet間で完璧にバランスをとるわけではないかもしれません。1つのDropletに4つのリクエストが送信され、もう1つに6つのリクエストが送信されたということがあります。この動作は正常です。

このステップでは、cdktfを使用してリソースをプロビジョニングし、次にデジタルオーシャンコンソールを使用してDropletsとロードバランサーのIPアドレスを見つけました。その後、それぞれのDropletとロードバランサーにリクエストを送信して動作を確認しました。

次のステップでは、デジタルオーシャンコンソールにログインせずに、DropletsとロードバランサーのIPアドレスを取得します。

ステップ8 — 情報の出力

前のステップでは、DropletとロードバランサーのIPアドレスを取得するためにデジタルオーシャンコンソールにログインする必要がありました。このステップでは、この情報がcdktf deployコマンドの出力に表示されるようにコードをわずかに変更して、コンソールへの移動を省略します。

Terraformは、管理されているリソースの構成と状態をステートファイルに記録します。infraスタックの場合、ステートファイルはinfra/terraform.infra.tfstateにあります。このステートファイル内にDropletsとロードバランサーのIPアドレスを見つけることができます。

ただし、大きなファイルを整理するのは不便です。CDKTFはTerraformOutput構造を提供しており、これを使用して変数を出力し、スタックの外部で使用できるようにします。出力は、cdktf deployの実行後にstdoutに表示されます。cdktf outputを実行すると、いつでも出力を表示できます。

注意: このチュートリアルでは、コンソールに情報を出力するだけですが、その真の力は他のスタックからの出力を入力として使用するスタックを使用することにあります。この機能はクロススタック参照として知られています。

main.tsファイルを更新して、ロードバランサーとDropletsのIPアドレスの出力を含めます。

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
    }))
  }
}
...

ファイルを保存して閉じます。

cdktf deployを実行して変更を反映します。

  1. cdktf deploy

出力には、次のような内容が表示されます:

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

この出力により、インフラストラクチャの変更は行われず、スタックからの出力のみが表示されます。

矢印キーを使用して承認を選択し、ENTERキーを押します。端末出力の最後には、次のような内容が表示されます:

Output
infra droplet0IP = droplet_foo_ip droplet1IP = droplet_bar_ip loadBalancerIP = load_balancer_ip

これで、cdktf deployまたはcdktf outputを実行するたびに、DropletsとロードバランサーのIPアドレスが端末出力に表示され、DigitalOceanコンソールからその情報にアクセスする必要がなくなります。

これで、2つのDropletsとロードバランサーをプロビジョニングし、それらが動作していることを確認しました。開発したCDKTFプロジェクトをベースにして、より洗練されたインフラストラクチャを定義することができます(参照実装はdo-community / digitalocean-cdktf-typescriptで見つけることができます)。

このチュートリアルでプロビジョニングされたリソースには料金が発生します。作成されたインフラストラクチャを使用する予定がない場合は、破棄してください。次の最終ステップでは、このチュートリアルで作成されたリソースを破棄してプロジェクトをクリーンアップします。

ステップ9 — インフラストラクチャの破棄

このステップでは、このチュートリアルで作成されたすべてのリソースを削除します。

infra/ディレクトリ内にまだいる場合、cdktf destroyを実行します:

  1. cdktf destroy

以下のような出力が表示されるはずです:

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) が破棄されます - 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) が破棄されます - 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

この時点では、各リソースの横に + ではなく - が表示され、CDKTFがリソースを破棄する予定であることを示します。提案された変更を確認し、矢印キーを使用して承認を選択し、ENTER キーを押します。DigitalOcean プロバイダーは、リソースを破棄するために DigitalOcean API と通信します。

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]

ロードバランサーは依存関係がないため(他のリソースがその入力でロードバランサーを参照していないため)、最初に削除されました。ロードバランサーがドロップレットを参照しているため、ドロップレットはロードバランサーが破棄された後にのみ破棄できます。

リソースが破棄されると、出力に次の行が表示されます:

Output
Destroy complete! Resources: 3 destroyed.

結論

このチュートリアルでは、CDKTFを使用して、NGINXサーバーを実行する2つのDigitalOceanドロップレットから成るロードバランスされたWebページをプロビジョニングおよび破棄しました。また、リソースに関する情報をターミナルに出力しました。

CDKTFはTerraformの抽象化レイヤーです。Terraformの理解があるとCDKTFの理解が助けになります。Terraformについてもっと学びたい場合は、Terraformを詳しくカバーしたTerraformでインフラストラクチャを管理する方法シリーズを読むことができます。

CDKTFについてさらに学ぶには、公式のCDK for Terraformドキュメントチュートリアルをチェックしてください。

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