CLOVER🍀

That was when it all began.

TerraformでNode.js+PostgreSQLなアプリケーションをHerokuにデプロイする

これは、なにをしたくて書いたもの?

Terraformの勉強にと、Herokuを使って、アプリケーションとデータベースをデプロイする超簡単な例を作って試してみようと。

お題

Herokuのフリープランで利用可能なPostgreSQLを使い、以下のサンプルとほぼ同等なアプリケーションをHerokuにデプロイ
してみます。

Connecting in Node.js

information_schema、tables内のtable_schema、table_nameを取得するだけの、簡単なサンプルです。

環境

今回の環境は、こちら。

$ terraform version
Terraform v0.11.13

TerraformのHeroku Providerは、1.9.0を使用します。

また、お題でも書きましたが、Herokuはフリープランで使っています。

Terraformのリソース定義ファイル

それでは、最初にTerraformのリソース定義ファイルを作成します。

今回は、アプリケーションをデプロイするので「heroku_app」と「heroku_build」を使い、

Heroku: heroku_app - Terraform by HashiCorp

Heroku: heroku_build - Terraform by HashiCorp

PostgreSQLのデプロイには「heroku_addon」を使用します。

Heroku: heroku_addon - Terraform by HashiCorp

利用するHeroku Postgresのプランは、「hobby-dev」です。

Heroku Postgres - Add-ons - Heroku Elements

で、作成したメインのリソース定義ファイル。

heroku-app-postgresql.tf

provider "heroku" {
  email = "${var.heroku_email}"
  api_key = "${var.heroku_api_key}"
}

resource "heroku_app" "app_postgresql" {
  name = "app-postgresql"
  region = "us"
}

resource "heroku_build" "app_postgresql_build" {
  app = "${heroku_app.app_postgresql.name}"

  source = {
    url = "https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz"
  }
}

resource "heroku_addon" "postgresql" {
  app = "${heroku_app.app_postgresql.name}"
  plan = "heroku-postgresql:hobby-dev"
}

「heroku_build」でアプリケーションのソースコードアーカイブを参照していますが、これは後でまた。

PostgreSQLのリソース定義は、こちらですね。

resource "heroku_addon" "postgresql" {
  app = "${heroku_app.app_postgresql.name}"
  plan = "heroku-postgresql:hobby-dev"
}

「hobby-dev」プランを利用するようにしています。

TerraformでHerokuを使う時には、メールアドレスとAPIキーが必要になりますが、今回は変数として切り出すことにしました。

変数宣言をした定義ファイル。
variables.tf

variable "heroku_email" { }
variable "heroku_api_key" { }

メインの定義ファイルからは、この内容をvarで参照しています。

provider "heroku" {
  email = "${var.heroku_email}"
  api_key = "${var.heroku_api_key}"
}

実際に使う値は、tfvarsファイルに記載。
secret.tfvars

heroku_email = "....."
heroku_api_key = "....."

このあたりは、こちらを参考に。

Input Variables | Terraform - HashiCorp Learn

Input Variables - Configuration Language - Terraform by HashiCorp

Terraformで利用する際には、以下のように「-var-file」として指定します。

$ terraform apply -var-file=secret.tfvars

とりあえず、ここまでで「terraform init」してHeroku Providerをダウンロードしておきます。

$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "heroku" (1.9.0)...

サンプルアプリケーションの作成

それでは、サンプルアプリケーションを作成します。

Node.js+Expressで、簡単なREST APIとして作ってみましょう。ExpressでPostgreSQLを使う例としては、以下を参照しました。

PostgreSQL

PostgreSQLとの接続には、pg-promiseを使います、と。

GitHub - vitaly-t/pg-promise: Promises/A+ interface for PostgreSQL

pg-promise API

Expressとpg-promiseのインストール。

$ npm i express pg-promise

インストールされたライブラリのバージョン。

  "dependencies": {
    "express": "^4.16.4",
    "pg-promise": "^8.7.2"
  }

作成したソースコード
server.js

const express = require('express');
const pgp = require('pg-promise')({ });
const app = express();

