前言

前陣子使用 AWS 的 EC2 來部署一個測試用的 backend api,以利 Netlify 上的前端網站作溝通,但因為近代瀏覽器(ex: Chrome…)遵守 CSP(Content-Security-Policy) 限制,避免 HTTPS 協定的網站前端 javascript 透過 fetch 方式去索取 HTTP 協定的任何資源,以避免 MIT(中間人攻擊),所以勢必得將自身 backend 的網域加上憑證,起初想到用 Nginx 搭配 Let’s Encrypt 來定期每三個月更新憑證,但後來發現 Caddy 更好用更簡潔,就順手紀錄一下操作的歷程。

Caddy 介紹

Caddy 是一個功能強大、可擴充的平台,可以提供(serve)服務或網站,底層是用 Go 撰寫的,依照 Caddy 官網所說,它不僅僅能作為 Web server 或是 proxy,更能作為 server of servers.

Caddy 預設以 HTTPS 為通訊協定(也支援 HTTP/2),支援 gzip 壓縮,而且不再需要每三個月定期去 Let’s Encrypt 更新憑證,因為 caddy 會自動幫我們更新憑證。此外,運行時不仰賴其他套件或 runtime,可獨立 host 在 container 內。依照官網所說,caddy 已經是 production-ready,可以取代上一世代的 Nginx 或 Apache。

前置作業:創建 EC2

首先,我先在 AWS 上創建 EC2 的 instance:

  • t2.micro 的 Ubuntu 22.04
  • 透過 security group 指派 HTTP/HTTPS/SSH 的 inbound rules
  • 透過 security group 指派 0.0.0.0/0 作為 outbound rules
  • (Optional) 自己去任何一個 DNS Provider(ex: GoDaddy, Gandi, …)註冊一個域名

這邊 AMI(Amazon Machine Image)我不使用 AWS Linux 做為我的 EC2 機器,因為發現 AWS Linux 預設的 OS 版本是 RedHat,在安裝 Caddy 時有遇到用 yum 安裝的問題,所幸改為常用的 Ubuntu 後,就簡單解決了

安裝 Caddy

依照 Caddy 官網 所說的 ubuntu 步驟安裝

sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

安裝好後,執行 caddy,出現底下訊息就代表安裝成功

$caddy
Caddy is an extensible server platform written in Go.

At its core, Caddy merely manages configuration. Modules are plugged
in statically at compile-time to provide useful functionality. Caddy's
standard distribution includes common modules to serve HTTP, TLS,
...

接著,打開瀏覽器連到 EC2 的 HTTP 位址,就會看到底下畫面

caddy-default

除了本地安裝以外,也能使用 Docker 來 host Caddy,docker image 在此

docker run -d -p 80:80 -v caddy_data:/data caddy

執行 Caddy

Caddy 允許使用者用兩種方式來運行,一種是 API-based,另一種是 CLI-based,前者是透過 request 調用 caddy 定義的 api endpoint,語法上使用 json,可用 REST Client 這類 API 工具來管理,後者是搭配 Caddyfile 這個配置檔和 shell command 來完成 Caddy 的運行(其實 CLI 底層也是去呼叫 API),而我個人是習慣用 Caddyfile 來管理,語法不再是 json,而是 Caddy 定義的更 human-readable 的語法,使用方式雷同 Nginx 的配置檔。

執行 caddy run 啟動 caddy server 後,Caddy 預設會在 http://localhost:2019 作為 admin 的控制 endpoint

常用的 Caddy 指令如下:

# help指令
caddy

# 運行caddy server,預設會找當前路徑的Caddyfile
caddy run

# 依照指定的Caddyfile路徑來運行caddy server
caddy run --config /path/to/Caddyfile

# 背景運行caddy server
caddy start

# 當使用背景運行時,停止caddy server
caddy stop

# Reload caddy config來完成zero downtime,當新的 caddy 服務啟動失敗,將會 rollback 回舊版,確保零停機
caddy reload

撰寫 Caddyfile

用 CLI-based 與 Caddy 互動時常會搭配 Caddyfile 作為驅動的配置檔,整體 Caddyfile 的撰寫架構如 官網 這張圖所示:

caddyfile-visual

需注意的是這篇文章使用的是 caddy2,不是 caddy1,因爲兩者在撰寫配置上語法有些微差異,詳細語法可查看 https://caddyserver.com/docs/caddyfile

底下是官方文件的最簡單範例,在當前路徑下創建一個 Caddyfile

vim Caddyfile

在裡面加上這兩行

localhost {
    respond "Hello, world!"
}

接著,運行 caddy server,連到 HTTPS 的 localhost 就會看到"Hello, world!“這行字!

sudo caddy start

curl https://localhost   # Hello, world!

至此就可以成功看到 Caddy server 已經運行在 HTTPS 上!

用 Caddyfile 撰寫 Reverse Proxy

接著,想把 Caddy server 作為 reverse proxy,而這也是我一開始想解決的問題

example.yourdomain.com {
  reverse_proxy localhost:9000
  tls example@email.com  # Optional
}

配置檔顯淺易懂,第一行是我的 backend api server 的 Domain Name(填上我自己註冊的域名,或是 EC2 的 DNS name),括號裡頭是針對該網域的所有 Caddy 參數,reverse_proxy 用於將所有對該網域的請求都導向 localhost:9000,而我 local 的 api server 就是聽在 9000 port.完成簡單的反向代理需求.tls是 Caddy 向 Let’s Encrypt 定期更新憑證時需要知道我註冊的信箱來做 ACME 考驗

除此之外,要讓前後端都能反向代理,也能透過以下 CaddyFile 實現:

example.yourdomain.com {
  # 開啟 Zstandard 和 Gzip 的壓縮
  encode zstd gzip

  log {
    # 預設路徑: /var/log/access.log
  }

  # Server
  handle_path /api/* {
    reverse_proxy :8000
  }

  # Client
	handle {
    reverse_proxy :3000
  }
}

後端運行在 8000 port,build 好的前端會由 3000 port 來 expose 出去,這邊的 handle_path 會將所有發往 /api/* 的請求都導向 8000 port,而 handle 則會將所有請求(除了 /api/* 以外)導向 3000 port,完成前後端都代理的需求

除了上述功能以外,Caddy 還有其他豐富的功能,我也沒有全部都碰過,詳細請見 官網文件

總結

以上就是 Caddy 的使用心得,因緣際會下使用到 Caddy 這個 lightweight 的 web server,總結來說用起來體感非常好,預設就有 HTTPS 非常的方便,但實際走一遍之後,才發現有遇到滿多坑,還有關於 DNS 的設定與憑證的 ACME 挑戰,之後有機會再來寫一篇文章來仔細研究.