開始接觸 Pulumi 後,發現 GCP + Python 可以參考的資料很少,以建置 GCP HTTP Load Balancer 來說,Pulumi 官方提供了 Typescript 的寫法1,若自行對應相同資源的 Python API 範例,則會在執行時遇到一些小錯誤(稍後會提到)。因此本篇將會分享如何透過 Python 建立 GCP HTTP Load Balancer,完整的實作程式碼請至 GitHub 2查看。

架構說明

透過 HTTP Load Balancer,將請求導流至後端的 Nginx 伺服器,由於 VM 在初始化時,會透過 startup script 從外網抓取相關套件並安裝,因此需要為 VM 配置 Public IP 或 NAT proxy,讓 VM 可以與網際網路進行通訊。但為安全考量,我們只允許來自 Load Balancer 的請求,禁止透過 VM 的 Public IP 來存取服務。

Pulumi 程式說明

1. 建置基本的網路資源

在這個階段,我們主要會建置 VPC、一個靜態的 Public IP(等會會給 Load Balancer 使用),以及設定防火牆規則:

network_base.py:

def setup():
    # STEP 1. Create VPC network
    compute_network = compute.Network("network")

    # STEP 2. Create IP address for load balancer
    lb_addr = compute.GlobalAddress("load-balancer-address")

    # STEP 3. Create firewall rules to VPC network
    compute_firewall = compute.Firewall(
        "firewall",
        network = compute_network.name,
        allows = [compute.FirewallAllowArgs(
            protocol = "tcp",
            ports = ["80"]
        )],
        # Only allow the traffic from load balancer and health probe
        source_ranges = [lb_addr.address, "35.191.0.0/16", "130.211.0.0/22"]
    )

如同一開始說明的安全要求,在防火牆的配置中,source_ranges 只允許來自我們預先配置的 Load Balancer IP 和 Health check 探測 IP,以及 Port 80。

切記要開放 Health check 的來源 IP,否則會發現建置完所有資源後,HTTP 的請求會失敗,這是由於 Healty 的 Instance 為 0,所以沒有後端機器可處理請求。


2. 建置 Nginx 伺服器

模組化 Nginx 伺服器的配置,並且透過 metadata_startup_script 來幫我們在建立 VM 時,就安裝好 Docker 環境,並啟動一個 Nginx 的容器(mapping port 為 80:80):

Server.py:

def create_nginx_server(...):
    ...
    return compute.Instance(
        ...
        metadata_startup_script = startup_script,
        ...
    )

接下來,指定我們想要建立的 Nginx 伺服器數量,此範例將建立 2 台 VM (count = 2)

servers.py:

from models import Server

def setup(count, ...):
    ...
    for i in range(count):
        compute_instance = Server.create_nginx_server(
            ...
        )

3. 建置 HTTP Load Balancer

GCP 在處理負載平衡時,主要分為三個元件:

  • Forwarding Rule
    • 設定請求要由哪個 Proxy 處理。
  • Proxy
    • GCP 提供了多種 Proxy,例如: HTTPS Proxy、TCP Proxy 等,此範例我們使用 HTTP Proxy 來處理 HTTP 的請求。
  • URL Map
    • URL Map 則用於處理請求要導向哪個後端服務(Backend service),例如: host 為 a.com 的請求導向後端服務 A,host 為 b.com 的請求導向後端服務 B,也可以根據 path 設定規則。

因此,要完成 HTTP Load Balancer 配置的大致步驟為:

  1. 建立 Instance Group,並加入前面所建立的 VMs
  2. 建立 HTTP Load Balancer,並配置:
    1. Backend service (指向剛所建立的 Instance Group)
    2. Health Check
    3. URL Map
    4. Frontend 的 Forwarding Rule

load_balancer.py:

def setup(address, servers):
    # STEP 1. Create instance group
    webservers = compute.InstanceGroup("webservers",
        instances = servers,
        ...
    )

    # STEP 2. Add health check
    http_health_check = compute.HealthCheck("http-health-check",
        ...
    )

    # STEP 3. Create backend service
    backend_service = compute.BackendService("default-backend-service",
        health_checks = http_health_check.id,
        backends = [compute.BackendServiceBackendArgs(
            group = webservers.id
        )]
    )

    # STEP 4. Define URL map
    url_map = compute.URLMap("default-url-map",
        default_service = backend_service.id,
        host_rules = [...],
        path_matchers = [...]
    )])

    # STEP 5. Create HTTP proxy
    target_http_proxy = compute.TargetHttpProxy("target-http-proxy",
        url_map = url_map.self_link
    )

    # STEP 6. Forwarding rule for External Network Load Balancing using Backend Services
    forwarding_rule = compute.GlobalForwardingRule("forwarding-rule",
        port_range = "80",
        target = target_http_proxy.self_link,
        ip_address = address
    )

Troubleshooting

如一開始所提的小錯誤,使用官方的範例設定 BackendService 的 health_checks 時:

default_backend_service = gcp.compute.BackendService("defaultBackendService", health_checks=[default_http_health_check.id])

會遇到以下錯誤:

panic: Error reading level config: '' expected type 'string', got unconvertible type '[]interface {}', value: '[74D93920-ED26-11E3-AC10-0800200C9A66]'

這是由於 health_checks 的參數型態應為 str,而非 array,因此需將其修正為 str

default_backend_service = gcp.compute.BackendService("defaultBackendService", health_checks=default_http_health_check.id)

執行部署及部署結果

  1. 部署資源
    $ pulumi up
    
    ...
    Do you want to perform this update? yes
    ...
        Type                                 Name                     Status
    +   pulumi:pulumi:Stack                  loadbalancer-dev         created
    +   ├─ gcp:compute:GlobalAddress         load-balancer-address    created
    +   ├─ gcp:compute:Network               network                  created
    +   ├─ gcp:compute:HealthCheck           http-health-check        created
    +   ├─ gcp:compute:Instance              nginx-server-1           created
    +   ├─ gcp:compute:Instance              nginx-server-2           created
    +   ├─ gcp:compute:Firewall              firewall                 created
    +   ├─ gcp:compute:InstanceGroup         webservers               created
    +   ├─ gcp:compute:BackendService        default-backend-service  created
    +   ├─ gcp:compute:URLMap                default-url-map          created
    +   ├─ gcp:compute:TargetHttpProxy       target-http-proxy        created
    +   └─ gcp:compute:GlobalForwardingRule  forwarding-rule          created
    
    Outputs:
        load-balancer-ip: "<load-balancer-ip>"
    
    Resources:
        + 12 created
    
  2. 打開瀏覽器,並輸入 http://<load-balancer-ip>:80,即可看到 Nginx 的頁面
  3. 輸入 http://<instance-ip>:80,則會被禁止存取

1. Pulumi 官方範例
2. Pulumi 完整程式碼