Skip to main content

DockerでGoアプリケーションをコンテナ化するためのベストプラクティス

著者
Headshot of Jekayin-Oluwa Olabemiwo

Jekayin-Oluwa Olabemiwo

blog feature docker labels

2022年3月23日

0 分で読めます

Goアプリケーションとコンテナは相性抜群です。Goのアプリケーションバイナリはサイズが小さく、DockerやKubernetesが得意とするマイクロサービスのデプロイ戦略に最適です。ただし、この相乗効果にも課題はあります。コンテナイメージに簡単に入り込むセキュリティ上の落とし穴を避けるには、コンテナのベストプラクティスと重要な概念を理解することが大切です。

この記事では、サンプルのGoアプリケーションをコーディングし、安全にコンテナ化して実行する方法を学びます。

前提条件

このチュートリアルを進めるには、DockerとGoがマシンにインストールされており、両方の基本的な知識が必要です。Dockerの入門手順とダウンロードについては、Dockerドキュメントの初期設定とセットアップページをご覧ください。Goの優れたチュートリアルは公式のGoドキュメントで確認できます。または、Linux向けのGoのインストールページに進んでください。

サンプルのGoアプリケーションを作成する

まず、Go APIを作成しましょう。最初に、マシン上でGoアプリケーションを配置するディレクトリに移動します。次に、「godocker」というディレクトリを作成します。このgodocker/ディレクトリで、次のコマンドを実行してGoモジュールを定義します。

go mod init godocker

次に、「godocker」ディレクトリ内に「main.go」というファイルを作成します。このファイルにAPIのコードを記述します。現在時刻をAPI経由で提供し、処理するロジックを追加しましょう。

次のコードをgodocker/main.goに入力します。

package main

import (
"encoding/json"
"fmt"
"log"
"net/http"
)

type Time struct {
CurrentTime string `json:"current_time"`
}

func main() {
// defining router
mux := http.NewServeMux()
mux.HandleFunc("/time", getTime)

// starting server
fmt.Println("Server is running at 127.0.0.1:8080")
log.Fatal(http.ListenAndServe( "localhost:8080", mux))
}

func getTime(w http.ResponseWriter, r *http.Request) {
currentTime := []Time{
    { CurrentTime: http.TimeFormat },
}

json.NewEncoder(w).Encode(currentTime)
}

このコードは、httpパッケージのTimeFormat変数を使って、API経由で現在時刻を提供します。次に、NewServeMuxライブラリをmuxとして定義し、HTTPサーバーを設定します。最後に、/timeエンドポイントを登録してAPIで現在時刻を返し、/timeエンドポイントのレスポンスをJSONとしてエンコードします。

go mod init godocker

簡単にテストしてみましょう。シェルターミナルで次のコマンドを実行してアプリケーションを起動します。

go run main.go

次のような出力が表示されます。

Server is running at 127.0.0.1:8080

次に、別のターミナルウィンドウでcURLを使ってAPIをテストします。ターミナルで次のコマンドを入力してください。

curl http://127.0.0.1:8080/time

次のような出力が表示されます。

[{"current_time":"Mon, 02 Jan 2006 15:04:05 GMT"}]

Dockerfileを準備する

Dockerfileには、アプリケーションをコンテナとしてパッケージ化してデプロイするための一連の指示が記述されています。このセクションではDockerfileを作成し、サンプルアプリケーションをコンテナとしてパッケージ化するために使える指示をいくつか確認します。

Dockerの構文バージョンを指定する

まず、構文ディレクティブを追加します。godocker/ディレクトリに「Dockerfile」という新しいファイルを作成し、最初の行に次のコードを入力します。

# syntax=docker/dockerfile:1

構文ディレクティブでは、Dockerfileのビルドに使用する構文の場所を指定します。このコード行では、Dockerfileの構文の場所をdocker/dockerfile:1として定義しています。これは最新リリースの構文バージョンです。Dockerは、Buildkitバックエンドを使ってDockerfileをビルドする前に、構文バージョンを確認します。

この行がファイルの先頭でコメントとして記述されていることを確認してください。。構文ディレクティブの後には、空行を入れるのが慣例です。

メモリ使用量の少ないベースイメージを継承する

次に、FROM命令で継承するDockerベースイメージを指定します。

