😇

n8nをCloud Runにサクッと(Supabase x Pulumi)

に公開

この記事では、オープンソースのワークフロー自動化ツール「n8n」を、GCPのサーバーレス環境であるCloud Runにサクッとデプロイする方法を紹介します。
データベースにはSupabase、IaCツールにはPulumiを使っていきます。

はじめに

最近、個人的にn8nをよく触っています。
これ、プログラミングが苦手な自分みたいな奴でも、ノードを繋いでいくだけで本格的な自動化ワークフローが作れちゃう優れものなんです。

このn8nはセルフホスト用にDockerイメージが公式から提供されているので、「これ、どこかのサーバーレス環境にサクッとデプロイできたらもっと便利だな」と考えました。

技術スタックの選択

今回の構成は以下の通りです。

1. 環境:Cloud Run

サーバーレスでコンテナを動かすなら、とりあえず一番お手軽なCloud Runにします。
スケーリングやデプロイの手軽さはピカイチです。

2. データベース:Supabase

n8nはワークフローや認証情報などを永続化するためにデータベースが必要です。
Cloud Runはステートレスなので、外部のDBが必要になります。
今回は、無料ではじめられ、PostgreSQLが簡単に使えるSupabaseを選択しました。

GCPにデプロイするのでCloudSQLも選択肢にあったのですが、お手軽感でいうとSupabaseに勝るものは無いと思いました

3. IaC:Pulumi

インフラの構成管理(IaC)には、Pulumiを選択しました。AWSにはCDKがありますが、GCPで同様のツールを探していました。
せっかくなら使ったことのないものに挑戦したいと思い、TypeScriptやGoで書けるPulumiを今回初めて使ってみることにしました。

準備

まずは、デプロイに必要なツールやサービスを準備します。

GCPのプロジェクト作成

GCPコンソールから新しいプロジェクトを作成するか、既存のプロジェクトを利用します。
プロジェクトIDは控えておきましょう。

gcloud CLIインストール

GCPをコマンドラインから操作するために、gcloud CLIをインストールし、初期設定を済ませておきます。

# ログイン
gcloud auth login

# プロジェクト設定
gcloud config set project YOUR_PROJECT_ID

詳細は公式ドキュメントを参照してください。

Pulumiインストール

Pulumi CLIをインストールします。macOSならHomebrewが簡単です。

brew install pulumi

インストール後、Pulumiにログインします。ローカルのファイルシステムをバックエンドにすることも可能です。

# PulumiのSaaSバックエンドを利用する場合
pulumi login

# ローカルバックエンドを利用する場合
pulumi login --local

詳細は公式ドキュメントを参照してください。

Supabaseプロジェクト作成

Supabase公式サイトでアカウントを登録し、新しいプロジェクトを作成します。
プロジェクト作成後、以下のデータベース接続情報を取得しておきます。

  1. Supabaseのダッシュボードでプロジェクトを選択
  2. ヘッダーのConnectボタンからモーダルを開く
  3. Connection string > URI タブから、以下の情報をメモしておきます。
    • Host
    • Database name
    • User
    • Port

プロジェクト作成時のパスワードも忘れずメモしてください

IaC作成

いよいよインフラをコード化していきます。

Pulumiのプロジェクト作成

作業用のディレクトリを作成し、Pulumiプロジェクトを初期化します。今回はGCPのTypeScriptテンプレートを使います。

mkdir n8n-pulumi && cd n8n-pulumi
pulumi new gcp-typescript

いくつか質問されますが、プロジェクト名やGCPのプロジェクトID、リージョン(例: asia-northeast1)などを設定してください。

一度Terraformで書いてみる (思考の整理)

本格的に実装する前に「もしTerraformで書くならどんなリソースが必要か?」を頭の中で整理しました。

※このセクションは思考整理のためであり、実際にこのTerraformコードを使用するわけではありません

  • 各種GCP APIの有効化 (google_project_service)
  • Cloud Run用のサービスアカウント (google_service_account)
  • DBパスワード等を格納するSecret Manager (google_secret_manager_secret)
  • Cloud Runサービス本体 (google_cloud_run_v2_service)
  • 外部からアクセス許可するためのIAM (google_cloud_run_service_iam_member)

以下Terraformのコードでざっくり書いて見ました。

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = ">= 3.0"
    }
  }
}

provider "google" {
  project = var.project_id
  region  = var.region
}

# --------------------------------------------------
# APIの有効化
# --------------------------------------------------
resource "google_project_service" "apis" {
  for_each = toset([
    "run.googleapis.com",
    "secretmanager.googleapis.com",
  ])
  service                    = each.key
  disable_dependent_services = true
}

resource "google_service_account" "n8n_service_account" {
  account_id   = "n8n-service-account"
  display_name = "n8n Service Account"
  project      = var.project_id
}

