LY Corporation Tech Blog

支持 LY Corporation 和 LY Corporation Group (LINE Plus, LINE Taiwan and LINE Vietnam) 服務,宣傳技術和開發文化。

從無到有:用 Terraform 與 GitOps 打造企業級 n8n 自動化流程引擎

前言:

大家好,我是 Evan,今天想快速介紹一下,如何建置當前很紅的自動化工具 n8n,n8n 作為一款強大的開源自動化工具,提供豐富的節點使其能串接各種服務,實現複雜的自動化流程與應用。目前公司內部也正積極推廣。然而,要將 n8n 從單機運行模式提升至可維護、可擴展的企業級服務,一套穩固的基礎設施與標準化的部署策略便不可或缺。

這次的目標不僅是部署 n8n,更是要實踐「用自動化來管理自動化工具」。因此我將避免手動操作雲端服務商所提供的 UI 介面,轉而採用更自動化的方法,直接透過程式從頭開始建置所有基礎設施與服務。

因此,本文將分享如何運用 LINE 內部的平台,透過 Terraform 的Infrastructure as Code (IaC) 特性與以 ArgoCD 為核心的 GitOps 理念,完整建置一套包含負載均衡並具備高安全性的 n8n 服務。

image

source: Link

核心理念:

  • 基礎設施即代碼 (Infrastructure as Code, IaC): 透過 Terraform 程式碼來定義與管理所有雲端資源(VM、Kubernetes 叢集、LB、DNS)。此舉可確保環境的一致性、可重複性,並實現版本控制。
  • GitOps: 以 Git 作為部署的唯一真相來源 (Single Source of Truth)。所有應用程式的配置變更都必須經由 Git 提交,再由 ArgoCD 自動同步至 Kubernetes 叢集,從而建立一個可追蹤、可審計的部署流程。

Part 1: Terraform 自動化建立雲端基礎設施 (IaC)

一切的起點,是定義我們服務所需的雲端資源。我們使用 Terraform 來宣告式地管理所有基礎設施。

Step 1: 環境初始化與 Provider 設定

要與平台溝通,首先必須設定 Terraform 的 providerprovider.tf 是我們的入口點,它告訴 Terraform 要使用哪個服務商的 API。

# provider.tf
terraform {
  required_version = ">= xxx"
  required_providers {
    選用的服務商 = {
      source  = "your-company-registry"
      version = ">= xxx"
    }
  }
}

provider "選用的服務商" {
    object storage {}
}

💡 實踐技巧:
為了管理敏感憑證與環境變數,我們使用 .envrc 檔案儲存這些資訊,並將其加入 .gitignore。搭配 direnv 工具,可以在進入專案目錄時自動載入環境變數,避免了手動 source 的繁瑣與疏忽,同時有效隔離不同專案的環境。

Step 2: 創建 Kubernetes Service 叢集

n8n 將以容器化的方式運行,因此我們需要一個 Kubernetes cluster。透過 xxx_cluster 資源來定義叢集規格。

 # (示意)
resource "xxx_cluster" "n8n-dev" {
  k8s_version      = "v1.xx.2"
  name             = "xxx-dev"
  network_plugin   = "xxx"
  # ... 其他設定 ...
}

resource "xxx_pool" "n8n-worker" {
  cluster_id = xxx__cluster.n8n-dev.id
  flavor_id  = "304016xxxx" # VM 的規格
  image_id   = "e7xxxx-d47f-4573-b1af-xxxxxxxxx # OS 映像檔
  quantity   = x # 節點數量
  # ... 其他設定 ...
}

# 引用 SRE 團隊維護的標準化 Ingress 節點池
module "xxx-pool" {
  source     = "git::https://xxxxx"
  cluster_id = xxx
  cmdb_owner = ["xxxxx"]
  # ... 其他設定 ...
}
 

💡 實踐技巧與經驗:

  • 模組化 (Modularity): 直接引用由 SRE 團隊提供的 pool Terraform 模組,讓我們無需關心 Ingress Controller (Traefik) 節點的底層細節,只需傳入必要參數,即可獲得一個符合團隊規範的標準化節點池,顯著降低了維護成本並確保一致性。
  • 問題排解 (Quota 不足): 執行 terraform apply 時遇到的 Exceeded Quota 錯誤是常見問題。除了刪除閒置資源外,更進階的作法是使用 terraform import 將現存但未被 Terraform 管理的資源,納入當前狀態檔(state)中,以避免重複創建。

