Docker

도커 튜토리얼 2부: Go로 만든 REST API를 Dockerfile 이용해서 도커에 넣기

드리프트2 2024. 3. 31. 17:28

 

안녕하세요?

 

도커 튜토리얼 시리즈 2번째입니다.

 

오늘은 1편에서 배운 도커 기초를 바탕으로 Dockerfile에 대해 살펴보겠습니다.

 

Dockerfile은 도커 이미지를 쉽게 만들어 주는 파일인데요.

 

오늘은 Go 언어로 작성한 REST API를 Dockerfile을 이용해서 도커에 넣어 실행시켜 보겠습니다.


** 목 차 **


Go REST API 작성하기

테스트를 위해 먼저 Go 언어로 REST API를 만들어야겠죠.

 

가장 기본적인 Todo 앱을 만들생각입니다.

mkdir dockerfile-golang-rest-api

cd dockerfile-golang-rest-api

go mod init rest-api-todo

go get github.com/gin-gonic/gin

touch main.go

 

gin-gonic을 이용해서 todo 앱을 만들어 보겠습니다.

 

main.go 파일을 열어 아래와 같이 작성해 볼까요?

package main

import (
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

func displayRoot(context *gin.Context) {
    context.IndentedJSON(http.StatusOK, "Hello Todo App")
}

func main() {
    gin.SetMode(gin.ReleaseMode)

    router := gin.Default()

    if router != nil {
        fmt.Println("Started REST API..........")
    }

    router.GET("/", displayRoot)

    router.Run("0.0.0.0:80")
}

 

위와 같이 작성하고 터미널에서 'go run main.go'식으로 작동해 보겠습니다.

 

브라우저에서 "localhost" 주소로 접속하면 아래와 같이 나옵니다.

 

 

그리고 터미널 창은 아래와 같이 나올겁니다.

 

go run main.go
Started REST API..........
[GIN] 2024/03/31 - 10:07:48 | 200 |      90.958µs |             ::1 | GET      "/"

 

서버가 완벽히 작동하네요.

 

그럼 본격적으로 Go 언어를 이용한 Todo 앱을 확장해 보겠습니다.

package main

import (
    "errors"
    "fmt"
    "net/http"

    "github.com/gin-gonic/gin"
)

type todo struct {
    ID        string `json:"id"`
    Item      string `json:"item"`
    Completed bool   `json:"completed"`
}

var todos = []todo{
    {ID: "1", Item: "Todo Test 1", Completed: false},
    {ID: "2", Item: "Todo Test 2", Completed: true},
    {ID: "3", Item: "Todo Test 3", Completed: false},
}

func displayRoot(context *gin.Context) {
    context.IndentedJSON(http.StatusOK, "Hello Todo App")
}

func getTodos(context *gin.Context) {
    context.IndentedJSON(http.StatusOK, todos)
}

func getTodo(context *gin.Context) {
    id := context.Param("id")
    todo, err := getTodoById(id)

    if err != nil {
        context.IndentedJSON(http.StatusNotFound, gin.H{"message": "Todo not Found!"})
    }
    context.IndentedJSON(http.StatusOK, todo)
}

func getTodoById(id string) (*todo, error) {
    for i, t := range todos {
        if t.ID == id {
            return &todos[i], nil
        }

    }
    return nil, errors.New("todos not found")
}

func addTodo(context *gin.Context) {
    var newTodo todo

    if err := context.BindJSON(&newTodo); err != nil {
        return
    }
    todos = append(todos, newTodo)
    context.IndentedJSON(http.StatusCreated, newTodo)
}

func toggleTodoStatus(context *gin.Context) {
    id := context.Param("id")
    todo, err := getTodoById(id)

    if err != nil {
        context.IndentedJSON(http.StatusNotFound, gin.H{"message": "Todo not found"})
    }
    todo.Completed = !todo.Completed
    context.IndentedJSON(http.StatusOK, todo)
}

func main() {
    gin.SetMode(gin.ReleaseMode)

    router := gin.Default()

    if router != nil {
        fmt.Println("Started REST API..........")
    }

    router.GET("/", displayRoot)
    router.GET("/todos", getTodos)
    router.GET("/todos/:id", getTodo)
    router.POST("/todos", addTodo)
    router.PATCH("/todos/:id", toggleTodoStatus)

    router.Run("0.0.0.0:80")
}

 

getTodos, getTodo, addTodo, toggleTodoStatus 등 Todo 앱을 위한 최소한의 라우터를 완성시켰네요.

 

위와 같이 브라우저에서도 작동이 잘 되고 있습니다.


Dockerfile 이란?

도커 파일(Dockerfile)은 이미지를 빌드하기 위한 명령어들을 담고 있는 텍스트 문서인데요.

 

이 문서는 사용자가 커맨드 라인에서 호출할 수 있는 가능한 모든 명령어를 포함하고 있습니다.

 

그래서 도커는 Dockerfile을 읽어서 이미지를 자동으로 빌드할 수 있습니다.

 

그리고 Dockerfile은 이미지를 구성하는데 필요한 모든 명령어를 순서대로 담고 있습니다.

 

가장 중요한 명령어는 다음과 같은데요.

 

명령어 기능
FROM 지정된 기본 이미지를 가져옵니다.
WORKDIR 이미지의 파일 시스템에서 다른 명령어를 실행할 디렉토리를 지정합니다.
COPY 파일과 디렉토리를 복사합니다.
ADD 로컬 또는 원격 파일과 디렉토리를 이미지에 추가합니다.
RUN 이미지 내에서 OS 명령어를 실행합니다.
ENV 환경 변수를 설정합니다.
EXPOSE 컨테이너가 특정 포트에서 시작되도록 도커에 알립니다.
USER 사용자와 그룹 ID를 설정합니다. 애플리케이션은 이 사용자로 실행됩니다.
CMD 컨테이너 시작 후 실행될 명령어를 지정합니다.
ENTRYPOINT 기본 실행 파일을 지정합니다.
SHELL 기본 쉘 이미지를 설정합니다.

 

이제, 위 명령어를 사용하여 Dockerfile을 작성하면, 이미지 빌드 과정을 자동화할 수 있습니다.

touch Dockerfile

 

위와 같이 'Dockerfile' 이라는 확장자가 없는 파일을 만들었으면, 아래와 같이 Dockerfile 내부 명령어를 넣어주면 됩니다.

FROM alpine:3.18

SHELL ["/bin/sh", "-c"]

RUN adduser -D -u 1000 go_api_user

RUN apk update && \
     apk upgrade && \
     apk add --no-cache \
         curl

RUN curl -sSL https://dl.google.com/go/go1.21.6.linux-amd64.tar.gz | tar -C /usr/local -xz

ENV GOPATH=/go
ENV PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
ENV API_URL=http://127.0.0.1/

WORKDIR /api

COPY . .

RUN go mod download

USER go_api_user

EXPOSE 80

CMD ["sh"]

 

이 Dockerfile에 대해 더 자세히 설명드리자면,

  1. 기본 이미지 설정: Dockerfile은 Alpine3.18 Linux 기본 이미지를 사용하며, 기본 쉘로 "sh"를 지정하고 "-c" 플래그로 명령 줄 인수를 받을 수 있도록 합니다.
  2. 사용자 생성: ROOT 사용자로 작업하는 것은 권장되지 않으므로 사용자를 생성합니다.
  3. 저장소 목록 업데이트 및 시스템 업그레이드: 로컬 파일에 저장소 목록을 업데이트하고 시스템(Alpine Linux)을 업그레이드합니다. 또한 웹에서 파일을 다운로드하기 위해 curl을 설치합니다.
  4. Go 패키지 다운로드 및 추출: 공식 저장소에서 Go 패키지(.tar) 파일을 다운로드하고 원하는 폴더에 추출합니다.
  5. 환경 변수 설정: 필요한 환경 변수를 설정합니다.
  6. 작업 디렉토리 설정: 위에서 만든 'go_api_user' 사용자를 위한 작업 디렉토리를 지정합니다.
  7. REST API Go 프로그램 포트 설정: 컨테이너의 80번 포트를 사용하여 REST API Go 프로그램을 실행합니다. 이를 localhost의 원하는 포트에 매핑합니다. 😏
  8. 이미지 실행: 컨테이너를 "sh" 애플리케이션과 함께 시작합니다. 이는 UNIX의 기본 최소 쉘이며, 컨테이너가 80번 포트를 사용하여 실행됩니다.

dockerignore 파일

도커 파일(Dockerfile)을 완성한 후, Dockerfile에서 생성된 도커 이미지에 특정 파일을 포함시키지 않으려면 .dockerignore 파일을 추가하면 됩니다.

 

git을 사용하다보면 .gitignore 파일이 있는데 이것과 매우 유사합니다.

 

dockerignore 파일의 내용 예를 들어보면,

# .dockerignore 파일 내용
file_to_exclude.txt
directory_to_exclude/

 

위 예시에서 file_to_exclude.txtdirectory_to_exclude/는 도커 이미지에 포함되지 않도록 설정된 파일 및 디렉토리입니다.

 

이렇게 .dockerignore 파일을 작성하면 원하는 파일을 이미지에 포함시키지 않을 수 있습니다.


도커 이미지 만들기

이제 도커로 우리가 작성했던 Go 언어의 todo 앱과 'Dockerfile'이 있는 현재 폴더에서 도커 이미지를 만들어 보겠습니다.

 

'docker build' 명령어를 이용해서 아래와 같이 실행하면 됩니다.

docker build -t rest_api_golang_todos .

 

실행결과는 아래와 같이 나오는데요.

 docker build -t rest_api_golang_todos .
[+] Building 71.1s (13/13) FINISHED                                            docker:desktop-linux
 => [internal] load build definition from Dockerfile                                           0.0s
 => => transferring dockerfile: 468B                                                           0.0s
 => [internal] load metadata for docker.io/library/alpine:3.18                                 2.7s
 => [auth] library/alpine:pull token for registry-1.docker.io                                  0.0s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 2B                                                                0.0s
 => [1/7] FROM docker.io/library/alpine:3.18@sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb7  1.1s
 => => resolve docker.io/library/alpine:3.18@sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb7  0.0s
 => => sha256:24b42af5b7bdb9ccf1252e508ee0a4fd85eb3286a4596c422739ae6beb3038f4 528B / 528B     0.0s
 => => sha256:33abbf0321492ff7379e60c252c05c4e7ed4dccf46fcca6c558067c25e76dc8 1.49kB / 1.49kB  0.0s
 => => sha256:c6b39de5b33961661dc939b997cc1d30cda01e38005a6c6625fd9c7e748bab4 3.33MB / 3.33MB  1.0s
 => => sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb74f49c4cd5cc4c663c7e516 1.64kB / 1.64kB  0.0s
 => => extracting sha256:c6b39de5b33961661dc939b997cc1d30cda01e38005a6c6625fd9c7e748bab44      0.1s
 => [internal] load build context                                                              0.0s
 => => transferring context: 19.57kB                                                           0.0s
 => [2/7] RUN adduser -D -u 1000 go_api_user                                                   0.2s
 => [3/7] RUN apk update &&      apk upgrade &&      apk add --no-cache          curl          3.8s
 => [4/7] RUN curl -sSL https://dl.google.com/go/go1.21.6.linux-amd64.tar.gz | tar -C /usr/l  16.4s
 => [5/7] WORKDIR /api                                                                         0.0s
 => [6/7] COPY . .                                                                             0.0s
 => [7/7] RUN go mod download                                                                 45.5s
 => exporting to image                                                                         1.2s
 => => exporting layers                                                                        1.2s
 => => writing image sha256:5e471b36e45db1c15281b3078d9e9fb60ee0b5c70d62a03403328083a42b5de5   0.0s
 => => naming to docker.io/library/rest_api_golang_todos                                       0.0s

What's Next?
  View a summary of image vulnerabilities and recommendations → docker scout quickview

 

이제 도커 이미지가 생성됐으니 이미지 리스트를 보겠습니다.

 

'docker images' 명령어를 치면 아래와 같이 나옵니다.

 docker images
REPOSITORY              TAG       IMAGE ID       CREATED              SIZE
rest_api_golang_todos   latest    5e471b36e45d   About a minute ago   717MB

 

717MB의 'rest_api_golang_todos'라는 이미지가 만들어졌네요.


도커 이미지 실행하기

이제 위에서 만든 도커 이미지를 실행해 보겠습니다.

 

잠깐 여기서 필요한게 localhost와의 포트 매핑인데요.

 

우리가 만든 todo앱은 80 포트를 사용하고 있습니다.

 

그런데 테스트를 위해 로컬에서는 다른 포트로 매핑하겠습니다.

 

이 때 쓰이는 플래그 옵션이 바로 '-p' 옵션인데요.

 

'-p' 옵션은 '-p host_port:container_port' 이런석으로 사용하면 됩니다.

docker run -it -p 9090:80 rest_api_golang_todos

 

위와 같이 실행하면 되는데요.

 

그러면 아래와 같이 도커가 실행되면서 우리가 설정한 알파인 리눅스와 해당 'api' 폴더가 나타납니다.

 docker run -it -p 9090:80 rest_api_golang_todos
/api $ ls
Dockerfile            go.sum                main.go
go.mod                README.md

 

위와 같이 'ls' 명령어를 실행시키니까 우리가 로컬에서 개발했던 모든 파일이 나옵니다.

 

왜냐하면 Dockerfile의 아래 명령어 때문입니다.

WORKDIR /api

COPY . .

 

이제 알파인 리눅스에서 'go run main.go' 명령어를 실행해 볼까요?

/api $ go run main.go
Started REST API..........

 

위와 같이 조금 시간이 걸리고 난 후 REST API가 작동됩니다.

 

이제 브라우저에서 'localhost:9090'포트로 이동해서 보면 웹 애플리케이션이 잘 작동하는 걸 볼 수 있을 겁니다.

 

위 그림과 같이 잘 작동됩니다.

 

그리고 터미널 창에서도 아래와 같이 gin-gonic의 로그가 잘 보이고요.

/api $ go run main.go
Started REST API..........
[GIN] 2024/03/31 - 01:36:44 | 200 |    3.585792ms |    192.168.65.1 | GET      "/"
[GIN] 2024/03/31 - 01:36:44 | 404 |       1.834µs |    192.168.65.1 | GET      "/favicon.ico"
[GIN] 2024/03/31 - 01:37:25 | 200 |    7.273833ms |    192.168.65.1 | GET      "/todos"
[GIN] 2024/03/31 - 01:37:33 | 200 |     831.625µs |    192.168.65.1 | GET      "/todos/1"

Dockerfile에 실행 코드까지 넣기

우리가 위에서는 docer run 옵션에서 '-it' 옵션을 주었는데요.

 

이걸 빼고 실행하면 그냥 끝나버립니다.

docker run -p 9090:80 rest_api_golang_todos

 

그래서 Dockerfile에 우리가 만든 앱을 실행하는 명령어를 추가하면 되는데요.

 

마지막에 아래와 같이 ENTRYPOINT를 지정하면 됩니다.

FROM alpine:3.18 AS builder

SHELL ["/bin/sh", "-c"]

RUN adduser -D -u 1000 go_api_user

RUN apk update && \
     apk upgrade && \
     apk add --no-cache \
         curl

RUN curl -sSL https://dl.google.com/go/go1.21.6.linux-amd64.tar.gz | tar -C /usr/local -xz

ENV GOPATH=/go
ENV PATH=$PATH:/usr/local/go/bin:$GOPATH/bin
ENV API_URL=http://127.0.0.1/

WORKDIR /api

COPY . .

RUN go mod download

RUN go build -o main .

USER go_api_user

EXPOSE 80

FROM scratch

COPY --from=builder /api/main .

ENTRYPOINT ["/main"]

 

위 도커파일에서는 main.go 파일을 컴파일까지 하고 있습니다.

 

그리고 컴파일된 main 이라는 실행파일을 builder라는 알파인 리눅스에서 카피해 오고 있습니다.

 

그리고 ENTRYPOINT를 지정하면 해당 파일이 실행하게 되는데요.

 

이제 도커 이미지를 다시 만들어야 하는데요.

docker build -t rest_api_golang_todos . 

 

도커가 다시 다 만들어지면 'docker run'을 아래와 같이 실행해 봅시다.

docker run -p 9090:80 rest_api_golang_todos
Started REST API..........
[GIN] 2024/03/31 - 01:59:00 | 200 |    7.434959ms |    192.168.65.1 | GET      "/todos/1"

 

이제 클라우드에 바로 올릴 수 있는 코드가 완성되었네요.


DockerHub에 이미지 올려보기

도커 이미지에 tag를 부여하면 버전 관리가 아주 쉬운데요.

docker image tag b2ea rest_api_golang_todos:v1

docker images
REPOSITORY              TAG       IMAGE ID       CREATED          SIZE
rest_api_golang_todos   latest    b2ea50f60cec   6 minutes ago    11.5MB
rest_api_golang_todos   v1        b2ea50f60cec   6 minutes ago    11.5MB

 

위와 같이 TAG에 v1이라는 이름으로 rest_api_golang_todos 이미지가 생겼습니다.

 

그런데 rest_api_golang_todos라는 이름은 DockerHUB에 누군가 먼저 올려놨을 수 있기 때문에 TAG 이름에 본인 아이디를 넣는걸 추천드립니다.

 

docker image tag b2ea cpro95/rest_api_golang_todos:v1

docker images
REPOSITORY                     TAG       IMAGE ID       CREATED          SIZE
cpro95/rest_api_golang_todos   v1        b2ea50f60cec   8 minutes ago    11.5MB
rest_api_golang_todos          latest    b2ea50f60cec   8 minutes ago    11.5MB
rest_api_golang_todos          v1        b2ea50f60cec   8 minutes ago    11.5MB

 

이제 도커에 로그인하겠습니다.

docker login

Authenticating with existing credentials...
Login Succeeded

 

저는 docker desktop에서 로그인 해 놓은 상태라서 위와 같이 바로 로그인 되었습니다.

 

이제 도커 이미지를 push 해보겠습니다.

docker push cpro95/rest_api_golang_todos:v1

The push refers to repository [docker.io/cpro95/rest_api_golang_todos]
d928abf30a5d: Pushed 
v1: digest: sha256:0947ac45f239698619d52f04ac2aa6cd64e899a7bd8bfed3796a2c1a688e0e97 size: 527

 

위와 같이 나오네요.

 

도커 push 가 성공했습니다.

 

DockerHUB 홈페이지에서 볼까요?

 

위와 같이 잘 올라가 있네요.

 

그리고 홈페이지에서 Tags를 클릭하면 v1이라는 버전이 나오면서 PULL 할 수 있는 코드도 제공해 줍니다.

 

이제 dockerhub에 있는 이미지를 pull해서 사용해 볼까요?

 

docker rmi b2ea --force 

 

위와 같이 현재 로컬에 있는 도커 이미지를 강제로 지웠습니다.

 

docker images
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

 

위와 같이 현재 도커 이미지가 없는 상태입니다

 

이제 도커 pull 해보겠습니다.

docker pull cpro95/rest_api_golang_todos:v1

docker images
REPOSITORY                     TAG       IMAGE ID       CREATED          SIZE
cpro95/rest_api_golang_todos   v1        b2ea50f60cec   24 minutes ago   11.5MB

 

도커 풀 명령어가 잘 작동하네요.

 

여기서 도커 런 명령어는 아래와 같이 해야 합니다.

docker run -p 9090:80 cpro95/rest_api_golang_todos:v1
Started REST API..........

 

우리가 태그이름을 다르게 해서 꼭 태그 이름에 유의해서 run 명령어를 작성해야 합니다.