[Kubernetes] Cluster: Kubespray를 이용해 클러스터 구성하기 - 8. 오프라인 배포: The Hard Way - 6. Private Go Module Proxy
서종호(가시다)님의 On-Premise K8s Hands-on Study 6주차 학습 내용을 기반으로 합니다.
TL;DR
이번 글에서는 폐쇄망 환경에서 Go 모듈 설치를 위한 사설 Go 모듈 프록시(Athens)를 구축한다.
- Athens 서버 구축: admin에서 Athens를 컨테이너로 실행하고, 필요한 Go 모듈 캐싱
- offline 모드 전환: 캐시된 모듈만 서빙하도록 Athens를 offline 모드로 재시작
- Go 클라이언트 설정: k8s-node에서 admin의 Athens를 GOPROXY로 사용
- 정리: 이후 kubespray-offline 실습을 위해 Athens 종료 및 설정 삭제
| 순서 | 위치 | 작업 | 목적 |
|---|---|---|---|
| 1 | admin | Athens 설치 + 온라인 모드 실행 | 사설 Go 모듈 프록시 서버 구축 |
| 2 | admin | 필요한 Go 모듈 캐싱 | 폐쇄망에서 필요한 모듈 사전 다운로드 |
| 3 | admin | offline 모드로 재시작 | 캐시된 모듈만 서빙하도록 전환 |
| 4 | k8s-node | Go 설치 + GOPROXY 설정 | 사설 Go 모듈 프록시 사용 확인 |
| 5 | admin, k8s-node | Athens 종료 + GOPROXY 설정 삭제 | 이후 실습을 위한 정리 |
사설 Go 모듈 프록시의 필요성
이전 글(8.1.5)에서 사설 PyPI 미러를 구축했다. 이번에는 Go 모듈을 폐쇄망에서 설치할 수 있어야 한다.
kubespray-offline이나 내부 Go 애플리케이션을 빌드할 때, Go 모듈을 미리 캐싱해두는 용도로 사용한다. 예를 들어 Helm 차트 관리 도구나 kubectl 플러그인을 직접 빌드하거나, Go 기반 모니터링 도구를 폐쇄망에서 컴파일해야 하는 경우다. 온라인 환경에서는 go get으로 공개 Go 모듈 프록시(proxy.golang.org)에서 바로 설치하면 되지만, 폐쇄망에서는 이 서버에 접근할 수 없다.
[온라인] k8s-node → proxy.golang.org → go get 모듈 다운로드
[폐쇄망] k8s-node → admin (Athens, :3000) → 사설 Go 모듈 프록시에서 다운로드
이전 글(8.1.3)에서 구축한 로컬 YUM/DNF 저장소가 리눅스 OS 패키지(RPM)를 위한 것이고, 이전 글(8.1.5)의 devpi-server가 Python 패키지(wheel/sdist)를 위한 것이라면, 이번에 구축하는 Athens는 Go 모듈(module)을 위한 동일한 역할이다.
| 저장소 | 대상 | 도구 | 프로토콜 |
|---|---|---|---|
| 로컬 YUM/DNF 저장소 | OS 패키지 (.rpm) |
dnf install |
HTTP (nginx) |
| 사설 PyPI 미러 | Python 패키지 (.whl) |
pip install |
HTTP (devpi-server) |
| 사설 Go 모듈 프록시 | Go 모듈 (.mod, .zip) |
go get |
HTTP (Athens) |
배경지식
Go 모듈과 go get
Go 모듈(module)은 Go의 패키지 관리 단위다. Go 1.11부터 도입된 모듈 시스템은 의존성 관리를 go.mod 파일로 선언적으로 처리한다.
go get은 Go의 표준 패키지 관리 명령으로, 기본적으로 VCS(버전 관리 시스템, GitHub, GitLab 등)에서 직접 소스를 가져온다. 이 방식에는 몇 가지 문제가 있다.
- 원본 저장소가 삭제되거나 태그가 변경되면 빌드가 깨짐
- 매번 VCS에 접근하므로 느리고, rate limit에 걸릴 수 있음
- 폐쇄망에서는 외부 접근 자체가 불가능
사용자: go get github.com/gin-gonic/gin
↓
go: GitHub에서 gin-gonic/gin 저장소 clone
→ go.mod 파싱하여 의존성 확인
→ 의존성 모듈도 각 VCS에서 다운로드
→ 설치
Go Module Proxy
Go Module Proxy는 Go 모듈을 캐싱하고 배포하는 중간 서버다. VCS에 직접 접근하는 대신 프록시를 통해 모듈을 가져온다.
go get github.com/gin-gonic/gin
[일반 흐름]
Go 클라이언트 → GitHub (직접 다운로드)
[프록시 사용 시]
Go 클라이언트 → Go Module Proxy → (캐시 없으면) GitHub에서 가져와 캐싱
→ (캐시 있으면) 캐시에서 즉시 응답
Go 클라이언트는 GOPROXY 환경변수로 프록시 서버 주소를 지정한다.
export GOPROXY=http://192.168.10.10:3000
공개 vs 사설 프록시
| 구분 | 예시 | 용도 |
|---|---|---|
| 공개 프록시 | proxy.golang.org (Go 기본값) |
인터넷 환경에서 캐싱/속도 향상 |
| 사설 프록시 | Athens, goproxy 등 자체 운영 | 폐쇄망, 사내 모듈 배포 |
Go의 기본 GOPROXY 값은 https://proxy.golang.org,direct다. proxy.golang.org는 Google이 운영하는 공개 모듈 프록시 서버이고, direct는 프록시에 모듈이 없으면 VCS에 직접 접근하라는 의미다.
Go Module API
Go Module Proxy는 표준 API를 구현한다. 모든 Go 모듈 프록시(Athens, goproxy 등)는 이 API를 따른다.
GET /{module}/@v/list → 모듈의 버전 목록
GET /{module}/@v/{version}.info → 버전 메타데이터 (JSON)
GET /{module}/@v/{version}.mod → go.mod 파일
GET /{module}/@v/{version}.zip → 모듈 소스 zip
GET /{module}/@latest → 최신 버전 정보
go get이 go get github.com/gin-gonic/gin@v1.11.0을 실행하면:
github.com/gin-gonic/gin/@v/list엔드포인트에서 사용 가능한 버전 목록을 받음github.com/gin-gonic/gin/@v/v1.11.0.info에서 버전 메타데이터 확인github.com/gin-gonic/gin/@v/v1.11.0.zip다운로드- 로컬에 설치
클라이언트(go get) 입장에서는 Module API만 제공하면 어떤 서버든 동일하게 동작한다.
# 기본 프록시 사용
go get github.com/gin-gonic/gin
# 사설 프록시 사용 (GOPROXY로 서버 주소만 변경)
export GOPROXY=http://192.168.10.10:3000
go get github.com/gin-gonic/gin
GOPROXY로 서버 주소만 바꾸면, go get은 공개 프록시든 사설 프록시든 동일한 방식으로 모듈을 가져온다.
Go Module Proxy 구현체
Go Module Proxy를 구축하는 도구는 여러 가지가 있다.
| 도구 | 특징 | offline 모드 |
|---|---|---|
| Athens | Go 모듈 전용 캐싱 프록시. offline 모드 지원 | O |
| goproxy | 경량 Go 모듈 프록시. 단순하고 빠름 | O |
| Nexus Repository | 범용 아티팩트 저장소 (Go, Maven, npm, Docker 등 지원) | O |
| JFrog Artifactory | 범용 아티팩트 저장소. Virtual Repository로 체이닝 지원 | O |
이 실습에서는 Athens를 사용한다. Go 모듈 전용으로 가볍고, offline 모드를 네이티브로 지원하여 “인터넷이 될 때 외부 모듈을 캐시해두고, 폐쇄망에서는 캐시된 것만 서빙”하는 구성이 자연스럽기 때문이다.
Athens
Athens는 Go 모듈 캐싱 프록시다. 외부에서 모듈을 가져와 캐싱하고, offline 모드로 설정하면 캐시된 모듈만 서빙할 수 있어서 폐쇄망에 적합하다.
동작 모드
Athens는 두 가지 모드로 동작한다.
[온라인 모드]
go get gin → Athens → (캐시 없음) → GitHub/proxy.golang.org → 캐싱 → 응답
[offline 모드]
go get gin → Athens → (캐시 확인) → 있으면 응답, 없으면 404
| 모드 | 동작 | 용도 |
|---|---|---|
| 온라인 모드 | 캐시에 없는 모듈을 upstream(VCS, proxy.golang.org)에서 가져와 캐싱 | 초기 모듈 캐싱 |
| offline 모드 | 캐시된 모듈만 서빙. 외부 접근 차단 | 폐쇄망 운영 |
환경변수
| 변수 | 값 | 의미 |
|---|---|---|
ATHENS_STORAGE_TYPE |
disk |
캐시 저장소 타입 (disk, memory, s3 등) |
ATHENS_DISK_STORAGE_ROOT |
/var/lib/athens |
디스크 캐시 저장 경로 |
ATHENS_NETWORK_MODE |
offline |
offline 모드 활성화 (외부 VCS 접근 차단) |
ATHENS_DOWNLOAD_MODE |
none |
새 모듈 다운로드 시도 안 함 |
GOPROXY 환경변수
Go 클라이언트가 사설 프록시를 사용하도록 설정하는 방법은 두 가지가 있다.
일회성 사용
export GOPROXY=http://192.168.10.10:3000
export GONOSUMDB=*
go get github.com/gin-gonic/gin
| 변수 | 의미 |
|---|---|
GOPROXY |
모듈을 검색할 프록시 서버 URL |
GONOSUMDB |
checksum database 검증을 건너뛸 모듈 패턴 (*는 모두 건너뛰기) |
전역 설정 (profile)
# /etc/profile.d/go-proxy.sh (시스템 전역)
export GOPROXY=http://192.168.10.10:3000
export GONOSUMDB=*
전역 설정을 하면 이후 모든 go get 명령이 자동으로 사설 프록시를 사용한다. GOPROXY를 매번 지정할 필요가 없다.
GONOSUMDB=*가 필요한 이유는 사설 프록시가 Go의 공식 checksum database(sum.golang.org)와 연동되지 않기 때문이다. 이전 글(8.1.5)의trusted-host설정과 동일한 맥락이다.
실습
| 순서 | 위치 | 작업 | 목적 |
|---|---|---|---|
| 1 | admin | Athens 설치 + 온라인 모드 실행 | 사설 Go 모듈 프록시 서버 구축 |
| 2 | admin | 필요한 Go 모듈 캐싱 | 폐쇄망에서 필요한 모듈 사전 다운로드 |
| 3 | admin | offline 모드로 재시작 | 캐시된 모듈만 서빙하도록 전환 |
| 4 | k8s-node | Go 설치 + GOPROXY 설정 | 사설 Go 모듈 프록시 사용 확인 |
| 5 | admin, k8s-node | Athens 종료 + GOPROXY 설정 삭제 | 이후 실습을 위한 정리 |
1. [admin] Athens 설치 및 기동 (온라인 모드)
Athens는 컨테이너 이미지로 제공되므로, podman을 사용하여 실행한다.
모듈 저장 디렉토리 생성
root@admin:~# mkdir -p /data/athens-storage
Athens는 캐시된 모듈을 디스크에 저장한다. 디렉토리를 미리 생성하지 않으면 컨테이너가 시작 시 에러로 종료된다.
컨테이너 실행
Athens 컨테이너를 온라인 모드로 실행한다. 이 단계에서는 인터넷 접근이 가능해야 하므로, --network host를 사용하여 호스트 네트워크를 직접 사용한다.
root@admin:~# podman run -d \
--name athens \
--network host \
-v /data/athens-storage:/var/lib/athens \
-e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \
-e ATHENS_STORAGE_TYPE=disk \
-e ATHENS_PORT=:3000 \
docker.io/gomods/athens:latest
Trying to pull docker.io/gomods/athens:latest...
Getting image source signatures
Copying blob 964a2c7bdd02 done |
Copying blob 7764deb7d5b3 done |
Copying blob 5af85bede67d done |
Copying blob 32a3f0bdc711 done |
Copying blob d8ad8cd72600 skipped: already exists
Copying blob 780258d8c79e done |
Copying blob 64619a1c732a done |
Copying blob 10304b5b7693 done |
Copying blob 9ea8a5bc68b4 done |
Copying config 70407182aa done |
Writing manifest to image destination
853fe4657f13d5148afa7e9613e0851d765e76a430ae0058bfbc53f66488db2a
| 옵션 | 의미 |
|---|---|
--network host |
호스트 네트워크 사용. DNS 및 외부 접근을 위해 필요 |
-v /data/athens-storage:/var/lib/athens |
호스트 디렉토리를 컨테이너 캐시 경로로 마운트 |
-e ATHENS_STORAGE_TYPE=disk |
캐시 저장소 타입을 디스크로 지정 |
-e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens |
디스크 캐시 저장 경로 |
-e ATHENS_PORT=:3000 |
리스닝 포트 (기본값: 3000) |
--network host를 사용하는 이유는 Athens가 upstream(proxy.golang.org, GitHub 등)에서 모듈을 가져올 때 DNS resolve와 외부 인터넷 접근이 필요하기 때문이다. podman 기본 브릿지 네트워크를 사용하면 DNS가 정상 동작하지 않을 수 있다. 이전 글(8.1.2)에서 구성한 bind DNS 서버의allow-query설정이 podman 브릿지 대역(10.88.0.0/16)을 포함하지 않으면 DNS 질의가 거부된다.
동작 확인
root@admin:~# curl http://192.168.10.10:3000
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"></meta>
<title>Athens</title>
...
<body>
<h1>Welcome to Athens</h1>
<h2>Configuring your client</h2>
<pre>GOPROXY=http://192.168.10.10:3000,direct</pre>
<h2>How to use the Athens API</h2>
<p>Use the <a href="/catalog">catalog</a> endpoint to get a list of all modules in the proxy</p>
...
</body>
</html>
Athens 서버가 정상적으로 실행되었다. 브라우저에서 http://192.168.10.10:3000에 접속하면 Athens 웹 페이지를 확인할 수 있다.

