Go 언어별 가이드
Go 언어별 가이드
이 가이드는 Docker를 사용해 Go 어플리케이션을 컨테이너화하는 방법을 알려줘요.
출처: 문서
본문
이 가이드는 Docker를 사용해 컨테이너화된 Go 어플리케이션을 만들고, 테스트하고, 배포하는 방법을 보여줘요.
감사의 말: Docker는 이 가이드에 기여한 Oliver Frolovs 님께 감사를 전해요.
무엇을 배울까요?
이 가이드에서 배울 내용:
- Go로 작성된 프로그램용 컨테이너 이미지를 빌드하는 지침을 담는 Dockerfile 만들기
- 이미지를 로컬 Docker 인스턴스에서 컨테이너로 실행하고 컨테이너 수명주기 관리하기
- Dockerfile을 읽고 유지 관리하기 쉽게 유지하면서 효율적으로 작은 이미지를 빌드하기 위해 멀티스테이지 빌드 사용하기
- Docker Compose를 사용해 개발 환경에서 여러 관련 컨테이너가 함께 실행되도록 오케스트레이션하기
사전 요구사항
Go와 그 툴체인에 대한 기본적인 이해가 있다고 가정해요. 이건 Go 튜토리얼이 아니에요. 어느 언어든 처음이라면 Go 웹사이트 가 탐구하기 좋은 곳이니 go(말장난 의도) 확인해보세요!
또한 몇 가지 기본적인 Docker 개념 과 Dockerfile 형식 에 대해 최소한 어렴풋이 익숙해야 해요.
내 Docker 설정에는 BuildKit이 활성화되어 있어야 해요. BuildKit은 Docker Desktop 의 모든 사용자에게 기본으로 활성화돼 있어요. Docker Desktop을 설치했다면 BuildKit을 수동으로 활성화할 필요가 없어요. Linux에서 Docker를 실행한다면 BuildKit 시작하기 페이지를 확인해주세요.
명령줄에 대한 약간의 익숙함도 기대돼요.
다음 단계
이 가이드의 목표는 나만의 Go 어플리케이션을 컨테이너화하고 클라우드에 배포할 수 있도록 충분한 예제와 지침을 제공하는 거예요. 첫 Go 이미지를 빌드하는 것부터 시작해요.
Go 이미지 빌드
개요
이 섹션에서는 컨테이너 이미지를 빌드할 거예요. 이미지에는 어플리케이션 실행에 필요한 모든 것(컴파일된 어플리케이션 바이너리 파일, 런타임, 라이브러리, 그리고 어플리케이션이 필요로 하는 모든 기타 리소스)이 포함돼요.
필요한 소프트웨어
이 튜토리얼을 완료하려면 다음이 필요해요:
- 로컬에서 실행되는 Docker. Docker 다운로드·설치 지침 을 따르세요.
- 파일을 편집할 IDE 또는 텍스트 편집기. Visual Studio Code 는 무료이며 인기 있는 선택이지만 편안한 아무거나 사용할 수 있어요.
- Git 클라이언트. 이 가이드는 명령줄 기반 git 클라이언트를 사용하지만 내게 맞는 아무거나 사용할 수 있어요.
- 명령줄 터미널 어플리케이션. 이 모듈의 예제는 Linux 셸에서 가져온 것이지만, 최소한의(있더라도) 수정으로 PowerShell, Windows Command Prompt, OS X Terminal에서도 작동해야 해요.
예제 어플리케이션 만나보기
예제 어플리케이션은 마이크로서비스의 풍자적 묘사예요. Go 어플리케이션 컨테이너화의 기초를 배우는 데 초점을 맞추기 위해 의도적으로 사소하게 만들었어요.
이 어플리케이션은 두 개의 HTTP 엔드포인트를 제공해요:
/로의 요청에 하트 기호(<3)를 담은 문자열로 응답해요./health로의 요청에{"Status" : "OK"}JSON으로 응답해요.
다른 요청에는 HTTP 오류 404로 응답해요.
어플리케이션은 PORT 환경 변수 값으로 정의된 TCP 포트에서 수신 대기해요. 기본값은 8080 이에요.
어플리케이션은 상태가 없어요(stateless).
어플리케이션의 전체 소스 코드는 GitHub: github.com/docker/docker-gs-ping 에 있어요. 포크해서 마음껏 실험해보는 것을 권장해요.
계속하려면 어플리케이션 리포지토리를 로컬 머신에 클론해주세요:
$ git clone https://github.com/docker/docker-gs-ping
Go에 익숙하다면 어플리케이션의 main.go 파일은 간단해요:
package main
import (
"net/http"
"os"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.GET("/", func(c echo.Context) error {
return c.HTML(http.StatusOK, "Hello, Docker! <3")
})
e.GET("/health", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
httpPort := os.Getenv("PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
// Simple implementation of an integer minimum
// Adapted from: https://gobyexample.com/testing-and-benchmarking
func IntMin(a, b int) int {
if a < b {
return a
}
return b
}
어플리케이션용 Dockerfile 만들기
Docker로 컨테이너 이미지를 빌드하려면 빌드 지침이 있는 Dockerfile 이 필요해요.
BuildKit이 지정된 버전의 문법 규칙에 따라 파일을 해석하도록 지시하는 (선택) 파서 지시문 줄로 Dockerfile 을 시작해요.
그런 다음 Docker에게 어플리케이션에 사용할 베이스 이미지를 알려줘요:
# syntax=docker/dockerfile:1
FROM golang:1.19
Docker 이미지는 다른 이미지에서 상속받을 수 있어요. 따라서 베이스 이미지를 처음부터 만들지 않고, Go 어플리케이션을 컴파일·실행하는 데 필요한 모든 도구와 라이브러리가 이미 있는 공식 Go 이미지를 사용할 수 있어요.
참고: 나만의 베이스 이미지를 만드는 것에 궁금하다면 이 가이드의 다음 섹션(베이스 이미지 만들기 )을 확인할 수 있어요. 하지만 지금 당장 해야 할 작업을 계속하는 데는 필요하지 않다는 점을 명심하세요.
이제 다가올 컨테이너 이미지의 베이스 이미지를 정의했으니 그 위에 빌드하기 시작할 수 있어요.
나머지 명령을 실행하기 쉽게 만들기 위해, 빌드 중인 이미지 안에 디렉터리를 만드세요. 이것은 또한 Docker가 모든 후속 명령의 기본 대상으로 이 디렉터리를 사용하도록 지시해요. 이렇게 하면 Dockerfile 에 전체 파일 경로를 입력할 필요 없이 상대 경로가 이 디렉터리를 기반으로 해요.
WORKDIR /app
보통 Go로 작성된 프로젝트를 다운로드한 후 가장 먼저 하는 일은 그것을 컴파일하는 데 필요한 모듈을 설치하는 거예요. 베이스 이미지에는 툴체인이 이미 있지만 내 소스 코드는 아직 없다는 점을 주목하세요.
따라서 이미지 안에서 go mod download 를 실행하기 전에 go.mod 와 go.sum 파일을 이미지로 복사해야 해요. 이를 위해 COPY 명령을 사용해요.
가장 간단한 형태에서 COPY 명령은 두 개의 매개변수를 취해요. 첫 번째 매개변수는 Docker에게 이미지로 복사할 파일을 알려주고, 마지막 매개변수는 그 파일을 복사할 위치를 Docker에게 알려줘요.
go.mod 와 go.sum 파일을 프로젝트 디렉터리 /app 으로 복사하세요. WORKDIR 을 사용했기 때문에 이것이 이미지 안의 현재 디렉터리( ./ )예요. 대부분의 경우 사용자가 무슨 뜻인지 알아낼 수 있는(대부분) 후행 슬래시( / ) 사용에 무관심해 보이는 일부 현대 셸과 달리, Docker의 COPY 명령은 후행 슬래시 해석에 상당히 민감해요.
COPY go.mod go.sum ./
참고: COPY 명령의 후행 슬래시 처리에 익숙해지고 싶다면 Dockerfile 참조 를 참고하세요. 이 후행 슬래시는 상상 이상으로 많은 방식에서 문제를 일으킬 수 있어요.
이제 빌드 중인 Docker 이미지 안에 모듈 파일이 있으니 RUN 명령을 사용해 거기서도 go mod download 를 실행할 수 있어요. 이것은 내 머신에서 go 를 로컬로 실행하는 것과 정확히 동일하게 작동하지만, 이번에는 이 Go 모듈들이 이미지 안의 디렉터리에 설치될 거예요.
RUN go mod download
이 시점에서 이미지 안에 Go 툴체인 1.19.x 버전과 모든 Go 의존성이 설치되어 있어요.
다음으로 할 일은 소스 코드를 이미지로 복사하는 거예요. 이전에 모듈 파일에서 했던 것처럼 COPY 명령을 사용할 거예요.
COPY *.go ./
이 COPY 명령은 와일드카드를 사용해 호스트의 현재 디렉터리(Dockerfile 이 있는 디렉터리)에 있는 .go 확장자를 가진 모든 파일을 이미지 안의 현재 디렉터리로 복사해요.
COPY *.go ./ 는 그 한 디렉터리의 .go 파일만 일치시켜요. pkg/ 나 다른 하위 디렉터리 아래의 패키지는 제외되므로 go build 가 그것들을 찾지 못해 실패해요. 다중 패키지 모듈의 경우 대신 모듈 루트( COPY . . )를 복사하고 .dockerignore 파일을 사용해 컨텍스트를 작게 유지하세요.
이제 어플리케이션을 컴파일하려면 익숙한 RUN 명령을 사용해요:
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
이것은 익숙할 거예요. 이 명령의 결과는 docker-gs-ping 이라는 정적 어플리케이션 바이너리가 되고, 빌드 중인 이미지의 파일시스템 루트에 위치해요. 바이너리를 그 이미지 안의 원하는 다른 곳에 둘 수도 있었어요. 이와 관련해 루트 디렉터리는 특별한 의미가 없어요. 가독성을 위해 파일 경로를 짧게 유지하는 데 사용하는 것이 편리해요.
이제 남은 것은 이미지가 컨테이너를 시작하는 데 사용될 때 실행할 명령을 Docker에게 알려주는 거예요.
이것은 CMD 명령으로 해요:
CMD ["/docker-gs-ping"]
완전한 Dockerfile :
# syntax=docker/dockerfile:1
FROM golang:1.19
# Set destination for COPY
WORKDIR /app
# Download Go modules
COPY go.mod go.sum ./
RUN go mod download
# Copy the source code. Note the slash at the end, as explained in
# https://docs.docker.com/reference/dockerfile/#copy
COPY *.go ./
# Build
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
# Optional:
# To bind to a TCP port, runtime parameters must be supplied to the docker command.
# But we can document in the Dockerfile what ports
# the application is going to listen on by default.
# https://docs.docker.com/reference/dockerfile/#expose
EXPOSE 8080
# Run
CMD ["/docker-gs-ping"]
Dockerfile 은 주석도 포함할 수 있어요. 주석은 항상 # 기호로 시작하며 줄의 시작에 있어야 해요. 주석은 내 Dockerfile 을 문서화할 수 있게 해주는 편의를 위한 거예요.
Dockerfile 지시문이라는 개념도 있는데, 추가한 syntax 지시문 같은 것이에요. 지시문은 항상 Dockerfile 의 맨 위에 있어야 하므로, 주석을 추가할 때는 사용한 지시문 뒤에 주석이 오도록 하세요:
# syntax=docker/dockerfile:1
# A sample microservice in Go packaged into a container image.
FROM golang:1.19
# ...
이미지 빌드
이제 Dockerfile 을 만들었으니 그로부터 이미지를 빌드해요. docker build 명령은 Dockerfile 과 컨텍스트에서 Docker 이미지를 만들어요. 빌드 컨텍스트는 지정된 경로나 URL에 있는 파일들의 집합이에요. Docker 빌드 과정은 컨텍스트에 있는 어떤 파일이든 접근할 수 있어요.
빌드 명령은 선택적으로 --tag 플래그를 취해요. 이 플래그는 사람이 읽고 인식하기 쉬운 문자열 값으로 이미지에 라벨을 붙이는 데 사용돼요. --tag 를 전달하지 않으면 Docker는 latest 를 기본값으로 사용해요.
첫 Docker 이미지를 빌드해주세요.
$ docker build --tag docker-gs-ping .
빌드 과정은 빌드 단계를 거치면서 몇 가지 진단 메시지를 출력해요. 다음은 이러한 메시지가 어떻게 보일 수 있는지에 대한 예시예요.
[+] Building 2.2s (15/15) FINISHED
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 701B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1 1.1s
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4d6a7782a409b14 0.0s
=> [internal] load build definition from Dockerfile 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load metadata for docker.io/library/golang:1.19 0.7s
=> [1/6] FROM docker.io/library/golang:1.19@sha256:5d947843dde82ba1df5ac1b2ebb70b203d106f0423bf5183df3dc96f6bc5a705 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 6.08kB 0.0s
=> CACHED [2/6] WORKDIR /app 0.0s
=> CACHED [3/6] COPY go.mod go.sum ./ 0.0s
=> CACHED [4/6] RUN go mod download 0.0s
=> CACHED [5/6] COPY *.go ./ 0.0s
=> CACHED [6/6] RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping 0.0s
=> exporting to image 0.0s
=> => exporting layers 0.0s
=> => writing image sha256:ede8ff889a0d9bc33f7a8da0673763c887a258eb53837dd52445cdca7b7df7e3 0.0s
=> => naming to docker.io/library/docker-gs-ping 0.0s
내 정확한 출력은 다를 수 있지만 오류가 없다면 첫 출력 줄에서 FINISHED 라는 단어가 보여야 해요. 이는 Docker가 docker-gs-ping 이라는 이미지를 성공적으로 빌드했음을 의미해요.
로컬 이미지 보기
로컬 머신에 있는 이미지 목록을 보려면 두 가지 옵션이 있어요. 하나는 CLI를 사용하는 것이고 다른 하나는 Docker Desktop 을 사용하는 것이에요. 터미널에서 작업하고 있으니 CLI로 이미지를 나열하는 것을 살펴볼게요.
이미지를 나열하려면 docker image ls 명령(또는 docker images 축약형)을 실행해주세요:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping latest 7f153fbcc0a8 2 minutes ago 1.11GB
...
내 정확한 출력은 다를 수 있지만 docker-gs-ping 이미지가 latest 태그와 함께 보여야 해요. 이미지를 빌드할 때 커스텀 태그를 지정하지 않았으므로 Docker는 태그가 latest 일 것이라고 가정했어요. 이는 특별한 값이에요.
이미지 태그 지정
이미지 이름은 슬래시로 구분된 이름 구성 요소로 이루어져 있어요. 이름 구성 요소는 소문자, 숫자, 구분자를 포함할 수 있어요. 구분자는 마침표, 밑줄 한두 개 또는 대시 한 개 이상으로 정의돼요. 이름 구성 요소는 구분자로 시작하거나 끝날 수 없어요.
이미지는 매니페스트와 레이어 목록으로 구성돼요. 간단히 말해 태그는 이러한 아티팩트의 조합을 가리켜요. 이미지에 여러 태그를 가질 수 있고, 실제로 대부분의 이미지는 여러 태그를 가져요. 빌드한 이미지에 두 번째 태그를 만들고 그 레이어를 살펴봐요.
docker image tag (또는 docker tag 축약형) 명령을 사용해 이미지에 새 태그를 만들어요. 이 명령은 두 개의 인자를 취하며, 첫 번째 인자는 소스 이미지이고 두 번째는 만들 새 태그예요. 다음 명령은 빌드한 docker-gs-ping:latest 용 docker-gs-ping:v1.0 새 태그를 만들어요:
$ docker image tag docker-gs-ping:latest docker-gs-ping:v1.0
Docker tag 명령은 이미지에 새 태그를 만들어요. 새 이미지를 만들지는 않아요. 태그는 같은 이미지를 가리키며 이미지를 참조하는 또 다른 방법이에요.
이제 docker image ls 명령을 다시 실행해 업데이트된 로컬 이미지 목록을 확인해주세요:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping latest 7f153fbcc0a8 6 minutes ago 1.11GB
docker-gs-ping v1.0 7f153fbcc0a8 6 minutes ago 1.11GB
...
docker-gs-ping 으로 시작하는 두 이미지가 보여요. IMAGE ID 열을 보면 두 이미지의 값이 같다는 것을 알 수 있어서 같은 이미지라는 걸 알 수 있어요. 이 값은 Docker가 이미지를 내부적으로 식별하는 데 사용하는 고유 식별자예요.
방금 만든 태그를 제거해주세요. 이를 위해 docker image rm 명령 또는 축약형 docker rmi ( "remove image" 의 줄임말)을 사용해요:
$ docker image rm docker-gs-ping:v1.0
Untagged: docker-gs-ping:v1.0
Docker의 응답이 이미지가 제거된 것이 아니라 태그만 해제(unttag)됐다는 것을 알려주는 것에 주목하세요.
다음 명령으로 이를 확인해주세요:
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping latest 7f153fbcc0a8 7 minutes ago 1.11GB
...
v1.0 태그가 Docker 인스턴스가 유지하는 이미지 목록에 더 이상 없어요. 태그는 제거됐지만 머신에 docker-gs-ping:latest 태그가 아직 있으므로 이미지는 존재해요.
멀티스테이지 빌드
docker-gs-ping 이미지가 1GB가 넘는다는 것을 알아차렸을 거예요. 작은 컴파일된 Go 어플리케이션치고는 많은 양이에요. 또한 이미지를 빌드한 후 컴파일러를 포함한 전체 Go 도구 모음이 어떻게 됐는지 궁금할 수도 있어요.
답은 전체 툴체인이 여전히 컨테이너 이미지 안에 있다는 거예요. 이는 파일 크기가 커서 불편할 뿐 아니라, 컨테이너가 배포될 때 보안 위험을 제시할 수도 있어요.
이 두 문제는 멀티스테이지 빌드 를 사용해 해결할 수 있어요. 요약하자면 멀티스테이지 빌드 는 한 빌드 스테이지의 아티팩트를 다른 스테이지로 옮길 수 있고, 각 빌드 스테이지는 서로 다른 베이스 이미지에서 인스턴스화될 수 있어요.
따라서 다음 예제에서는 전체 규모의 공식 Go 이미지를 사용해 어플리케이션을 빌드할 거예요. 그런 다음 어플리케이션 바이너리를, 베이스가 매우 날렵하고 Go 툴체인이나 다른 선택적 구성 요소를 포함하지 않는 다른 이미지로 복사할 거예요.
샘플 어플리케이션 리포지토리의 Dockerfile.multistage 는 다음 내용을 가져요:
# syntax=docker/dockerfile:1
# Build the application from source
FROM golang:1.19 AS build-stage
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY *.go ./
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
# Run the tests in the container
FROM build-stage AS run-test-stage
RUN go test -v ./...
# Deploy the application binary into a lean image
FROM gcr.io/distroless/base-debian12 AS build-release-stage
WORKDIR /
COPY --from=build-stage /docker-gs-ping /docker-gs-ping
EXPOSE 8080
USER nonroot:nonroot
ENTRYPOINT ["/docker-gs-ping"]
이제 Dockerfile이 두 개 있으므로 이미지를 빌드하는 데 어떤 Dockerfile을 사용할지 Docker에게 알려야 해요. 새 이미지에 multistage 태그를 붙여주세요. 이 태그(latest 를 제외한 다른 것과 마찬가지로)는 Docker에게 특별한 의미가 없어요. 내가 선택한 것이에요.
$ docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .
docker-gs-ping:multistage 와 docker-gs-ping:latest 의 크기를 비교하면 몇 자릿수 차이가 있음을 알 수 있어요.
$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
docker-gs-ping multistage e3fdde09f172 About a minute ago 28.1MB
docker-gs-ping latest 336a3f164d0f About an hour ago 1.11GB
빌드의 두 번째 스테이지에서 사용한 "distroless" 베이스 이미지는 매우 최소한이며 정적 바이너리의 날렵한 배포를 위해 설계되었기 때문이에요.
멀티 아키텍처 빌드의 가능성 등 멀티스테이지 빌드 에는 더 많은 것이 있으니 자유롭게 확인해보세요. 하지만 여기서 진행하는 데는 필수적이지 않아요.
Go 이미지를 컨테이너로 실행
사전 요구사항
Go 이미지 빌드 하기 에서 Go 어플리케이션을 컨테이너화하는 단계를 작업하세요.
개요
이전 모듈에서 예제 어플리케이션용 Dockerfile 을 만든 다음 docker build 명령으로 Docker 이미지를 만들었어요. 이제 이미지가 있으니 그 이미지를 실행해 어플리케이션이 올바르게 실행되는지 볼 수 있어요.
컨테이너는 호스트와 분리된 자체 파일시스템, 자체 네트워킹, 자체 고립된 프로세스 트리를 가진다는 점을 제외하면 정상적인 운영 체제 프로세스예요.
컨테이너 안에서 이미지를 실행하려면 docker run 명령을 사용해요. 하나의 매개변수(이미지 이름)를 요구해요. 내 이미지를 시작하고 올바르게 실행되는지 확인해주세요. 터미널에서 다음 명령을 실행해주세요.
$ docker run docker-gs-ping
____ __
/ __/ ___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.10.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:8080
이 명령을 실행하면 명령 프롬프트로 돌아오지 않았다는 것을 알 수 있을 거예요. 이는 내 어플리케이션이 REST 서버이고 들어오는 요청을 기다리며 루프에서 실행되어, 컨테이너를 중지할 때까지 OS에 제어를 돌려주지 않기 때문이에요.
curl 명령을 사용해 서버에 GET 요청을 만들어주세요.
$ curl http://localhost:8080/
curl: (7) Failed to connect to localhost port 8080: Connection refused
내 curl 명령은 서버에 대한 연결이 거부되었기 때문에 실패했어요. 즉 localhost의 8080 포트에 연결할 수 없었다는 의미예요. 이것은 네트워킹을 포함한 격리 상태에서 컨테이너가 실행 중이기 때문에 예상된 결과예요. 컨테이너를 중지하고 내 로컬 네트워크에 8080 포트를 게시해 다시 시작해주세요.
컨테이너를 중지하려면 ctrl-c 를 누르세요. 그러면 터미널 프롬프트로 돌아와요.
컨테이너에 포트를 게시하려면 docker run 명령에서 --publish 플래그( 줄여서 -p )를 사용할 거예요. --publish 명령의 형식은 [host_port]:[container_port] 이에요. 따라서 컨테이너 안의 8080 포트를 컨테이너 밖의 3000 포트에 노출하려면 --publish 플래그에 3000:8080 을 전달하면 돼요.
컨테이너를 시작하고 호스트의 8080 포트에 8080 포트를 노출해주세요.
$ docker run --publish 8080:8080 docker-gs-ping
이제 curl 명령을 다시 실행해주세요.
$ curl http://localhost:8080/
Hello, Docker! <3
성공! 컨테이너 안에서 실행 중인 어플리케이션에 8080 포트로 연결할 수 있었어요. 컨테이너가 실행 중인 터미널로 다시 전환하면 GET 요청이 콘솔에 로깅되는 것을 볼 수 있어요.
ctrl-c 를 눌러 컨테이너를 중지해주세요.
분리(Detached) 모드로 실행
지금까지 훌륭하지만 샘플 어플리케이션은 웹 서버이며, 컨테이너에 터미널을 연결해 둘 필요가 없어야 해요. Docker는 백그라운드에서 분리된 모드로 컨테이너를 실행할 수 있어요. 이를 위해 --detach 또는 줄여서 -d 를 사용할 수 있어요. Docker는 이전과 동일하게 컨테이너를 시작하지만 이번에는 컨테이너에서 분리되어 터미널 프롬프트로 돌아와요.
$ docker run -d -p 8080:8080 docker-gs-ping
d75e61fcad1e0c0eca69a3f767be6ba28a66625ce4dc42201a8a323e8313c14e
Docker는 백그라운드에서 컨테이너를 시작하고 터미널에 컨테이너 ID를 출력했어요.
다시 한번 컨테이너가 실행 중인지 확인해주세요. 같은 curl 명령을 실행해요:
$ curl http://localhost:8080/
Hello, Docker! <3
컨테이너 나열
컨테이너를 백그라운드에서 실행했으니 컨테이너가 실행 중인지, 아니면 머신에서 다른 어떤 컨테이너가 실행 중인지 어떻게 알 수 있을까요? 머신에서 실행 중인 컨테이너 목록을 보려면 docker ps 를 실행해요. 이것은 Linux 머신에서 프로세스 목록을 보는 데 ps 명령을 사용하는 것과 유사해요.
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d75e61fcad1e docker-gs-ping "/docker-gs-ping" 41 seconds ago Up 40 seconds 0.0.0.0:8080->8080/tcp inspiring_ishizaka
ps 명령은 실행 중인 컨테이너에 대해 많은 것을 알려줘요. 컨테이너 ID, 컨테이너 안에서 실행 중인 이미지, 컨테이너를 시작하는 데 사용된 명령, 생성 시기, 상태, 노출된 포트, 컨테이너 이름을 볼 수 있어요.
컨테이너의 이름이 어디서 왔는지 궁금할 거예요. 시작할 때 컨테이너에 이름을 제공하지 않았으므로 Docker가 임의의 이름을 생성했어요. 잠시 후에 이 문제를 해결하겠지만 먼저 컨테이너를 중지해야 해요. 컨테이너를 중지하려면 docker stop 명령을 실행하고 컨테이너의 이름이나 ID를 전달하세요.
$ docker stop inspiring_ishizaka
컨테이너 중지, 시작, 이름 지정
Docker 컨테이너는 시작·중지·재시작될 수 있어요. 컨테이너를 중지하면 제거되지 않고 상태가 중지됨으로 바뀌며 컨테이너 안의 프로세스가 중지돼요. docker ps 명령을 실행했을 때 기본 출력은 실행 중인 컨테이너만 표시하는 것이에요. --all 또는 줄여서 -a 를 전달하면 중지된 컨테이너와 실행 중인 컨테이너를 포함한 시스템의 모든 컨테이너를 볼 수 있어요.
$ docker ps --all
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d75e61fcad1e docker-gs-ping "/docker-gs-ping" About a minute ago Exited (2) 23 seconds ago inspiring_ishizaka
f65dbbb9a548 docker-gs-ping "/docker-gs-ping" 3 minutes ago Exited (2) 2 minutes ago wizardly_joliot
aade1bf3d330 docker-gs-ping "/docker-gs-ping" 3 minutes ago Exited (2) 3 minutes ago magical_carson
52d5ce3c15f0 docker-gs-ping "/docker-gs-ping" 9 minutes ago Exited (2) 3 minutes ago gifted_mestorf
따라왔다면 여러 컨테이너가 나열되어 있는 것을 볼 수 있어요. 이것은 시작하고 중지했지만 아직 제거하지 않은 컨테이너들이에요.
방금 중지한 컨테이너를 재시작해주세요. 컨테이너의 이름을 찾아 다음 restart 명령에서 컨테이너의 이름을 바꿔주세요:
$ docker restart inspiring_ishizaka
이제 ps 명령으로 모든 컨테이너를 다시 나열해주세요:
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
d75e61fcad1e docker-gs-ping "/docker-gs-ping" 2 minutes ago Up 5 seconds 0.0.0.0:8080->8080/tcp inspiring_ishizaka
f65dbbb9a548 docker-gs-ping "/docker-gs-ping" 4 minutes ago Exited (2) 2 minutes ago wizardly_joliot
aade1bf3d330 docker-gs-ping "/docker-gs-ping" 4 minutes ago Exited (2) 4 minutes ago magical_carson
52d5ce3c15f0 docker-gs-ping "/docker-gs-ping" 10 minutes ago Exited (2) 4 minutes ago gifted_mestorf
방금 재시작한 컨테이너가 분리된 모드로 시작됐고 8080 포트가 노출된 것을 주목하세요. 또한 컨테이너의 상태가 Up X seconds 임을 주목하세요. 컨테이너를 재시작하면 원래 시작된 것과 같은 플래그나 명령으로 시작돼요.
모든 컨테이너를 중지·제거하고 임의의 이름 문제를 고치는 것을 살펴봐요.
방금 시작한 컨테이너를 중지해주세요. 실행 중인 컨테이너의 이름을 찾아 다음 명령에서 컨테이너의 이름으로 바꿔주세요:
$ docker stop inspiring_ishizaka
inspiring_ishizaka
이제 모든 컨테이너가 중지됐으니 제거해주세요. 컨테이너가 제거되면 더 이상 실행되지 않고 중지된 상태에도 있지 않아요. 대신 컨테이너 안의 프로세스가 종료되고 컨테이너의 메타데이터가 제거돼요.
컨테이너를 제거하려면 docker rm 명령을 실행하고 컨테이너 이름을 전달하세요. 한 명령에 여러 컨테이너 이름을 전달할 수 있어요.
다시 한번 다음 명령의 컨테이너 이름을 내 시스템의 컨테이너 이름으로 바꿔주세요:
$ docker rm inspiring_ishizaka wizardly_joliot magical_carson gifted_mestorf
inspiring_ishizaka
wizardly_joliot
magical_carson
gifted_mestorf
docker ps --all 명령을 다시 실행해 모든 컨테이너가 사라졌는지 확인해주세요.
이제 성가신 임의 이름 문제를 다뤄볼게요. 표준 관행은 컨테이너에 이름을 지정하는 것인데, 그 이유는 컨테이너에서 무엇이 실행 중이고 어떤 어플리케이션이나 서비스와 연관되어 있는지 식별하기 쉽기 때문이에요. 코드에서 변수에 좋은 이름 규칙을 사용하면 읽기가 더 단순해지는 것과 같아요. 컨테이너 이름 지정도 그렇아요.
컨테이너에 이름을 지정하려면 run 명령에 --name 플래그를 전달해야 해요:
$ docker run -d -p 8080:8080 --name rest-server docker-gs-ping
3bbc6a3102ea368c8b966e1878a5ea9b1fc61187afaac1276c41db22e4b7f48f
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3bbc6a3102ea docker-gs-ping "/docker-gs-ping" 25 seconds ago Up 24 seconds 0.0.0.0:8080->8080/tcp rest-server
이제 이름에 기반해 내 컨테이너를 쉽게 식별할 수 있어요.
Go 개발에 컨테이너 사용
사전 요구사항
컨테이너 수명주기를 관리하는 방법을 배우기 위해 이미지를 컨테이너로 실행 하기 모듈의 단계를 작업하세요.
소개
이 모듈에서는 컨테이너에서 데이터베이스 엔진을 실행하고 그것을 예제 어플리케이션의 확장 버전에 연결하는 방법을 살펴볼 거예요. 영구 데이터를 유지하기 위한 몇 가지 옵션과 컨테이너가 서로 통신하도록 연결하는 방법을 볼 거예요. 마지막으로 Docker Compose를 사용해 이와 같은 멀티 컨테이너 로컬 개발 환경을 효과적으로 관리하는 방법을 배워요.
로컬 데이터베이스와 컨테이너
사용할 데이터베이스 엔진은 CockroachDB 예요. 현대적이고 클라우드 네이티브이며 분산된 SQL 데이터베이스예요.
소스 코드에서 CockroachDB를 컴파일하거나 운영 체제의 네이티브 패키지 관리자로 설치하는 대신 CockroachDB용 Docker 이미지 를 사용해 컨테이너에서 실행할 거예요.
CockroachDB는 PostgreSQL과 상당 부분 호환되며, 특히 환경 변수의 기본 이름에서 후자와 많은 관례를 공유해요. 따라서 Postgres에 익숙하다면 익숙한 환경 변수 이름을 보더라도 놀라지 마세요. pgx , pq , GORM , upper/db 같은 Postgres와 작동하는 Go 모듈은 CockroachDB와도 작동해요.
Go와 CockroachDB의 관계에 대한 자세한 내용은 CockroachDB 문서 를 참고할 수 있지만, 이 가이드를 계속하는 데는 필요하지 않아요.
저장소
데이터베이스의 요점은 영구적인 데이터 저장소를 갖는 것이에요. 볼륨 은 Docker 컨테이너가 생성·사용하는 데이터를 영속화하는 선호되는 메커니즘이에요. 따라서 CockroachDB를 시작하기 전에 그것의 볼륨을 만들어요.
관리형 볼륨을 만들려면 다음을 실행해주세요:
$ docker volume create roach
roach
다음 명령으로 Docker 인스턴스의 모든 관리형 볼륨 목록을 볼 수 있어요:
$ docker volume list
DRIVER VOLUME NAME
local roach
네트워킹
예제 어플리케이션과 데이터베이스 엔진은 네트워크를 통해 서로 통신할 거예요. 다양한 종류의 네트워크 구성이 가능하며, 사용자 정의 브리지 네트워크라고 불리는 것을 사용할 거예요. 이는 DNS 조회 서비스를 제공해 내 데이터베이스 엔진 컨테이너를 호스트 이름으로 참조할 수 있게 해줘요.
다음 명령은 mynet 이라는 새 브리지 네트워크를 만들어요:
$ docker network create -d bridge mynet
51344edd6430b5acd121822cacc99f8bc39be63dd125a3b3cd517b6485ab7709
관리형 볼륨의 경우와 마찬가지로 Docker 인스턴스에 설정된 모든 네트워크를 나열하는 명령이 있어요:
$ docker network list
NETWORK ID NAME DRIVER SCOPE
0ac2b1819fa4 bridge bridge local
51344edd6430 mynet bridge local
daed20bbecce host host local
6aee44f40a39 none null local
내 브리지 네트워크 mynet 이 성공적으로 만들어졌어요. bridge , host , none 이라는 다른 세 네트워크는 기본 네트워크이며 Docker가 직접 만든 것이에요. 이 가이드와 관련은 없지만, Docker 네트워킹에 대해 더 배우려면 네트워킹 개요 섹션을 참고할 수 있어요.
볼륨과 네트워크에 좋은 이름 선택
컴퓨터 과학에는 두 가지 어려운 일만 있다는 속담이 있어요: 캐시 무효화와 이름 짓기. 그리고 off-by-one 오류. 네트워크나 관리형 볼륨의 이름을 선택할 때는 의도된 용도를 나타내는 이름을 선택하는 것이 좋아요. 이 가이드는 간결함을 목표로 하므로 짧고 일반적인 이름을 사용했어요.
데이터베이스 엔진 시작
이제 잡일이 끝났으니 컨테이너에서 CockroachDB를 실행하고 방금 만든 볼륨과 네트워크에 연결할 수 있어요. 다음 명령을 실행하면 Docker가 Docker Hub에서 이미지를 내려받아 로컬에서 실행해줄 거예요:
$ docker run -d \
--name roach \
--hostname db \
--network mynet \
-p 26257:26257 \
-p 8080:8080 \
-v roach:/cockroach/cockroach-data \
cockroachdb/cockroach:latest-v25.4 start-single-node \
--insecure
#
# ... output omitted ...
latest-v25.4 태그를 영리하게 사용해 25.4의 최신 패치 버전을 내려받는지 확인하는 것을 주목하세요. 사용 가능한 태그의 다양성은 이미지 관리자에 따라 달라요. 여기서 의도는 시간이 지나도 알려진 작동 버전에서 너무 벗어나지 않으면서 CockroachDB의 최신 패치 버전을 갖는 것이었어요. CockroachDB 이미지에 사용 가능한 태그를 보려면 Docker Hub의 CockroachDB 페이지 로 이동할 수 있어요.
데이터베이스 엔진 구성
이제 데이터베이스 엔진이 가동되고 있으니 어플리케이션이 사용하기 시작하기 전에 할 구성이 있어요. 다행히 많지 않아요. 다음을 해야 해요:
- 빈 데이터베이스 만들기.
- 데이터베이스 엔진에 새 사용자 계정 등록.
- 새 사용자에게 데이터베이스 접근 권한 부여.
CockroachDB 내장 SQL 셸의 도움으로 할 수 있어요. 데이터베이스 엔진이 실행 중인 같은 컨테이너에서 SQL 셸을 시작하려면 다음을 입력해주세요:
$ docker exec -it roach ./cockroach sql --insecure
SQL 셸에서 예제 어플리케이션이 사용할 데이터베이스를 만들어주세요:
CREATE DATABASE mydb;
데이터베이스 엔진에 새 SQL 사용자 계정을 등록해주세요. 사용자 이름 totoro 를 사용하세요.
CREATE USER totoro;
새 사용자에게 필요한 권한을 부여해주세요:
GRANT ALL ON DATABASE mydb TO totoro;
quit 을 입력해 셸을 종료해주세요.
다음은 SQL 셸과의 상호작용 예시예요.
$ sudo docker exec -it roach ./cockroach sql --insecure
#
# Welcome to the CockroachDB SQL shell.
#
# All statements must be terminated by a semicolon.
#
# To exit, type:
#
# \q.
#
# Server version: CockroachDB CCL v20.1.15 (x86_64-unknown-linux-gnu, built 2021/04/26 16:11:58, go1.13.9) (same version as client)
# Cluster ID: 7f43a490-ccd6-4c2a-9534-21f393ca80ce
#
# Enter \? for a brief introduction.
#
root@:26257/defaultdb> CREATE DATABASE mydb;
CREATE DATABASE
Time: 22.985478ms
root@:26257/defaultdb> CREATE USER totoro;
CREATE ROLE
Time: 13.921659ms
root@:26257/defaultdb> GRANT ALL ON DATABASE mydb TO totoro;
GRANT
Time: 14.217559ms
root@:26257/defaultdb> quit
oliver@hki:~$
예제 어플리케이션 만나보기
이제 데이터베이스 엔진을 시작하고 구성했으니 어플리케이션으로 주의를 돌릴 수 있어요.
이 모듈의 예제 어플리케이션은 이전 모듈에서 사용한 docker-gs-ping 어플리케이션의 확장 버전이에요. 두 가지 옵션이 있어요:
- 이 장에서 제시된 새 확장 버전과 일치하도록 로컬 사본의 docker-gs-ping 을 업데이트하거나;
- docker/docker-gs-ping-dev 리포지토리를 클론할 수 있어요. 후자의 접근법이 권장돼요.
예제 어플리케이션을 체크아웃하려면 다음을 실행해주세요:
$ git clone https://github.com/docker/docker-gs-ping-dev.git
#
# ... output omitted ...
어플리케이션의 main.go 는 이제 데이터베이스 초기화 코드와 새 비즈니스 요구사항을 구현하는 코드를 포함해요:
- { "value" : string } JSON을 담은 /send 로의 HTTP POST 요청은 그 값을 데이터베이스에 저장해야 해요.
또한 다른 비즈니스 요구사항에 대한 업데이트도 있어요. 이전 요구사항은:
- 어플리케이션은 / 로의 요청에 하트 기호( "<3" )를 담은 텍스트 메시지로 응답해요.
그리고 이제는:
- 어플리케이션은 괄호로 묶여 데이터베이스에 저장된 메시지 수를 포함한 문자열로 응답해요.
출력 예시:
Hello, Docker! (7)
main.go 의 전체 소스 코드 목록은 다음과 같아요.
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"github.com/cenkalti/backoff/v4"
"github.com/cockroachdb/cockroach-go/v2/crdb"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
db, err := initStore()
if err != nil {
log.Fatalf("failed to initialize the store: %s", err)
}
defer db.Close()
e.GET("/", func(c echo.Context) error {
return rootHandler(db, c)
})
e.GET("/ping", func(c echo.Context) error {
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
})
e.POST("/send", func(c echo.Context) error {
return sendHandler(db, c)
})
httpPort := os.Getenv("HTTP_PORT")
if httpPort == "" {
httpPort = "8080"
}
e.Logger.Fatal(e.Start(":" + httpPort))
}
type Message struct {
Value string `json:"value"`
}
func initStore() (*sql.DB, error) {
pgConnString := fmt.Sprintf("host=%s port=%s dbname=%s user=%s password=%s sslmode=disable",
os.Getenv("PGHOST"),
os.Getenv("PGPORT"),
os.Getenv("PGDATABASE"),
os.Getenv("PGUSER"),
os.Getenv("PGPASSWORD"),
)
var (
db *sql.DB
err error
)
openDB := func() error {
db, err = sql.Open("postgres", pgConnString)
return err
}
err = backoff.Retry(openDB, backoff.NewExponentialBackOff())
if err != nil {
return nil, err
}
// ...
이 코드는 계속되며 루트 핸들러가 데이터베이스의 메시지 수를 반환하고, send 핸들러가 값을 데이터베이스에 저장하는 것을 포함해요. 그런 다음 어플리케이션을 빌드·실행하고 데이터베이스 컨테이너에 연결합니다.
Docker Compose로 더 나은 생산성
이 시점에서 docker 명령에 긴 인자 목록을 다루는 것을 피할 방법이 없는지 궁금할 거예요. 이 시리즈에서 사용한 장난감 예제는 데이터베이스에 대한 연결을 정의하기 위해 다섯 개의 환경 변수를 요구해요. 실제 어플리케이션은 훨씬 더 많은 것을 필요로 할 수 있어요. 그런 다음 의존성의 문제도 있어요. 이상적으로는 어플리케이션이 실행되기 전에 데이터베이스가 시작되도록 확인하고 싶어요. 그리고 데이터베이스 인스턴스를 띄우는 데는 많은 옵션이 있는 또 다른 Docker 명령이 필요할 수 있어요. 하지만 로컬 개발을 위해 이러한 배포를 오케스트레이션하는 더 나은 방법이 있어요.
이 섹션에서는 docker-gs-ping-roach 어플리케이션과 CockroachDB 데이터베이스 엔진을 단일 명령으로 시작하는 Docker Compose 파일을 만들 거예요.
Docker Compose 구성
어플리케이션의 디렉터리에 다음 내용으로 compose.yaml 이라는 새 텍스트 파일을 만들어주세요.
services:
docker-gs-ping-roach:
depends_on:
- roach
build:
context: .
container_name: rest-server
hostname: rest-server
networks:
- mynet
ports:
- 80:8080
environment:
- PGUSER=${PGUSER:-totoro}
- PGPASSWORD=${PGPASSWORD:?database password not set}
- PGHOST=${PGHOST:-db}
- PGPORT=${PGPORT:-26257}
- PGDATABASE=${PGDATABASE:-mydb}
deploy:
restart_policy:
condition: on-failure
roach:
image: cockroachdb/cockroach:latest-v25.4
container_name: roach
hostname: db
networks:
- mynet
ports:
- 26257:26257
- 8080:8080
volumes:
- roach:/cockroach/cockroach-data
command: start-single-node --insecure
volumes:
roach:
networks:
mynet:
driver: bridge
이 Docker Compose 구성은 docker run 명령에 전달할 모든 매개변수를 입력할 필요가 없어 매우 편리해요. Docker Compose 파일에서 선언적으로 할 수 있어요. Docker Compose 문서 페이지 는 상당히 방대하며 Docker Compose 파일 형식에 대한 전체 참조를 포함해요.
.env 파일
Docker Compose는 .env 파일이 있으면 자동으로 환경 변수를 읽어요. 내 Compose 파일은 PGPASSWORD 가 설정되기를 요구하므로 .env 파일에 다음 내용을 추가해주세요:
PGPASSWORD=whatever
CockroachDB를 안전하지 않은 모드에서 실행하므로 이 예제에서는 정확한 값은 중요하지 않아요. 오류가 나지 않도록 변수를 어떤 값으로든 설정했는지 확인하세요.
Compose 파일 병합
compose.yaml 파일 이름은 -f 플래그가 제공되지 않을 때 docker compose 명령이 인식하는 기본 파일 이름이에요. 이는 내 환경에 그러한 요구사항이 있다면 여러 Docker Compose 파일을 가질 수 있다는 뜻이에요. 또한 Docker Compose 파일은 합성이 가능하므로 명령줄에 여러 파일을 지정해 구성의 일부를 병합할 수 있어요. 다음 목록은 그러한 기능이 유용한 시나리오의 몇 가지 예시를 보여줘요:
- 로컬 개발을 위해 소스 코드에 바인드 마운트를 사용하지만 CI 테스트를 실행할 때는 사용하지 않기;
- 어떤 API 어플리케이션의 프론트엔드에 미리 빌드된 이미지를 사용하는 것과 소스 코드용 바인드 마운트를 만드는 것 사이를 전환하기;
- ...
요약
이 모듈에서는 데이터베이스를 실행하는 데 Docker와 컨테이너를 사용하고, 볼륨으로 데이터를 영속화하며, 네트워크로 컨테이너를 연결하고, Docker Compose로 멀티 컨테이너 개발 환경을 관리하는 방법을 배웠어요.
더 알아보기 (Learn more)
- Dockerfile 참조
- 멀티스테이지 빌드
- Docker Compose
- Go