在netlify上部署golang web应用

613 阅读5分钟

介绍

Netlify是一个专门托管静态文件的云。这使得它非常适合托管开发人员博客、宣传册网站,甚至只是一个个人简历。它甚至内置了对Hugo的支持。但是Netlify也有各种动态托管解决方案,他们的functions服务是托管Go Web应用程序的一种非常简单的方法,而且通常是免费的。

NetlifyNetlify Status

GitHubiconmonstr-github-1.svg

实现

假设我们有一个静态HTML网页,但我们希望在页面上有一个动态填充的信息流。例如,网页本身是静态的,但使用JavaScript从API中提取相关链接。这是在不牺牲包含其他内容的能力的情况下为页面保留较长缓存时间的好方法。理想情况下,我们可以自己部署网页,但无需部署一个定期将链接转储到数据库中的爬虫程序。

举个例子,让我们使用知乎热榜,它来自www.zhihu.com/api/v3/feed… ,让后端获取我们需要的信息,通过API提供给静态网页。

Go可以很容易地编写一个将JSON格式内容的URL转换为JSON的服务。

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

var (
	port = flag.Int("port", -1, "specify a port")
)

func main() {
	flag.Parse()
	http.HandleFunc("/api/feed", feed)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", *port), nil))
}

func feed(w http.ResponseWriter, r *http.Request) {
	url := "https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50&desktop=true"
	method := "GET"

	client := &http.Client{}
	req, err := http.NewRequest(method, url, nil)

	if err != nil {
		fmt.Println(err)
		return
	}

	res, err := client.Do(req)
	if err != nil {
		fmt.Println(err)
		return
	}
	defer res.Body.Close()

	body, err := ioutil.ReadAll(res.Body)
	if err != nil {
		fmt.Println(err)
		return
	}

	w.Header().Set("content-type", "application/json")
	w.Write(body)
}

如果我们从命令行使用go run myfile.go -port 8000运行它,Go标准包flag将为我们解析-port 8000,并提供一个在8000端口的API。

当我们查看Netlify的文档时,我们发现我们的第一个挑战是他们使用AWS Lambda,而不是普通的HTTP 服务,因此我们要么需要重写我们的服务以使用Lambda,要么找到一个适配器。最好避免将架构与特定的云提供商紧密耦合,因为这会导致难以从平台迁移,因此无需编写不必要的AWS特定代码。

AWS Lambda和Go标准http包之间的适配器已经存在。它们采用AWS Lambda函数使用和返回JSON对象,并将它们调整为正常的Gohttp.Handler调用。

Gateway使在本地开发中试用我们的代码变得容易,而无需启动某种AWS Lambda模拟器。

package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"github.com/apex/gateway"
)

var (
	port = flag.Int("port", -1, "specify a port")
)

func main() {
	flag.Parse()

	http.HandleFunc("/api/feed", feed)
	listener := gateway.ListenAndServe
	portStr := "n/a"

	if *port != -1 {
		portStr = fmt.Sprintf(":%d", *port)
		listener = http.ListenAndServe
		http.Handle("/", http.FileServer(http.Dir("./public")))
	}

	log.Fatal(listener(portStr, nil))
}package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"

	"github.com/apex/gateway"
)

var (
	port = flag.Int("port", -1, "specify a port")
)

func main() {
	flag.Parse()

	http.HandleFunc("/api/feed", feed)
	listener := gateway.ListenAndServe
	portStr := "n/a"

	if *port != -1 {
		portStr = fmt.Sprintf(":%d", *port)
		listener = http.ListenAndServe
		http.Handle("/", http.FileServer(http.Dir("./public")))
	}

	log.Fatal(listener(portStr, nil))
}

使用这个版本的代码,如果我们运行go run main.go -port 8000,服务器将在http://localhost:8000 上启动,但如果我们省略一个端口,它将以AWS Lambda模式启动。它还以HTTP模式运行文件服务器,因此我们可以在开发时在浏览器中预览正在处理的静态文件。

接下来,让我们添加一个带有一些基本JavaScript的静态网页:

static/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go Web App in Netlify</title>
</head>
<body>
    <h1>知乎热榜</h1>
    <ul id="contents"></ul>

    <script type="module">
        let options = {
            weekday: "short",
            year: "numeric",
            month: "short",
            day: "numeric",
            hour: "numeric",
            dayPeriod: "short"
        };

        const toDate = new Intl.DateTimeFormat("default", options);

        (async () => {
            let data = await fetch("/api/feed").then(rsp => rsp.json());
            let listEls = [];
            for (let item of data["data"]) {
                let row = document.createElement("li");
                let link = document.createElement("a");
                link.href = item["target"]["url"];
                link.innerText = item["target"]["title"];
                row.append(link);
                let d = new Date(item["target"]["created"]);
                row.innerHTML += " " + toDate.format(d);
                listEls.push(row);
            }
            let el = document.getElementById("contents");
            el.append(...listEls);
        })();
    </script>
</body>
</html>

至此,开发工作基本完成,现在需要操作Netlify。我们可以通过创建一个简单的构建命令和一个netlify.toml配置文件来告诉Netlify如何使用我们的静态文件和Go函数。

我们可以添加build.sh,告诉Go将我们的二进制文件编译到functions目录中。

set -euxo pipefail

mkdir -p "$(pwd)/functions"
GOBIN=$(pwd)/functions go install ./...
chmod +x "$(pwd)"/functions/*
go env

接下来我们的netlify.toml文件指定Netlify应该使用该脚本来构建我们的函数并在functions目录中查看它们。

[build]
  command = "./build.sh"
  functions = "functions"
  publish = "static"

[build.environment]
  GO_IMPORT_PATH = "github.com/surzia/go-netlify-app"
  GO111MODULE = "on"

由于我们在这个项目中使用Go模块进行依赖管理,我们还需要告诉Netlify将GO111MODULE设置为on,并为Netlify提供一个带有GO_IMPORT_PATH的导入路径,以作为构建Go文件的起点。我们可以在项目根目录中添加一个.go-version文件来指定我们想要的Go版本。

Netlify在/.netlify/functions/binary-name中提供其所有功能。我们可以重写我们的JavaScript以使用它们的URL。但如果可能的话,最好避免与供应商不必要的紧密耦合。

Netlify为我们提供了一个简单的解决方案,那就是他们的URL重写选项。我们可以将其添加到netlify.toml的末尾:

[[redirects]]
  from = "/api/*"
  to = "/.netlify/functions/gateway/:splat"
  status = 200

现在,对/api/*的任何请求都将发送到我们的二进制文件,称为gatewaystatus = 200意味着这应该作为服务器端重写来完成,因此不会有任何客户端重定向。

Netlify每月为我们提供125000次免费函数调用。这对于大约每三分钟一个请求来说已经足够了。作为最终的服务增强,最好通过缓存响应来确保我们不会使用比预期更多的函数调用。Netlify尊重标准的Cache-Control标头。通过添加w.Header().Set("Cache-Control", "public, max-age=300"),我们可以告诉Netlify的CDN每5分钟只向我们的函数发出一次请求,并且在我们网站的所有用户之间共享相同的响应。

结论

Netlify绝对不是托管Go服务的唯一方法,但它是最简单的方法之一。 通过其内置的Github和Gitlab集成,只需将Netlify指向一个存储库,它就会在推送到master时自动部署,并创建Pull Requests的部署预览。如果你只是想快速托管一些东西,而不需要花费几天的时间在环境变量秘密共享、内容交付网络缓存、持续交付等的初始设置上,那么Netlify就是不错的选择。

NetlifyNetlify Status

GitHubiconmonstr-github-1.svg