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公式サイトでアカウントを登録し、新しいプロジェクトを作成します。
プロジェクト作成後、以下のデータベース接続情報を取得しておきます。
- Supabaseのダッシュボードでプロジェクトを選択
- ヘッダーの
Connect
ボタンからモーダルを開く -
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
にコードを書いていきます。
ちなみに、今回作ったリポジトリのサンプルはこちらになります。
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値は、以下のようにコード上で取得します。
2. GCPのAPI有効化
run.googleapis.com
などをループで有効化しています。
3. サービスアカウント作成
Cloud Runが他のGCPサービス(今回はSecret Manager)にアクセスするための専用アカウントを作成します。
4. シークレット設定
pulumi config
で設定したDBパスワードと、n8nが認証情報を暗号化するために必要なN8N_ENCRYPTION_KEY
(ランダム生成)を、安全なSecret Managerに登録します。Cloud Runサービスでは、このSecret Managerの値を直接参照させます。
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から参照します。
-
6. IAM設定
roles/run.invoker
ロールを allUsers
に付与することで、認証なしで誰でもCloud Runサービスにアクセスできるようにしています。また、作成したサービスアカウントがSecret Managerから値を読み取れるよう、roles/secretmanager.secretAccessor
ロールを付与します。
7. 出力設定
Pulumiでは、index.ts
でexportした値を出力してくれるようです。
デプロイしてみる
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 RunとSupabaseの組み合わせは、ステートフルなアプリケーションを手軽にサーバーレスで動かすのに非常に便利。
- Pulumiを使えば、TypeScriptのような慣れた言語でインフラを宣言的に記述でき、初めてでもプレビュー機能のおかげで安心してインフラを管理できました(特にTerraformを使ってる人にはおすすめしたい)
IaCに慣れていないと少し難しく感じるかもしれませんが、一度コード化してしまえば、環境の再現や変更がとても楽になります。
ぜひ試してみてください!
Discussion