githubEdit

Go, Alpine & hosts file

Vấn đề

Team mình đang xây dựng một tính năng gửi email bằng Go với đoạn code như sau:

package tools

import (
	"log"
	"net/smtp"
	"os"
	"strings"

	"github.com/gomarkdown/markdown"
	"github.com/subosito/gotenv"
)

func init() {
	gotenv.Load()
}

func Getenv(key, fallBack string) string {

	value := os.Getenv(key)
	if len(value) == 0 {
		return fallBack
	}
	return value
}

func SendEmail(to, cc, bcc, body, subject string) bool {
	port := Getenv("SMTP_PORT", "587")
	smtpServerHost := Getenv("SMTP_SERVER", "smtp.gmail.com")
	senderEmail := Getenv("SMTP_USERNAME", "")
	senderPassword := Getenv("SMTP_PASSWORD", "")

	if smtpServerHost == "" || senderEmail == "" || senderPassword == "" {
		return false
	}

	headers := "From: " + senderEmail
	headers = headers + "\nTo: " + to
	if len(cc) > 0 {
		headers += "\nCc: " + cc
	}
	if len(bcc) > 0 {
		headers += "\nBcc: " + bcc
	}
	headers = strings.ReplaceAll(headers, ",", ";")

	smtpServer := smtpServerHost + ":" + port
	mime := "Content-Type: text/html; charset=UTF-8"
	body = string(markdown.ToHTML([]byte(body), nil, nil))
	msg := []byte(headers + "\nSubject: " + subject + "\n" + mime + "\n" + body + "\n")
	auth := smtp.PlainAuth("", senderEmail, senderPassword, smtpServerHost)
	err := smtp.SendMail(smtpServer, auth, senderEmail, strings.Split(to, ","), msg)

	if err != nil {
		log.Println(err)
		return false
	}
	return true
}

Dockerfile để build Docker image:

Do dùng mail server công ty không mở public nên phải dùng thông qua proxy, mình chạy câu lệnh docker run --rm --add-host=stmp.company.example:smtp.my-proxy.example image-name:1.0.0 send-email thì container vẫn gọi trực tiếp vào smtp.company.example chứ không gọi thông qua proxy.

Chú thích:

  • image-name là tên image mình build ra.

  • send-email là command gọi function SendEmail đã được xử lý và viết trong file main.go của dự án.

  • Một số tham số liên quan nhưng không quan trọng đã được bỏ ra khỏi command ví dụ.

Tìm hiểu và giải quyết

Sau khi đọc nhiều link trên mạng thì nguyên nhân có thể được mô tả vắn tắt như sau:

The Name Service Switch (NSS) configuration file, /etc/nsswitch.conf, được sử dụng bởi GNU C Library và một vài ứng dụng khác để xác định nguồn của việc phân giải tên và thứ tự sử dụng các nguồn đó. muslglibc là hai thư viện C, các ứng dụng Go thường được build bằng hai thư viện này. glibc có tính tolerance cao, nên sẽ có trường hợp failed-over cho việc không tìm thấy file /etc/nsswitch.conf, còn musl thì không.

Alpine là musl-based linux và trong image Alpine đã không tồn tại file /etc/nsswitch.conf, cộng với việc build Go bằng thư viện musl (go build -tags musl --ldflags "-extldflags -static" -o gitlab .) nên ứng dụng Go được build ra thì tìm kiếm file /etc/nsswitch.conf không thấy và dừng lại, dẫn đến việc ứng dụng không đọc file /etc/hosts. Với Python thì sẽ đọc file /etc/hosts.

Giải pháp là tạo ra file /etc/nsswitch.conf và cấu hình cho đọc Name-service từ file /etc/hosts. Thêm dòng RUN echo 'hosts: files dns' > /etc/nsswitch.conf vào cuối Dockerfile

  1. https://github.com/golang/go/issues/35305

  2. https://github.com/gliderlabs/docker-alpine/issues/367

  3. https://github.com/docker-library/docker/issues/82#issuecomment-334627834

  4. https://ops.tips/notes/glibc-golang-learnings/

  5. https://github.com/golang/go/blob/4bd95702dd1e81f383ee67c14945620d30247908/src/net/conf.go#L219-L253

Last updated