n8nをGCP環境で構築してみた

目次

前提(準備)

GCP プロジェクトn8n-chelsea-labs(課金有効)

Supabase:プロジェクト作成済み。以下4つを控える

  • SUPABASE_HOST:例 db.xxxxx.supabase.coRESTの https://xxxx.supabase.co ではなく DB のホスト
  • SUPABASE_PORT:通常 5432
  • SUPABASE_DATABASE:通常 postgres
  • SUPABASE_USER:通常 postgres
  • SUPABASE_PASSWORD:作成時に決めたDBパスワード

ローカルPCに以下導入済み

  • Node.js LTS(推奨 18 以上)
  • Pulumi CLI
  • gcloud CLI(ログイン済み)
# GCPログイン & プロジェクト選択
gcloud auth login
gcloud config set project n8n-chelsea-labs

# ADC(Application Default Credentials)も設定しておくとPulumiから認証が通りやすい
gcloud auth application-default login

権限目安(自分のアカウントに付与):
roles/owner があれば十分です。最小構成なら
roles/run.admin, roles/iam.serviceAccountAdmin,
roles/secretmanager.admin, roles/serviceusage.serviceUsageAdmin など。

Compute Engine API

gcloud で一括有効化

gcloud services enable \
  compute.googleapis.com \
  run.googleapis.com \
  secretmanager.googleapis.com \
  --project=n8n-chelsea-labs

1) プロジェクト初期化

mkdir n8n-pulumi && cd n8n-pulumi
pulumi new gcp-typescript
# 対話で:
# Project name: n8n-pulumi (任意)
# Stack name: dev (任意)
# gcp:project: n8n-chelsea-labs
# gcp:region: asia-northeast1

Pulumi の状態(state)保存はデフォルトで Pulumi Cloud。ローカルにしたい場合は
pulumi login file://~/.pulumi を先に実行。

1)Pulumi Cloud にログイン(推奨)

  1. https://app.pulumi.com/user/settings/tokensCreate token をクリック
  2. 説明(例:n8n-pulumi-dev)と期限を入れて Create
  3. 表示された access token をコピー(この画面でしか見えません!)
  4. さっきのターミナルのプロンプトに ペーストして Enter
Enter your access token from https://app.pulumi.com/account/tokens
    or hit <ENTER> to log in using your browser                   :  <ここに貼り付け>

Pulumi プロジェクトの初期化

プロンプトが出ている:

Project name (n8n-pulumi):
そのまま Enter を押すと、デフォルト値の n8n-pulumi が採用されます。
(別の名前を使いたければ入力してから Enter)

続けて以下の質問が出ます:
Project description (A minimal Google Cloud TypeScript Pulumi program):

→ Enter(デフォルトでOK)

Node.js/npm がインストールされていない環境

下記のようなメッセージが出る場合があり、Node.js をインストールする必要がある。

The package manager to use for installing dependencies [Use arrows to move, type to filter] 
> npm [not found] 
pnpm [not found] 
yarn [not found] 
bun [not found]
✅ Pulumi プロジェクト作り直し

Node.js が入ったら、もう一度 Pulumi プロジェクトを作り直すのがスッキリします。

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

2) 依存パッケージの追加

npm i -D typescript ts-node @types/node
npm i @pulumi/pulumi @pulumi/gcp @pulumi/random @pulumi/command
  1. package.json
"scripts": {
  "build": "tsc",
  "up": "pulumi up",
  "preview": "pulumi preview",
  "destroy": "pulumi destroy"
}
✅ 修正後の package.json
{
  "name": "n8n-pulumi",
  "main": "index.ts",
  "scripts": {
    "build": "tsc",
    "up": "pulumi up",
    "preview": "pulumi preview",
    "destroy": "pulumi destroy"
  },
  "devDependencies": {
    "@types/node": "^18.19.123",
    "ts-node": "^10.9.2",
    "typescript": "^5.9.2"
  },
  "dependencies": {
    "@pulumi/command": "^1.1.0",
    "@pulumi/gcp": "^8.41.1",
    "@pulumi/pulumi": "^3.191.0",
    "@pulumi/random": "^4.18.3"
  }
}
  1. .gitignore
/Pulumi.*.yaml

✅ 修正後の .gitignore

/bin/
/node_modules/
/Pulumi.*.yaml

3) Pulumi Config に値を登録

Pulumi スタックを選択(dev 例)

pulumi stack select dev

現状の設定を確認する

pulumi config

設定する項目

# GCP 設定(new時に入れていなければ)
pulumi config set gcp:project n8n-chelsea-labs
pulumi config set gcp:region asia-northeast1

# Supabase 接続情報
pulumi config set supabaseHost "db.xxxxx.supabase.co"
pulumi config set supabasePort "5432"
pulumi config set supabaseDatabase "postgres"
pulumi config set supabaseUser "postgres"
pulumi config set --secret supabasePassword "YOUR_SUPABASE_DB_PASSWORD"

4) index.ts を作成(実装本体)

import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";
import * as random from "@pulumi/random";
import * as command from "@pulumi/command";

// ===== Config =====
const cfg = new pulumi.Config();
const project = gcp.config.project!;
const region = gcp.config.region!; // pulumi config の gcp:region を利用