次のコードをgodocker/Dockerfil`に追加します。

FROM golang:1.17-alpine

この命令を使えば、独自のDockerベースイメージをビルドする必要はありません。代わりに、Alpine Linux版のGoアプリケーション向け公式Dockerイメージを継承します。ベースイメージのGoバージョンは1.17です。alpineイメージは、ubuntuイメージのようなバリアントと比べて非常に小さいサイズです。

次に、各行の目的を簡単に説明するコメントをコードに含めながら、Dockerfileの残りを記述します。

godocker/Dockerfileを更新し、次のコードを記述します。

# syntax=docker/dockerfile:1

# specify the base image to  be used for the application, alpine or ubuntu
FROM golang:1.17-alpine

# create a working directory inside the image
WORKDIR /app

# copy Go modules and dependencies to image
COPY go.mod ./

# download Go modules and dependencies
RUN go mod download

# copy directory files i.e all files ending with .go
COPY *.go ./

# compile application
RUN go build -o /godocker

# tells Docker that the container listens on specified network ports at runtime
EXPOSE 8080

# command to be used to execute when the image is used to start a container
CMD [ "/godocker" ]

イメージをビルドする

Dockerfileが完成したので、docker buildコマンドを使ってDockerfileからDockerイメージをビルドしましょう。DockerはDockerデーモンを使ってイメージをビルドします。--tagオプション(-tと短縮可能)をdocker buildコマンドとともに使い、Dockerイメージに任意の名前を設定します。

ターミナルで次のコマンドを入力してください。

docker build --tag godocker .

docker buildコマンドに.を付けると、ビルドコンテキストとして使われる現在のディレクトリでイメージがビルドされます。ビルドコンテキストに/パスを使用するのは避けてください。ソースコード全体がDockerデーモンに転送される可能性があります。

ビルド出力にFINISHEDという行が含まれ、次のように表示されます。

[+] Building 6.8s (17/17) FINISHED                                                            
...
 => => writing image sha256:539bdb3e661f66d489467ef217e1b46786de9cf3c29dc9a2dd6b4e9fa763  0.0s
 => => naming to docker.io/library/godocker

この出力は、godockerタグ付きのDockerイメージが完全にビルドされたことを示しています。

ローカルイメージの一覧を表示するには、ターミナルで次のコマンドを入力します。

docker image ls

次のような出力が表示されます。

REPOSITORY              TAG        IMAGE ID        CREATED         SIZE
godocker                latest     539bdb3e661f    2 minutes ago   319MB
docker/getting-started  latest     720f449e5af2    1 hour ago      27.2MB

出力にある、新しくビルドしたgodockerイメージのサイズは319MBです。シンプルなAPIアプリケーションとしては大きすぎるため、より軽量なイメージを作成できるようビルドを最適化する必要があります。次のセクションでは、マルチステージビルドの概念を実装して、軽量なビルドを実現します。

マルチステージビルドを使う

マルチステージビルドを使うと、前のセクションで紹介した単一ステージの方法と比べて、はるかに小さなイメージを作成できます。マルチステージビルドでは、イメージを使って各フラグメントをビルドし、それらを実行するために必要な要素だけで構成される小さなイメージにパッケージ化します。アプリケーションの実行に必要な最低限の要素までイメージを削減すれば、セキュリティの脆弱性が生じる可能性を抑えられます。これを実現するには、Dockerfileで複数のFROM命令を使います。

公式のscratchイメージを使う

scratchの公式Dockerイメージを継承すれば、空のイメージからビルドを開始できます。このセクションでは、マルチステージビルドでscratchイメージを使う方法を紹介します。

まず、アプリケーションのルートディレクトリに移動します。次に、「Dockerfile.multistage」というファイルを作成し、以下のコードを入力します。

# syntax=docker/dockerfile:1

##
## STEP 1 - BUILD
##

# specify the base image to  be used for the application, alpine or ubuntu
FROM golang:1.17-alpine AS build

# create a working directory inside the image
WORKDIR /app

# copy Go modules and dependencies to image
COPY go.mod ./

# download Go modules and dependencies
RUN go mod download

# copy directory files i.e all files ending with .go
COPY *.go ./

# compile application
RUN go build -o /godocker

##
## STEP 2 - DEPLOY
##
FROM scratch

WORKDIR /

COPY --from=build /godocker /godocker

EXPOSE 8080

ENTRYPOINT ["/godocker"]

このコードでは、公式のgolang:1.17-alpineイメージをベースイメージとして継承し、ステージ名をbuildに指定しています。次に、別のFROM命令を使ってマルチステージの仕組みを実装し、最初のステージでビルドしたバイナリを、2番目のステージの空のイメージにコピーします。

次に、新しいDockerfile.multistageファイルを使って、新しいイメージをビルドします。また、新しいイメージに「multistage」というタグを付けます。これにより、先ほどビルドしたイメージと区別できます。

ターミナルで次のコマンドを入力してください。

docker build -t godocker:multistage -f Dockerfile.multistage .

ビルドが正常に完了したら、ターミナルで次のコマンドを入力してイメージの一覧を確認します。

docker image ls

次のような出力が表示されます。

REPOSITORY              TAG        IMAGE ID        CREATED         SIZE
godocker                multistage 192cc137f88b    9 seconds ago   6.18MB
godocker                latest     539bdb3e661f    1 hour ago      319MB

この出力から、godocker:multistageイメージとgodocker:latestイメージのサイズに大きな差があることがわかります。単一ステージのイメージは319 MBですが、マルチステージのイメージは6.1 MBで、明らかに改善されています。コンテナはすばやく起動する必要があるため、このような最適化はGoアプリケーションのコンテナ化に不可欠です。

コンテナをデプロイする

パフォーマンスと効率の最適化に加えて、コンテナを安全に実行できるよう、最適なデプロイ方法についても考える必要があります。このチュートリアルでは、この段階でいくつかのベストプラクティスを実装します。

root以外のユーザーとして実行する

最小権限の原則に基づき、システムリソースへのアクセスを制限する必要があります。GoのDockerコンテナはアプリケーションコンテナであり、root権限で実行する必要はありません。そのため、セキュリティを強化するため、Dockerfileでアクセス権を制限した新しいユーザーとグループを作成しましょう。

root以外のユーザーを作成するには、godocker/Dockerfile.multistageの最初のFROM命令の直後に、次の行を追加します。

RUN useradd -u 1001 -m iamuser

この命令では、ARGキーワードでUSERNAMEとPASSWORDの引数を設定し、続いてRUN adduser命令でユーザーを作成します。

次に、最初のステージからユーザーの詳細をコピーし、2番目のステージに適用する命令を実装します。

次の例で太字になっているコードをgodocker/Dockerfile.multistageの2番目のステージに追加します。ファイルの末尾は次のようになります。

...
##
## STEP 2 - DEPLOY
##
FROM scratch

WORKDIR /

COPY --from=build /godocker /godocker

COPY --from=build /etc/passwd /etc/passwd

USER 1001

EXPOSE 8080

ENTRYPOINT ["/godocker"]

Kubernetesでは、securityContextフィールドでrunAsuser: UIDを指定できます。Podのセキュリティコンテキストを設定する方法については、Kubernetesのドキュメントをご覧ください。

読み取り専用のルートファイルシステムで実行する

アプリケーションのセキュリティを高めるもう1つの方法は、読み取り専用のファイルシステムでコンテナを実行することです。コンテナ内のファイルシステムを読み取り専用にするには、docker runコマンドに読み取り専用フラグを指定します。

ターミナルで次のコマンドを入力してください。

docker run -read-only godocker

Linuxのケイパビリティを削除または拒否する

Linuxのケイパビリティとは、Linuxの使用時に有効化または無効化できる権限の集合です。コンテナはLinuxのバリアントをベースとしているため、これらのケイパビリティを使ってアプリケーションのセキュリティを高めることもできます。一部のケイパビリティを削除すると、コンテナのリスクを軽減できます。

このチュートリアルでは、setuid以外のすべてのケイパビリティを削除しましょう。

ターミナルで次のコマンドを入力してください。

docker run --cap-drop=all --cap-add=setuid

CPUとメモリの使用量を制限する

docker runコマンドを使うと、Dockerコンテナがホストマシンのリソースを使用する量を制限できます。Dockerでは--cpusフラグを使います。

たとえば、コンテナが単一CPUの50%を超えて使用しないようにするには、Docker CLIで次のコマンドを入力します。

docker run -it --cpus=".5" alpine /bin/bash

2つのCPUを使う必要がある場合は、Docker CLIで次のコマンドを使って使用量を制限できます。

docker run -it --cpus=2 alpine /bin/bash

Dockerコンテナのメモリ使用量を1024MBに制限するには、次のようにdocker runコマンドを使います。

docker run -m 1024m --memory-reservation=256m alpine /bin/bash

このコマンドでは256MBのメモリ領域の上限も設定されます。Dockerがホストのメモリ不足を検出すると、この上限が適用されます。

まとめ

この記事では、Goアプリケーションをセットアップし、Dockerでコンテナ化する手順を紹介しました。また、パフォーマンスを最適化するマルチステージビルドを実装し、空の公式Dockerイメージからビルドする方法や、コンテナのビルドとデプロイに役立つベストプラクティスについても説明しました。これらのガイドラインは、DockerでGoのWebアプリケーションをコンテナ化する際に、効率性、セキュリティ、メモリ管理を強化するための出発点となります。

セキュリティのベストプラクティスについて詳しく知りたい方は、リソースセンターのSnyk Learnをご覧ください。