const postgresql = pgp(`${process.env.DATABASE_URL}?ssl=true`);

const port = process.env.PORT || 3000;

app.get('/', async (req, res) => {
    return postgresql
        .any('SELECT table_schema,table_name FROM information_schema.tables')
        .then(data => res.send(JSON.stringify(data)));
});

app.listen(port, () => console.log(`[${new Date()}] Server startup, port = ${port}`));

Connection StringとSSLの設定については、Herokuのドキュメントを参考にしています。

Connecting in Node.js

const postgresql = pgp(`${process.env.DATABASE_URL}?ssl=true`);

このスクリプトを「npm start」で起動できるようにして

  "scripts": {
    "start": "node server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

tar.gzで固めて、今回はGitHubに起きました。

https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz

中身は、こんな感じです。

$ tar -tvf nodejs-app-postgresql.tar.gz
-rw-rw-r-- xxxxx/xxxxx 487 2019-05-13 23:09 nodejs-app-postgresql/server.js
-rw-rw-r-- kazuxxxxxhira/xxxxx 19095 2019-05-13 22:33 nodejs-app-postgresql/package-lock.json
-rw-rw-r-- xxxxx/xxxxx   344 2019-05-13 22:34 nodejs-app-postgresql/package.json

このURLが、先ほど「heroku_build」のsourceに指定していたアーカイブです。

resource "heroku_build" "app_postgresql_build" {
  app = "${heroku_app.app_postgresql.name}"

  source = {
    url = "https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz"
  }
}

これで、準備は完了です。

デプロイ

それでは、ここまで定義した内容を、デプロイします。

まずは、「terraform plan」で確認。「terraform plan」時にも、「-var-file」で変数の内容を指定します。

$ terraform plan -var-file=secret.tfvars
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + heroku_addon.postgresql
      id:                      <computed>
      app:                     "app-postgresql"
      config_vars.#:           <computed>
      name:                    <computed>
      plan:                    "heroku-postgresql:hobby-dev"
      provider_id:             <computed>

  + heroku_app.app_postgresql
      id:                      <computed>
      all_config_vars.%:       <computed>
      config_vars.#:           <computed>
      git_url:                 <computed>
      heroku_hostname:         <computed>
      internal_routing:        <computed>
      name:                    "app-postgresql"
      region:                  "us"
      sensitive_config_vars.#: <computed>
      stack:                   <computed>
      uuid:                    <computed>
      web_url:                 <computed>

  + heroku_build.app_postgresql_build
      id:                      <computed>
      app:                     "app-postgresql"
      buildpacks.#:            <computed>
      local_checksum:          <computed>
      output_stream_url:       <computed>
      release_id:              <computed>
      slug_id:                 <computed>
      source.%:                "1"
      source.url:              "https://github.com/kazuhira-r/heroku-terraform-sandbox/raw/master/nodejs-app-postgresql.tar.gz"
      stack:                   <computed>
      status:                  <computed>
      user.%:                  <computed>
      uuid:                    <computed>


Plan: 3 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

では、デプロイ。

$ terraform apply -var-file=secret.tfvars

やや時間がかかりますが、デプロイが完了しました。

heroku_addon.postgresql: Creation complete after 1s (ID: d86ace58-a0a6-4bb8-9ba1-3def3d22921a)
heroku_build.app_postgresql_build: Still creating... (10s elapsed)
heroku_build.app_postgresql_build: Still creating... (20s elapsed)
heroku_build.app_postgresql_build: Creation complete after 27s (ID: 8e2731d5-2929-44dd-9eea-a246d9d07cf0)

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

確認。

$ curl -s https://app-postgresql.herokuapp.com | jq 
[
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_foreign_table"
  },
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_roles"
  },
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_settings"
  },
  {
    "table_schema": "pg_catalog",
    "table_name": "pg_subscription"
  },

〜省略〜

できました!

最後に、デプロイしたリソースを破棄しておしまい。

$ terraform destroy -var-file=secret.tfvars

どちらかというと、TerraformというよりNode.jsとHerokuにてこずりましたが、とりあえずやりたいことはできましたよ、と。