# --------------------------------------------------
# シークレット管理 (Secret Manager)
# --------------------------------------------------
# SupabaseのDBパスワード用シークレット
resource "google_secret_manager_secret" "db_password_secret" {
  secret_id = "supabase-db-password"
  replication {
    auto {}
  }
  depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "db_password_secret_version" {
  secret      = google_secret_manager_secret.db_password_secret.id
  secret_data = var.supabase_password
}

# n8nの暗号化キーをランダム生成してシークレットに保存
resource "random_string" "n8n_encryption_key" {
  length  = 32
  special = false
}
resource "google_secret_manager_secret" "n8n_encryption_key_secret" {
  secret_id = "n8n-encryption-key"
  replication {
    auto {}
  }
  depends_on = [google_project_service.apis]
}
resource "google_secret_manager_secret_version" "n8n_encryption_key_secret_version" {
  secret      = google_secret_manager_secret.n8n_encryption_key_secret.id
  secret_data = random_string.n8n_encryption_key.result
}

# --------------------------------------------------
# Cloud Run サービス
# --------------------------------------------------
resource "google_cloud_run_v2_service" "n8n_service" {
  name     = "n8n-supabase-service"
  location = var.region

  deletion_protection = false # デフォルトはtrueなので、削除保護を無効化

  template {
    # VPC接続は不要

    service_account = google_service_account.n8n_service_account.email

    scaling {
      min_instance_count = 1 # 最低1つのインスタンスを常に稼働させる
      max_instance_count = 5 # 最大10インスタンスまでスケールアウト可能
    }

    containers {
      image = "n8nio/n8n:latest"
      ports { container_port = 5678 }
      resources {
        limits = { cpu = "1", memory = "1Gi" }
      }

      # 環境変数をSupabase用に設定
      env {
        name  = "DB_TYPE"
        value = "postgresdb"
      }
      env {
        name  = "DB_POSTGRESDB_HOST"
        value = var.supabase_host
      }
      env {
        name  = "DB_POSTGRESDB_PORT"
        value = var.supabase_port
      }
      env {
        name  = "DB_POSTGRESDB_DATABASE"
        value = var.supabase_database
      }
      env {
        name  = "DB_POSTGRESDB_USER"
        value = var.supabase_user
      }
      env {
        name  = "DB_POSTGRESDB_SSL"
        value = "true"
      } # Supabase接続にはSSLが必須
      env {
        name = "DB_POSTGRESDB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.db_password_secret.secret_id
            version = "latest"
          }
        }
      }
      # その他n8nに必要な環境変数を諸々...
    }
  }
}

locals {
  # Cloud Run サービスの URI を設定したい環境変数名のリスト
  env_vars_for_service_uri = [
    "WEBHOOK_URL",
    "N8N_EDITOR_BASE_URL",
    # 他にもURIを設定したい環境変数があればここに追加
  ]
}

resource "null_resource" "update_n8n_webhook_url" {
  # Cloud Run サービスの URI が変更されたら、このリソースを再作成 (プロビジョナーを再実行) する
  triggers = {
    service_uri = google_cloud_run_v2_service.n8n_service.uri
    env_vars_string = join(",", [
      for env_name in local.env_vars_for_service_uri :
      format("%s=%s", env_name, google_cloud_run_v2_service.n8n_service.uri)
    ])
  }

  # Cloud Run サービスが作成された後に実行する
  depends_on = [google_cloud_run_v2_service.n8n_service]

  provisioner "local-exec" {
    command     = <<-EOT
      gcloud run services update ${google_cloud_run_v2_service.n8n_service.name} \
        --project=${var.project_id} \
        --region=${google_cloud_run_v2_service.n8n_service.location} \
        --set-env-vars=${self.triggers.env_vars_string} \
        --format=none
    EOT
    interpreter = ["bash", "-c"]
  }
}

# --------------------------------------------------
# IAM権限設定
# --------------------------------------------------
# Cloud Runを公開アクセス可能にする
resource "google_cloud_run_v2_service_iam_member" "n8n_invoker" {
  project  = google_cloud_run_v2_service.n8n_service.project
  location = google_cloud_run_v2_service.n8n_service.location
  name     = google_cloud_run_v2_service.n8n_service.name
  role     = "roles/run.invoker"
  member   = "allUsers"
}

# Cloud RunのサービスアカウントがSecret Managerを読み取れるようにする
resource "google_secret_manager_secret_iam_member" "db_password_accessor" {
  project   = google_secret_manager_secret.db_password_secret.project
  secret_id = google_secret_manager_secret.db_password_secret.secret_id
  role      = "roles/secretmanager.secretAccessor"
  member    = "serviceAccount:${google_service_account.n8n_service_account.email}"
}

# 以下略...

この構成をPulumiで実現していきます。

Terraformは最近触り始めたばかりなので、コードが雑なのはご容赦ください

いざ実装

index.ts にコードを書いていきます。

ちなみに、今回作ったリポジトリのサンプルはこちらになります。
https://github.com/takemo101/n8n-pulumi/tree/main

1. Config設定

Supabaseの接続情報など、環境ごとに変わる値はPulumiのConfigで管理します。ターミナルから以下のコマンドで設定します。
特にパスワードは --secret フラグを付けて暗号化しましょう。