Troubleshooting: /var/lib/athens 디렉토리 없음
호스트에 /data/athens-storage 디렉토리를 생성하지 않고 컨테이너를 실행하면 다음과 같은 에러로 종료된다.
root@admin:~# podman logs athens
INFO[10:57AM]: Exporter not specified. Traces won't be exported
FATAL[10:57AM]: Could not create App error=getting storage configuration: could not create new storage from os fs (root directory `/var/lib/athens` does not exist)
podman 볼륨 마운트는 호스트 디렉토리가 존재해야 정상 동작한다. mkdir -p /data/athens-storage로 디렉토리를 생성한 후 컨테이너를 재시작한다.
root@admin:~# mkdir -p /data/athens-storage
root@admin:~# podman rm athens
root@admin:~# podman run -d \
--name athens \
--network host \
-v /data/athens-storage:/var/lib/athens \
-e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \
-e ATHENS_STORAGE_TYPE=disk \
-e ATHENS_PORT=:3000 \
docker.io/gomods/athens:latest
Troubleshooting: DNS resolve 실패
Athens 컨테이너가 podman 기본 브릿지 네트워크를 사용하면, 컨테이너 내부에서 DNS resolve가 실패할 수 있다.
root@admin:/tmp/go-cache-test# go get github.com/gin-gonic/gin@latest
go: github.com/gin-gonic/gin@latest: module github.com/gin-gonic: reading http://192.168.10.10:3000/github.com/gin-gonic/@v/list: 500 Internal Server Error
Athens 컨테이너 내부에서 DNS 테스트를 해보면 아래와 같은 현상이 목격된다.
root@admin:~# podman exec -it athens nslookup proxy.golang.org
Server: 192.168.10.10
Address: 192.168.10.10:53
** server can't find proxy.golang.org: REFUSED
REFUSED는 DNS 서버(192.168.10.10의 bind)가 쿼리를 거부했다는 뜻이다. 이전 글(8.1.2)의 named.conf 설정을 보면:
allow-query { 127.0.0.1; 192.168.10.0/24; };
allow-recursion { 127.0.0.1; 192.168.10.0/24; };
podman 기본 브릿지 네트워크를 사용하면 컨테이너 IP가 10.88.0.x 대역이므로, 192.168.10.0/24에 해당하지 않아 bind가 DNS 질의를 거부한다.
해결 방법: --network host로 컨테이너를 실행한다. 컨테이너가 호스트 네트워크를 직접 사용하므로 DNS 질의 출발지가 127.0.0.1이 되어 bind가 허용한다.
root@admin:~# podman stop athens && podman rm athens
root@admin:~# podman run -d \
--name athens \
--network host \
-v /data/athens-storage:/var/lib/athens \
-e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \
-e ATHENS_STORAGE_TYPE=disk \
-e ATHENS_PORT=:3000 \
docker.io/gomods/athens:latest
Troubleshooting: DNSSEC 검증 실패
VM이 2일 이상 실행 상태로 유지되면, bind DNS 서버의 DNSSEC 트러스트 앵커가 만료되어 DNS resolve가 실패할 수 있다.
root@admin:~# curl -LO https://go.dev/dl/go1.25.5.linux-arm64.tar.gz
curl: (6) Could not resolve host: go.dev
root@admin:~# dig +short go.dev
;; communications error to 192.168.10.10#53: timed out
bind 로그를 확인하면:
root@admin:~# journalctl -u named -n 20
Feb 10 23:22:45 admin named[7674]: broken trust chain resolving 'go.dev/A/IN': 8.8.8.8#53
Feb 10 23:23:24 admin named[7674]: no valid RRSIG resolving 'go.dev/DS/IN': 168.126.63.1#53
Feb 10 23:23:24 admin named[7674]: validating dev/SOA: got insecure response; parent indicates it should be secure
named.conf에 dnssec-validation yes로 설정되어 있어서, bind가 DNS 응답의 DNSSEC 서명을 검증하려고 하는데, forwarder(168.126.63.1, 8.8.8.8)를 통해 받은 응답의 DNSSEC 트러스트 체인이 깨져있어서 검증에 실패한다.
해결 방법: 실습 환경에서는 dnssec-validation no로 설정하여 DNSSEC 검증을 비활성화한다.
root@admin:~# sed -i 's/dnssec-validation yes/dnssec-validation no/' /etc/named.conf
root@admin:~# systemctl restart named
root@admin:~# dig +short go.dev
216.239.32.21
216.239.34.21
216.239.36.21
216.239.38.21
DNSSEC 트러스트 앵커는 DNSSEC 서명 체인의 최상위 출발점(루트 DNS의 공개키)이다.
dnssec-validation yes는 관리자가 수동으로 트러스트 앵커를 관리해야 하는데, 키가 바뀌면 수동 갱신이 필요하다. 프로덕션에서는dnssec-validation auto를 사용하여 RFC 5011 자동 갱신으로 트러스트 앵커를 알아서 업데이트하는 것이 권장된다.
2. [admin] 필요한 Go 모듈 캐싱
인터넷이 되는 상태에서, 폐쇄망 노드에서 사용할 Go 모듈을 미리 다운로드하여 Athens에 캐싱한다.
Go 설치
root@admin:~# dnf install -y golang
# 실행 결과 (일부)
Last metadata expiration check: 0:04:21 ago on Tue 10 Feb 2026 07:10:33 PM KST.
Dependencies resolved.
==========================================================================================================
Package Architecture Version Repository Size
==========================================================================================================
Installing:
golang aarch64 1.25.5-1.el10_1 appstream 1.2 M
Installing dependencies:
golang-bin aarch64 1.25.5-1.el10_1 appstream 33 M
golang-race aarch64 1.25.5-1.el10_1 appstream 1.6 M
golang-src noarch 1.25.5-1.el10_1 appstream 11 M
...
Complete!
root@admin:~# go version
go version go1.25.5 (Red Hat 1.25.5-1.el10_1) linux/arm64
GOPROXY 설정
admin에서 go get을 실행할 때 Athens를 사용하도록 GOPROXY 환경변수를 설정한다.
root@admin:~# export GOPROXY=http://192.168.10.10:3000
root@admin:~# export GONOSUMDB=*
root@admin:~# export GOINSECURE=*
| 변수 | 의미 |
|---|---|
GOPROXY |
Athens 프록시 주소 |
GONOSUMDB |
checksum database 검증 건너뛰기 |
GOINSECURE |
HTTPS 대신 HTTP 허용 |
테스트 프로젝트 생성 및 모듈 캐싱
root@admin:~# mkdir -p /tmp/go-cache-test && cd /tmp/go-cache-test
root@admin:/tmp/go-cache-test# go mod init test-cache
go: creating new go.mod: module test-cache
root@admin:/tmp/go-cache-test# go get github.com/gin-gonic/gin@latest
go: downloading github.com/gin-gonic/gin v1.11.0
go: downloading github.com/gin-contrib/sse v1.1.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/quic-go/quic-go v0.54.0
go: downloading golang.org/x/net v0.42.0
go: downloading github.com/bytedance/sonic v1.14.0
go: downloading github.com/goccy/go-json v0.10.2
go: downloading github.com/json-iterator/go v1.1.12
...
go: added github.com/gin-gonic/gin v1.11.0
go: added golang.org/x/net v0.42.0
go: added golang.org/x/text v0.27.0
go: added golang.org/x/tools v0.34.0
go: added google.golang.org/protobuf v1.36.9
root@admin:/tmp/go-cache-test# go get google.golang.org/grpc@latest
go: downloading google.golang.org/grpc v1.78.0
...
go: added google.golang.org/genproto/googleapis/rpc v0.0.0-20251029180050-ab9386a59fda
go: added google.golang.org/grpc v1.78.0
go: upgraded google.golang.org/protobuf v1.36.9 => v1.36.10
go get을 실행하면 Athens가 upstream(proxy.golang.org, GitHub)에서 모듈을 가져와 캐싱한다. 의존성 패키지도 자동으로 다운로드된다.
캐시 확인
root@admin:/tmp/go-cache-test# ls -al /data/athens-storage/
total 4
drwxr-xr-x. 7 root root 102 Feb 10 20:05 .
drwxr-xr-x. 6 root root 75 Feb 10 19:12 ..
drwxr-xr-x. 22 root root 4096 Feb 10 20:05 github.com
drwxr-xr-x. 3 root root 15 Feb 10 20:04 golang.org
drwxr-xr-x. 5 root root 50 Feb 10 20:06 google.golang.org
drwxr-xr-x. 4 root root 37 Feb 10 20:05 gopkg.in
drwxr-xr-x. 3 root root 18 Feb 10 20:05 go.uber.org
Athens는 모듈을 {storage-root}/{module-path}/{version}/ 구조로 저장한다. 각 버전 디렉토리에는 .info, .mod, .zip 파일이 포함된다.
root@admin:/tmp/go-cache-test# ls /data/athens-storage/github.com/gin-gonic/gin/
v1.11.0
root@admin:/tmp/go-cache-test# ls /data/athens-storage/github.com/gin-gonic/gin/v1.11.0/
go.mod info source.zip
간단한 애플리케이션 테스트
실제로 모듈이 정상적으로 동작하는지 테스트해본다.
root@admin:/tmp/go-cache-test# cat << 'EOF' > main.go
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
r.Run(":8080")
}
EOF
root@admin:/tmp/go-cache-test# go mod tidy
go: downloading github.com/stretchr/testify v1.11.1
go: downloading github.com/google/go-cmp v0.7.0
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading github.com/go-playground/assert/v2 v2.2.0
go: downloading github.com/pmezard/go-difflib v1.0.0
go: downloading gopkg.in/yaml.v3 v3.0.1
root@admin:/tmp/go-cache-test# go run main.go &
[1] 12345
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
[GIN-debug] GET /ping --> main.main.func1 (3 handlers)
[GIN-debug] GET /health --> main.main.func2 (3 handlers)
[GIN-debug] Listening and serving HTTP on :8080
root@admin:/tmp/go-cache-test# curl localhost:8080/ping
{"message":"pong"}
root@admin:/tmp/go-cache-test# curl localhost:8080/health
{"status":"ok"}
root@admin:/tmp/go-cache-test# kill %1
Athens 프록시를 통해 다운로드한 모듈이 정상적으로 동작한다.
3. [admin] offline 모드로 전환
모듈 캐싱이 완료되면, Athens를 offline 모드로 재시작한다. offline 모드에서는 외부 VCS에 접근하지 않고 캐시된 모듈만 서빙한다.
컨테이너 재시작
root@admin:~# podman stop athens && podman rm athens
athens
athens
root@admin:~# podman run -d \
--name athens \
-p 3000:3000 \
-v /data/athens-storage:/var/lib/athens \
-e ATHENS_DISK_STORAGE_ROOT=/var/lib/athens \
-e ATHENS_STORAGE_TYPE=disk \
-e ATHENS_NETWORK_MODE=offline \
-e ATHENS_DOWNLOAD_MODE=none \
--restart always \
docker.io/gomods/athens:latest
b18e258657648451e85b32454d1e5c80ff2b93a61a85223cbff17db9724caf76
| 변수 | 값 | 의미 |
|---|---|---|
ATHENS_NETWORK_MODE |
offline |
offline 모드 활성화 (외부 VCS 접근 차단) |
ATHENS_DOWNLOAD_MODE |
none |
새 모듈 다운로드 시도 안 함 |
--restart always |
- | 컨테이너 재시작 정책 (항상 재시작) |
offline 모드에서는
--network host가 필요 없다. 외부 인터넷 접근이 불필요하므로 기본 브릿지 네트워크를 사용해도 된다.
동작 확인
root@admin:~# podman ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b18e25865764 docker.io/gomods/athens:latest athens-proxy -con... 5 seconds ago Up 4 seconds 0.0.0.0:3000->3000/tcp athens
root@admin:~# curl http://192.168.10.10:3000
<!DOCTYPE html>
<html>
...
<h1>Welcome to Athens</h1>
...
</html>
4. [k8s-node] Go 설치 및 GOPROXY 설정
k8s-node에서 admin의 Athens를 GOPROXY로 사용하도록 설정한다.
Go 설치
k8s-node는 폐쇄망이므로 yum/dnf 저장소를 사용할 수 없다. admin에서 Go 공식 tarball을 다운로드한 후 scp로 복사하여 수동 설치한다.
# [admin] Go tarball 다운로드 및 복사
root@admin:~# curl -LO https://go.dev/dl/go1.25.5.linux-arm64.tar.gz
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 75 100 75 0 0 180 0 --:--:-- --:--:-- --:--:-- 179
100 54.6M 100 54.6M 0 0 19.0M 0 0:00:02 0:00:02 --:--:-- 27.5M
root@admin:~# scp go1.25.5.linux-arm64.tar.gz root@192.168.10.11:/tmp/
go1.25.5.linux-arm64.tar.gz 100% 55MB 141.7MB/s 00:00
# [k8s-node1] Go 설치
root@week06-week06-k8s-node1:~# rm -rf /usr/local/go
root@week06-week06-k8s-node1:~# tar -C /usr/local -xzf /tmp/go1.25.5.linux-arm64.tar.gz
root@week06-week06-k8s-node1:~# echo 'export PATH=$PATH:/usr/local/go/bin' >> /etc/profile
root@week06-week06-k8s-node1:~# source /etc/profile
root@week06-week06-k8s-node1:~# go version
go version go1.25.5 linux/arm64
GOPROXY 전역 설정
root@week06-week06-k8s-node1:~# cat << 'EOF' >> /etc/profile.d/go-proxy.sh
export GOPROXY=http://192.168.10.10:3000
export GONOSUMDB=*
EOF
root@week06-week06-k8s-node1:~# source /etc/profile.d/go-proxy.sh
root@week06-week06-k8s-node1:~# go env GOPROXY
http://192.168.10.10:3000
| 설정 | 의미 |
|---|---|
GOPROXY |
Athens 프록시 주소 |
GONOSUMDB |
checksum database 검증 건너뛰기 |
캐시된 모듈 설치 테스트
Athens에 캐싱된 모듈(github.com/gin-gonic/gin)을 설치해 본다.
root@week06-week06-k8s-node1:~# mkdir -p /tmp/test-proxy && cd /tmp/test-proxy
root@week06-week06-k8s-node1:/tmp/test-proxy# go mod init test-proxy
go: creating new go.mod: module test-proxy
root@week06-week06-k8s-node1:/tmp/test-proxy# go get github.com/gin-gonic/gin
go: downloading github.com/gin-gonic/gin v1.11.0
go: downloading github.com/gin-contrib/sse v1.1.0
go: downloading golang.org/x/net v0.42.0
go: downloading github.com/quic-go/quic-go v0.54.0
go: downloading github.com/mattn/go-isatty v0.0.20
go: downloading github.com/go-playground/validator/v10 v10.27.0
go: downloading github.com/goccy/go-yaml v1.18.0
go: downloading github.com/pelletier/go-toml/v2 v2.2.4
go: downloading github.com/ugorji/go/codec v1.3.0
go: downloading google.golang.org/protobuf v1.36.9
go: downloading github.com/bytedance/sonic v1.14.0
go: downloading github.com/goccy/go-json v0.10.2
go: downloading github.com/json-iterator/go v1.1.12
go: downloading golang.org/x/sys v0.35.0
...
go: added github.com/gin-gonic/gin v1.11.0
go: added golang.org/x/net v0.42.0
go: added golang.org/x/text v0.27.0
go: added golang.org/x/tools v0.34.0
go: added google.golang.org/protobuf v1.36.9
모든 모듈이 admin의 Athens 프록시(http://192.168.10.10:3000)에서 다운로드되었다. 의존성 패키지도 함께 해결되었다.
Troubleshooting: 캐시에 없는 모듈 설치 시 에러
Athens에 캐싱되지 않은 모듈을 설치하면 에러가 발생한다.
root@week06-week06-k8s-node1:/tmp/test-proxy# go get github.com/aws/aws-sdk-go-v2/aws
go: github.com/aws/aws-sdk-go-v2/aws: no matching versions for query "upgrade"
go get의 기본 쿼리는 "upgrade" (최신 버전으로 업그레이드)다. Athens가 모듈 자체를 찾을 수 없으면 빈 버전 목록을 반환하므로, Go 도구 체인은 “매칭되는 버전이 없다”고 해석한다.
Athens 로그를 확인하면:
root@admin:~# podman logs athens | grep aws
INFO[2:37PM]: incoming request http-method=GET http-path=/github.com/aws/@v/list http-status=200 request-id=5f0b34a0-a8e9-454a-bbfb-71756f82412f
INFO[2:37PM]: Athens is in offline mode, use /list endpoint http-method=GET http-path=/github.com/aws/@latest kind=Not Found module= operation=download.LatestHandler ...
INFO[2:37PM]: incoming request http-method=GET http-path=/github.com/aws/@latest http-status=404 request-id=3f4a51c6-8967-41bc-940c-754bdca31045
Athens가 offline 모드이므로 캐시에 없는 모듈은 404를 반환한다. 이는 정상 동작이다.
5. 정리
이후 kubespray-offline 실습에서 다른 방식으로 Go 모듈 프록시를 구성할 예정이므로, 현재 구성을 정리한다.
admin
# Athens 컨테이너 종료
root@admin:~# podman stop athens && podman rm athens
athens
athens
k8s-node
# GOPROXY 전역 설정 삭제
root@week06-week06-k8s-node1:~# rm -f /etc/profile.d/go-proxy.sh
정리
이번 글에서는 폐쇄망 환경에서 Go 모듈을 설치할 수 있도록 사설 Go 모듈 프록시(Athens)를 구축했다.
| 순서 | 위치 | 작업 | 도구 |
|---|---|---|---|
| 1 | admin | Athens 설치 + 온라인 모드 실행 | podman run, --network host |
| 2 | admin | 필요한 Go 모듈 캐싱 | go get github.com/gin-gonic/gin |
| 3 | admin | offline 모드로 재시작 | ATHENS_NETWORK_MODE=offline |
| 4 | k8s-node | Go 설치 + GOPROXY 설정 | /etc/profile.d/go-proxy.sh, GOPROXY |
Athens의 offline 모드 덕분에, 온라인 환경에서 캐싱한 모듈을 폐쇄망에서 사용할 수 있었다.
현재까지의 폐쇄망 인프라 구성 상태:
| 구성요소 | 상태 |
|---|---|
| Network Gateway | 완료 (8.1.1) |
| NTP Server / Client | 완료 (8.1.2) |
| DNS Server / Client | 완료 (8.1.2) |
| Local Package Repository | 완료 (8.1.3) |
| Private Container Registry | 완료 (8.1.4) |
| Private PyPI Mirror | 완료 (8.1.5) |
| Private Go Module Proxy | 완료 (본 글) |
이것으로 폐쇄망 기반 인프라의 주요 구성요소 구축이 완료되었다. 각 구성요소를 수동으로 하나씩 구축하며 원리를 익혔으므로, 이후 kubespray-offline이 이 과정을 어떻게 자동화하는지 이해하기 수월할 것이다.
참고 자료
- Athens GitHub Repository
- Athens Documentation
- Go Modules Reference
- Go Module Proxy Protocol
- GOPROXY Environment Variable
댓글남기기