Step 3: Terraform State 遠端備份至 object storage

terraform.tfstate 是 Terraform 的核心,記錄了所有受控資源的狀態。將其保存在本地不利於團隊協作且風險極高。因此,我們將其遷移至我們內部儲存空間,它是一個 S3 相容的物件儲存服務。

# backend.tf
terraform {
    backend "s3" {
        bucket                      = "xxx-bucket"
        endpoint                    = "https://xxxxx"
        region                      = "xxx"
        key                         = "tfstate.tfstate"
        skip_requesting_account_id  = true
        skip_credentials_validation = true
        skip_region_validation      = true
        use_lockfile                = true # 啟用狀態鎖,防止多人同時執行
    }
}

💡 關鍵功能:

  • 狀態鎖 (State Locking): 遷移至 S3 backend 後,最重要的功能之一就是狀態鎖。當有成員執行 terraform apply 時,Terraform 會在公司內部的 object storage 上創建一個鎖定檔,防止其他人同時修改基礎設施,避免狀態不一致與資源衝突,是團隊協作的必要保障。
  • 遷移流程: 從 local backend 遷移至 s3 backend 需謹慎操作。標準流程為:terraform state pull -> 修改backend.tf配置文件 -> terraform init -reconfigure,確保本地的最新狀態能平滑地推送到遠端。

Part 2: 設計高可用網路架構

基礎設施就緒後,我們需要規劃如何將流量安全、高效地引導至我們的 n8n 服務。

Step 4: 雙層負載均衡 (Load Balancer) 架構

為了區分對外提供服務的 UI 和僅供內部系統使用的 Webhook,我們設計了公私網分離的雙層 LB 架構:

  • Public LB: 面向公網,負責將 n8n-tw.xxx-beta.com 的流量轉發至 Traefik Ingress Controller。這是給一般使用者訪問 n8n UI 的入口。
  • Private LB: 僅在內部網路可見,負責處理 webhook 的流量。這為 Webhook 提供了一個更安全的接入點。
