こんにちは、エキサイトでエンジニアをしております。吉川です。
エキサイトHDアドベントカレンダー2025の7日目の記事になります。
この記事の前提とゴール
この記事では 「プライベート環境内にセルフホストしたn8n」 で、Slackのイベント(Events API)をトリガーにワークフローを起動するための構成を紹介します。
- 前提
- ゴール
n8nとは
n8nは、ノーコード/ローコードで様々なサービス間の連携を実現できるワークフロー自動化ツールです。トリガーとアクションを組み合わせることで、複雑な業務プロセスを自動化できます。
n8nには大きく分けて2つの利用方法があります:
- n8n Cloud: n8nが提供するクラウドサービス。インフラの管理が不要で、すぐに利用開始できます
- セルフホスト: 自社のサーバーやクラウド環境に自分でn8nをデプロイする方法。カスタマイズ性が高く、データを自社環境内に保持できます
今回は、セキュリティ要件の観点からセルフホスト版のn8nをプライベートサブネット内に構築し、そこでSlackトリガーを利用する方法について解説します。
セルフホスト版のn8nをプライベートサブネット内に立てる
EC2インスタンスの準備
まず、AWSのプライベートサブネット内のEC2インスタンスを立ち上げ、インスタンス内で以下のようにcompose.yamlファイルを作成します。
services: n8n: image: n8nio/n8n:latest container_name: n8n ports: - "5678:5678" environment: - GENERIC_TIMEZONE=Asia/Tokyo # Slackに登録するRequest URL(=外部公開URL)のベース。 - WEBHOOK_URL=${PUBLIC_WEBHOOK_BASE_URL} volumes: - n8n_data:/home/node/.n8n volumes: n8n_data:
compose.yamlを作成したら、以下のコマンドでn8nを起動します。
# DockerとDockerComposeはインストールされている前提 # n8nコンテナの起動 docker compose up -d
PUBLIC_WEBHOOK_BASE_URL は compose.yaml と同じ階層の .env などで設定します(Slackに登録するRequest URLのベースなので https のURLになります)。
セキュリティグループの設定
プライベートサブネット内のn8nは、外部からの不正アクセスを防ぐために、セキュリティグループで厳格なアクセス制御を設定します。
- インバウンドルール:
- ポート5678(n8nのUIアクセス):社内の踏み台/VPN/SSM経由など、運用方針に沿った経路のみに限定
- ポート5678(Lambda → n8n 転送用):後述のLambda(またはLambdaに紐づくセキュリティグループ)からのみ許可
この設定により、社内からのみn8nの管理画面にアクセスでき、外部からの直接アクセスは遮断されます。

セルフホスト版n8nとSlackトリガー
Slackトリガーの基本設定
n8nでSlackトリガーノードを追加すると、SlackのEvent Subscriptionsに登録するためのRequest URL(Webhookエンドポイント)が提示されます。
このURLをSlackアプリのEvent Subscriptionsに設定することで、Slackでのイベント(メッセージ投稿、リアクション追加など)をトリガーにワークフローを実行できます。