pulumi config set gcp:region "asia-northeast1" # リージョン
pulumi config set supabaseHost "YOUR_SUPABASE_DB_HOST"
pulumi config set supabasePort "5432"
pulumi config set supabaseDatabase "postgres"
pulumi config set supabaseUser "postgres"

# --secret をつけると暗号化されてPulumi.<stack>.yamlに保存される
pulumi config set --secret supabasePassword "YOUR_SUPABASE_DB_PASSWORD"

設定したConfig値は、以下のようにコード上で取得します。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L6-L21

2. GCPのAPI有効化

run.googleapis.com などをループで有効化しています。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L26-L36

3. サービスアカウント作成

Cloud Runが他のGCPサービス(今回はSecret Manager)にアクセスするための専用アカウントを作成します。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L38-L48

4. シークレット設定

pulumi configで設定したDBパスワードと、n8nが認証情報を暗号化するために必要なN8N_ENCRYPTION_KEY(ランダム生成)を、安全なSecret Managerに登録します。Cloud Runサービスでは、このSecret Managerの値を直接参照させます。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L50-L95

5. Cloud Run設定

  • image: n8nの公式イメージ n8nio/n8n を指定します。
  • scaling: minInstanceCount: 1 を設定し、コールドスタートを避けることでWebhookなどの応答性を高めます。
  • resources: n8nはそれなりにメモリを消費するため、最低でも1Giを割り当てておくと安定します。
  • envs: n8nの動作に必要な環境変数を設定します。
    • DB_POSTGRESDB_...: Supabaseの接続情報を設定します。パスワードはSecret Managerから参照するようにしています。
    • WEBHOOK_URL: n8nが外部サービスからのトリガーを受け取るためのURLです。デプロイされるCloud Run自身のURLは、デプロイが完了するまで確定しません。Pulumiでは pulumi.interpolate${n8nService.uri}`` のように記述することで、この動的な値を宣言的に扱うことができます。
    • N8N_ENCRYPTION_KEY: n8nが保存する認証情報などを暗号化するための重要なキーです。これもSecret Managerから参照します。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L97-L156

6. IAM設定

roles/run.invoker ロールを allUsers に付与することで、認証なしで誰でもCloud Runサービスにアクセスできるようにしています。また、作成したサービスアカウントがSecret Managerから値を読み取れるよう、roles/secretmanager.secretAccessor ロールを付与します。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L185-L210

7. 出力設定

Pulumiでは、index.tsでexportした値を出力してくれるようです。

https://github.com/takemo101/n8n-pulumi/blob/fc2c471f9d93189f09552133ef47153efa8fd674/index.ts#L212-L216

デプロイしてみる

pulumi up

準備ができたら、いよいよデプロイです。以下のコマンドを実行します。

pulumi up

コマンドを実行すると、Pulumiが作成・変更するリソースのプレビューが表示されます。

Previewing update (dev)

     Type                                Name                               Plan
 +   pulumi:pulumi:Stack                 n8npulumi-dev                      create
 +   ├─ gcp:projects:Service             enable-run                         create
... (中略) ...
 +   ├─ gcp:cloudrunv2:Service           n8n-service                        create
 +   └─ gcp:cloudrunv2:ServiceIamMember  n8n-invoker                        create

Resources:
    + 13 to create

Do you want to perform this update?  [Use arrows to move, type to filter]
> yes
  no
  details

yesを選択してEnterキーを押すと、リソースの作成が始まります。数分待つとデプロイが完了し、OutputsとしてCloud RunのURLが表示されます。

Outputs:
  - n8nServiceAccountEmail: "n8n-service-account@xxxxxxx.iam.gserviceaccount.com"
  - n8nServiceUrl         : "https://n8n-supabase-service-xxxxxxxxxx.a.run.app"

Cloud Runを確認

表示されたURLにブラウザでアクセスしてみましょう。n8nの初期設定画面が表示されれば成功です!🎉

GCPコンソールのCloud Runのページでも、サービスがデプロイされていることを確認できます。

pulumi destroy

お片付けも簡単です。作成したリソースをすべて削除したい場合は、以下のコマンドを実行します。

pulumi destroy

プレビューが表示された後、yesを選択すると、今回作成したGCPリソースが一括で削除されます。

まとめ

今回は、n8n + Cloud Run + Supabase + Pulumi という構成で、ワークフロー自動化環境を構築してみました。

  • n8nはセルフホストできる強力なツールで、プログラミングが苦手でもGUIでポチポチするだけで本格的な自動化が実現できます。
  • Cloud RunSupabaseの組み合わせは、ステートフルなアプリケーションを手軽にサーバーレスで動かすのに非常に便利。
  • Pulumiを使えば、TypeScriptのような慣れた言語でインフラを宣言的に記述でき、初めてでもプレビュー機能のおかげで安心してインフラを管理できました(特にTerraformを使ってる人にはおすすめしたい)

IaCに慣れていないと少し難しく感じるかもしれませんが、一度コード化してしまえば、環境の再現や変更がとても楽になります。
ぜひ試してみてください!

株式会社ソニックムーブ

Discussion