# lb.tf (示意)
# --- Private LB ---
resource "xxx_lb" "xxx" {
  name     = "xxx-private"
  vip_type = "private"
  # ...
}
resource "xxx_lb_revision" "xxx" {
  load_balancer_id = xxx
  # ... 監聽 80/443,轉發至 Traefik 的 10080/10443
  dynamic "xxx" {
    for_each = local.xxx
    content {
      hostname = "${xxx}.xxx.com."
      port     = xxx
  }
  # ...
}

# --- Public LB (設定類似,但 vip_type = "public") ---
resource "xxx_lb" "xxx-public" {
  name     = "xxx-public"
  vip_type = "public"
  # ...
}
# ...

Step 5: DNS 解析鏈路

有了 LB,我們還需要設定 DNS,將 domain 指向複雜的 LB IP address。

# dns.tf (示意)
# 1. A Record: 將 LB 域名指向其 VIP
resource "xxx_dns_record_set" "xxx-public-lb-xxx-com" {
  name    = "xxx-public.xxx.com."
  type    = "A"
  records = [xxx_lb_revision.xxx]
  # ...
}

# 2. CNAME Record: 將最終使用者域名指向 LB 域名
resource "xxx_dns_record_set" "xxx-com" {
  name    = "n8n-tw.xxx.com."
  type    = "CNAME"
  records = ["xxx-public.xxx."]
  # ...
}

架構解析:
這樣的 使用者域名 -> CNAME -> LB 域名 -> A -> LB IP 解析鏈路提供了極大的靈活性。未來如果需要更換 LB,我們只需要修改 A 紀錄,而使用者的 CNAME 域名完全不受影響。


Part 3: GitOps 實現應用程式持續部署

基礎設施完成後,將轉向去對應用程式進行部署。所有 Kubernetes Manifests 將由一個獨立的 Git Repository 管理,並由 ArgoCD 負責同步。

image

source: Link

這邊就需要自行去寫 Kubernetes manifest 了!

Step 6: Ingress - 流量路由的最後一哩路

外部的負載均衡器(Load Balancer)成功地將流量送到了我們 Kubernetes 叢集的節點上,但接下來流量該何去何從?這時就需要叢集內部的「交通指揮官」—— Ingress Controller。

image

source: Link

這邊先快速介紹一下 Ingress Controller,在我們的架構中,這個角色由 Traefik 擔任。Ingress Controller 是一個在 Kubernetes 內部運行的應用程式,它專門負責接收來自外部的流量,並根據我們設定的規則,智慧地將這些流量分發到叢集內部的不同服務(Service)。

而在 Part 2 中,其實我們已經透過 Terraform 去引用了我們 SRE 團隊所提供的 node 的 pool 模組。這一步非常重要,因為它正是我們 Ingress 架構的基礎。

這個由 SRE 提供的 node 的 module 不僅僅是建立了一組普通的 node,它實際上為我們部署了一套由 SRE 團隊統一維護、標準化且高可用的 Ingress Controller,在我們的例子中就是 Traefik。

這意味著我們作為服務的開發者,無需自行安裝、設定或維護 Traefik 本身。因為 SRE 團隊已經為我們準備好了這個叢集內的「交通指揮官」,並確保了它的穩定與安全。

image

source: Link

因此我們的任務,就是創建 Ingress 這種 Kubernetes 資源,也就是去建立 Ingress.yaml,可以把它理解成一份「路由設定檔」,用來告訴這個已經存在的 Traefik:「當有流量進來時,請根據這些規則,將它轉發到我指定的服務」。

所以 Load Balance 將流量導入叢集內的 Traefik,而 Traefik 則依賴 Ingress 資源來決定如何將流量路由到最終的目標服務。

# ingress.yaml
apiVersion: xxx
kind: Ingress
metadata:
  name: xxx-ingress
  annotations:
    kubernetes.io/ingress.class: traefik # 指定由 traefik 處理
spec:
  rules:
  - host: xxx.com
    http:
      paths:
      - path: /
        pathType: xxx
        backend:
          service:
            name: n8n
            port:
              number: 5678

Step 7: 服務暴露的選擇 - ClusterIP vs. NodePort

這是一個關鍵的資安與架構選擇。最初,Service 可能被設定為 NodePort,這會直接在每個節點上暴露一個端口。

NodePort 的問題:

  1. 安全風險: 它繞過了我們的 LB 和 Ingress,任何知道節點 IP 的人都可以直接訪問該服務,失去了統一的流量入口和安全控制。
  2. 架構混亂: 在 Ingress 架構下,流量應該是 LB -> Ingress Controller -> Service -> PodNodePort 創造了一個不必要的旁路。

我們的選擇 ClusterIP:
我們將 Service 類型修改為 ClusterIP。這種 Service Type 只會在叢集內部創建一個虛擬 IP,只能從叢集內部訪問。

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: n8n
spec:
  type: ClusterIP # 正確的選擇!
  selector:
    app: n8n
  ports:
    - protocol: TCP
      port: 5678
      targetPort: 5678

這確保了所有外部流量都必須經過我們精心設計的 LB -> Traefik 路徑,實現了真正的安全隔離。

Step 8: Git Push 觸發 ArgoCD 同步

當我們將 ingress.yaml、修改後的 service.yaml 以及 kustomization.yaml 推送到 Git Repo 後,ArgoCD 會自動偵測到變更,將其與叢集內的當前狀態進行比對,並應用這些變更以完成部署。這就是 GitOps 工作流程的核心。

image

結論

透過整合 Terraform、Kubernetes 與 ArgoCD,不僅是單單部署了一個 n8n 服務,更是實踐了一整套現代化的 Cloud Native 應用交付流程。此架構也在可維護性、可擴展性與安全性方面帶來了顯著的提升,並減少了人為操作時可能發生的失誤,也使每次的更動都有據可查。維護性大幅提升!!

這次建置的過程真的是一次很寶貴的學習經驗,直接實作涵蓋了從底層網路、基礎設施、容器編排到應用層部署的完整生命週期。讓我用一篇文章來記錄一下這次整個開發的流程,也讓更多人能走進 Terraform 的世界。