問題:SlackのIPアドレスが固定されていない
ここで問題が発生します。Slackはグローバルな分散インフラストラクチャで運用されており、Webhookリクエストの送信元IPアドレスが固定されていません。そのため、セキュリティグループで特定のIPアドレスのみを許可する設定では、Slackからのリクエストを受け付けることができません。
かといって、すべてのIPアドレスからのアクセスを許可してしまうと、セキュリティリスクが高まり、プライベート環境に配置した意味がなくなってしまいます。
Lambdaを経由してn8nに送る
この問題を解決するために、AWS API Gateway + Lambdaを中継ポイントとして利用します。
この構成のポイントは以下です。
- Slackは インターネット上のHTTPSエンドポイント(= API Gateway)にしか投げられない
- n8nは プライベート環境内に閉じたまま、Lambdaからのみ到達可能にする
- Slackリクエストは IP制限ではなく署名検証(Signing Secret) で正当性を担保する
注意点として、SlackのEvents APIには「最初のURL検証(url_verification)」や「応答は概ね3秒以内」などの要件があるため、Lambda側でそれを満たす実装にします。
API Gatewayの設定
API GatewayでREST APIを作成し、エンドポイントを公開します。このURLをSlackアプリのEvent SubscriptionsのRequest URLに設定します。
Lambda関数の実装
Lambda関数では以下の処理を実装します。
- Slackからのリクエスト検証
- n8nへのリクエスト転送
プライベートサブネット内にリクエストを送信するため、LambdaのVPC設定で上記のEC2と同じVPC・サブネットを設定します。またEC2のセキュリティグループで、Lambdaのセキュリティグループからのアクセスを許可します。
また環境変数(必要ならSecrets Manager等)にSLACK_SIGNING_SECRET と N8N_TRIGGER_URL を設定します。N8N_TRIGGER_URL はn8nが表示するWebhook URL(上記のPUBLIC_WEBHOOK_BASE_URL)の パスはそのままに、ベースだけを http://<EC2のIP>:5678 にしたものを使います。
Lambdaのコードは以下のようになります。
import os import json import hmac import hashlib import base64 from datetime import datetime import requests # Slackからのリクエスト検証 def validate_request_from_slack(slack_signing_secret: str, headers: dict, body: str) -> bool: timestamp = headers.get("X-Slack-Request-Timestamp") slack_signature = headers.get("X-Slack-Signature") # 直近のリクエストか検証 current_time = int(datetime.now().timestamp()) if abs(current_time - int(timestamp)) > 60 * 5: return False # 署名検証 sig_basestring = f"v0:{timestamp}:{body}" signature = ( "v0=" + hmac.new( slack_signing_secret.encode("utf-8"), sig_basestring.encode("utf-8"), hashlib.sha256, ).hexdigest() ) return hmac.compare_digest(signature, slack_signature) # n8nへのリクエスト転送 def send_n8n(n8n_trigger_url: str, body: dict) -> None: requests.post( url=n8n_trigger_url, headers={"Content-Type": "application/json"}, json=body, ) def lambda_handler(event, context): headers = event.get("headers") or {} body = event.get("body") or "" if event.get("isBase64Encoded"): body = base64.b64decode(body).decode("utf-8") slack_signing_secret = os.environ.get("SLACK_SIGNING_SECRET", "") if not slack_signing_secret or not validate_request_from_slack(slack_signing_secret, headers, body): return {"statusCode": 401, "body": "invalid signature"} # SlackのURL検証(Event Subscriptionsを有効化する際に必須) try: payload = json.loads(body) if body else {} except json.JSONDecodeError: payload = {} if payload.get("type") == "url_verification": return { "statusCode": 200, "headers": {"Content-Type": "application/json"}, "body": json.dumps({"challenge": payload.get("challenge", "")}), } n8n_trigger_url = os.environ.get("N8N_TRIGGER_URL", "") if not n8n_trigger_url: return {"statusCode": 500, "body": "missing N8N_TRIGGER_URL"} send_n8n(n8n_slack_trigger_url, payload) return {"statusCode": 200, "body": "ok"}
ポイント:
- SlackのEvent Subscriptionsは最初に
url_verificationを投げてくるため、challenge応答がないと有効化できない - 署名検証は 「受け取った生のbody」 に対して行う(JSONとしてパースして並び替えると署名が一致しなくなる)
まとめ
セルフホスト版のn8nは、それ自体は無料(EC2料金は別途かかる)で使用でき、データも自社環境内に保持できるため、機密情報を扱う業務フローでも安心して利用できます。
また、非エンジニアの方でもn8nのGUIを使って安全に業務自動化を進められるようになり、組織全体の業務効率化に貢献できると考えています。
セキュリティ要件が厳しい環境でのワークフロー自動化を検討されている方の参考になれば幸いです。