golang - full static binary 실행파일 만들기(with. cgo)
📖 요약
- golang은 cgo를 사용하면 생성되는 binary가 glibc 의존성을 가지게 됩니다.
- glibc는 LGPL로 static link하면 코드 공개가 필요합니다.
- golang에서 cgo를 포함해 full static binary를 만들고 싶을 경우, MIT 라이센스로 배포되는 glibc의 대체제인 musl를 사용하면 됩니다.
- 단, dependency graph 상의 모든 코드가 glibc대신 musl을 쓰도록 다시 빌드되어야 되므로 OS자체가 musl을 기반으로 하는 alpine linux에서 빌드하는 방법을 추천 합니다.
😄 golang은 기본적으로 statically linked
된 binary를 만들어냅니다.
hello world
의 코드를 아무 옵션없이 그냥 빌드하면 static binary가 생성됩니다.
ubuntu linux에서 빌드 후 file
명령으로 해당 파일의 정보를 확인 해보면 statically linked
라고 표시되는 것을 확인 할 수 있음.
$ go build -o hello.noopt hello.go
$ file hello.noopt
hello.noopt: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
😓 net
또는 os/user
패키지를 사용하면…
domain의 ip를 출력하는 간단한 코드를 작성하고 빌드 옵션없이 빌드 합니다.
package main
import (
"fmt"
"net"
)
func main() {
ipaddrs, err := net.LookupHost("google.com")
fmt.Printf("ipaddrs = %v, err = %v", ipaddrs, err)
}
hello world의 경우와 동일하게 빌드 후 file
명령으로 확인합니다.
$ go build -o getip.noopt getip.go
$ file getip.noopt
getip.noopt: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, with debug_info, not stripped
dynamically linked
라고 표시됩니다. 즉 이 실행파일이 참조하고 있는 동적 라이브러리가 있고, 이 동적 라이브러리가 없는 환경에서는 실행이 불가능하다는 의미입니다.
원인은 os/user, net go package 문서에 나와 있습니다. 공식 문서 확인의 중요성
os/user
, net
두 패키지는 cgo
를 사용하도록 구현되어 있습니다. 두 패키지를 사용하더라도 static binary로 빌드하는 방법은 2가지가 있습니다.
osusergo
,netgo
빌드 tag를 사용하면 pure go로 구현 된 코드를 사용합니다.- 또는 go env 의
CGO_ENABLED=0
으로 빌드해도 동일하게 pure go로 구현된 코드를 사용합니다.
위에 작성했던 코드를 코드 변경없이, netgo
tag를 추가해서 빌드해서 file
명령으로 확인 해보면, statically linked
문구를 확인 할 수 있습니다.
$ go build -tags netgo -o getip.netgo getip.go
$ test file getip.netgo
getip.netgo: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
🤔 cgo
모듈이 꼭 필요한 경우라면?
os/user
와 net
처럼 pure go로 구현하는 방법이 불가능 한 custom 모듈이 libc 의존성을 가진 경우라면 다른 접근이 필요합니다.
C로 작성된 코드의 경우 대부분 libc에 대한 의존성을 가집니다.
glibc도 libc를 구현한 하나의 구현체 중 하나 일 뿐이므로, MIT 라이센스로 배포되는 다른 구현체인 musl을 사용 할 수 있습니다.
다만, 이를 해결 하기 위해서는 2가지 문제를 해결 해야 합니다.
- glibc는 LGPL(GNU Library General Public License)로 배포되므로, static link시 소스코드를 무조건 공개해야 합니다.
- musl 같은 대체 라이브러리를 사용 할 경우 dependency graph 상의 모든 라이브러리들을
musl
을 사용 하도록 다시 빌드 해야 합니다.
🔑 musl을 사용해서 빌드하기
대부분의 linux는 glibc를 사용합니다. musl을 설치하고 빌드시 musl을 사용해서 빌드하는 것도 가능하지만 이미 OS에 설치 된 라이브러리에 대한 의존성이 있는 경우라면 그 라이브러리도 musl을 사용하도록 다시 빌드해야 합니다.
glibc 대신 musl을 사용하는 OS라면 모든 라이브러리가 musl을 참조하도록 빌드되어 있으므로, 그런 OS에서 빌드를 하는 것이 가장 좋은 방법입니다.
alpine linux가 musl을 사용하는 OS 중 하나입니다.
alpine linux의 docker image에 build-base
패키지등을 설치 한 image를 만들어서 사용하면 됩니다.
FROM golang:1.21-alpine3.18
RUN apk update
RUN apk add alpine-sdk build-base swig cmake unzip bash ninja
RUN apk add openssl-dev bzip2-dev zlib-dev readline-dev sqlite-dev \
llvm ncurses-dev xz xz-dev tk-dev \
libxml2-dev xmlsec-dev libffi-dev
docker image를 만듭니다.
$ docker build -t alpine-builder .
docker run으로 빌드합니다. (build.sh
는 예시 입니다.)
--user
옵션을 미지정시 docker 내부에서 root로 빌드가 진행고 build output 디렉토리들이 root 권한으로 생성되므로--user
옵션을 지정하는 편이 좋습니다.
$ docker run -v .:/app -w /app --user $(id -u):$(id -g) alpine-builder sh build.sh