const supabaseHost = cfg.require("supabaseHost");
const supabasePort = cfg.get("supabasePort") || "5432";
const supabaseDatabase = cfg.get("supabaseDatabase") || "postgres";
const supabaseUser = cfg.get("supabaseUser") || "postgres";
const supabasePassword = cfg.requireSecret("supabasePassword");

// ===== Enable required APIs =====
const requiredApis = ["run.googleapis.com", "secretmanager.googleapis.com"];
const apiEnablers = requiredApis.map((svc) =>
  new gcp.projects.Service(svc.replace(/\./g, "-"), {
    service: svc,
    disableDependentServices: true,
    // disableOnDestroy: false, // 必要に応じて
  })
);

// ===== Service Account =====
const sa = new gcp.serviceaccount.Account("n8n-service-account", {
  accountId: "n8n-service-account",
  displayName: "n8n Service Account",
});

// ===== Secret Manager: Supabase DB password =====
const dbPasswordSecret = new gcp.secretmanager.Secret("supabase-db-password", {
  secretId: "supabase-db-password",
  replication: { auto: {} },
}, { dependsOn: apiEnablers });

const dbPasswordVersion = new gcp.secretmanager.SecretVersion("supabase-db-password-v", {
  secret: dbPasswordSecret.id,
  secretData: supabasePassword,
});

// ===== Secret Manager: N8N_ENCRYPTION_KEY(ランダム生成) =====
const encKey = new random.RandomPassword("n8n-encryption-key", { length: 32, special: false });

const encKeySecret = new gcp.secretmanager.Secret("n8n-encryption-key-secret", {
  secretId: "n8n-encryption-key",
  replication: { auto: {} },
}, { dependsOn: apiEnablers });

const encKeyVersion = new gcp.secretmanager.SecretVersion("n8n-encryption-key-v", {
  secret: encKeySecret.id,
  secretData: encKey.result.apply(v => v), // Output<string> を渡す
});

// ===== IAM: Cloud Run の SA に Secret 読み取り権限 =====
const dbSecretIam = new gcp.secretmanager.SecretIamMember("db-secret-accessor", {
  secretId: dbPasswordSecret.secretId,
  role: "roles/secretmanager.secretAccessor",
  member: pulumi.interpolate`serviceAccount:${sa.email}`,
});
const encSecretIam = new gcp.secretmanager.SecretIamMember("enc-secret-accessor", {
  secretId: encKeySecret.secretId,
  role: "roles/secretmanager.secretAccessor",
  member: pulumi.interpolate`serviceAccount:${sa.email}`,
});

// ===== Cloud Run (v2) Service =====
const serviceName = "n8n-supabase-service";

const n8n = new gcp.cloudrunv2.Service("n8n-service", {
  name: serviceName,
  location: region,
  deletionProtection: false,
  template: {
    serviceAccount: sa.email,
    scaling: {
      minInstanceCount: 1, // コールドスタート回避(課金に注意)
      maxInstanceCount: 5,
    },
    containers: [{
      image: "docker.io/n8nio/n8n:latest",
      ports: { containerPort: 5678 },
      resources: { limits: { cpu: "1", memory: "1Gi" } },
      envs: [
        { name: "DB_TYPE", value: "postgresdb" },
        { name: "DB_POSTGRESDB_HOST", value: supabaseHost },
        { name: "DB_POSTGRESDB_PORT", value: supabasePort },
        { name: "DB_POSTGRESDB_DATABASE", value: supabaseDatabase },
        { name: "DB_POSTGRESDB_USER", value: supabaseUser },
        { name: "DB_POSTGRESDB_SCHEMA", value: "public" },
        { name: "DB_POSTGRESDB_SSL", value: "true" }, // Supabase は SSL 必須
        { name: "N8N_DIAGNOSTICS_ENABLED", value: "false" }, // 任意:テレメトリ無効
        // パスワード & 暗号鍵は Secret Manager 参照
        {
          name: "DB_POSTGRESDB_PASSWORD",
          valueSource: { secretKeyRef: { secret: dbPasswordSecret.name, version: "latest" } },
        },
        {
          name: "N8N_ENCRYPTION_KEY",
          valueSource: { secretKeyRef: { secret: encKeySecret.name, version: "latest" } },
        },
        // 必要なら後で N8N_HOST / PROTOCOL / PORT を追加(Cloud Run は自動でHTTPS化される)
      ],
    }],
  },
}, { dependsOn: [dbSecretIam, encSecretIam, ...apiEnablers] });

// ===== IAM: 公開アクセス(未認証Invoker) =====
const invoker = new gcp.cloudrunv2.ServiceIamMember("n8n-invoker", {
  name: n8n.name,
  location: region,
  role: "roles/run.invoker",
  member: "allUsers",
}, { dependsOn: n8n });      // ← サービス作成後に実行させる

// =====(オプション)デプロイ後にベースURLを注入 =====
// Cloud Run の URL はデプロイ後に確定するため、gcloud で追い書き
const setBaseUrl = new command.local.Command("set-base-url", {
  create: pulumi.interpolate`gcloud run services update ${serviceName} --project=${project} --region=${region} --set-env-vars=WEBHOOK_URL=${n8n.uri},N8N_EDITOR_BASE_URL=${n8n.uri} --format=none`,
}, { dependsOn: n8n });

// ===== Outputs =====
export const n8nServiceAccountEmail = sa.email;
export const n8nServiceUrl = n8n.uri;

5) デプロイ

pulumi preview   # 作成プランを確認
pulumi up        # yes で実行
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次