Red Hat OpenShift Study / コンテナのデプロイ - (0) Dockerおさらい


はじめに

このシリーズでは、コンテナ化されたアプリケーションをRed Hat OpenShift上にデプロイする流れについて調査した内容を整理していこうと思います。
OpenShiftはKubernetesをベースとしてエンタープライズ向けに各種機能が拡張されており、アプリケーションのデプロイ部分についても多くの有用な機能が提供されています。特にアプリケーション(コンテナ)をOpenShiftクラスターにデプロイするためのoc new-appコマンドではかなり強力な機能が提供されていますが、反面、様々なことができすぎて何をやっているのか理解するのがかなり困難でした。ここでは自分なりの理解を整理していきたいと思います。
最初は本題に入る前のおさらいとして、OpenShift環境ではない場合のDocker環境について前提となる考え方を整理しておきます。

関連記事

Red Hat OpenShift Study / コンテナのデプロイ - (0) Dockerおさらい
Red Hat OpenShift Study / コンテナのデプロイ - (1) Dockerイメージを元にしたデプロイ
Red Hat OpenShift Study / コンテナのデプロイ - (2) Dockerビルド
Red Hat OpenShift Study / コンテナのデプロイ - (3) s2i ビルド

Docker関連用語の整理

コンテナ利用時のざっくりとした概要図

主要な登場人物はこちら。

Dockerコンテナ:
Dockerイメージを基にして作成される仮想環境のインスタンスです。コンテナが稼働することでミドルウェアやアプリケーションのサービスを提供します。

Dockerイメージ:
Dockerコンテナの元になるイミュータブルな情報の塊です。カーネルより上のミドルウェアやアプリケーション・モジュールなどが組み込まれています。
Dokcerイメージはその内容によって様々な用途で使用されます。
例えばmysqlやnginxなどのミドルウェアが稼働できるようにしたDockerイメージというのは多数提供されており、そのようなイメージを使うと構成パラメーターなどを与えることですぐにサービスを利用開始できます。
あるいは、最小限のOSパッケージのみ提供した、他のDockerイメージを作成する際のベースとなるようなものも提供されています。これを使って独自のアプリケーションを組み込んだDockerイメージをビルドすることができます。
また、後々取り上げますが、アプリケーションのコンパイラなどを組み込んだいわゆる開発環境として利用するためのDockerイメージというのもあります。これは"ビルダー・イメージ"と呼ばれたりします。(これはOpenShiftでは非常に重要です!)

Dockerリポジトリ:
Dockerイメージは通常Dockerレジストリというサーバー上で管理されます。この時、Dockerイメージはタグ付けされてバージョン管理されるので、同じ内容の異なるDockerイメージ(異なるバージョン)の集合を、リポジトリという単位で管理されることになります。
"リポジトリ名"と"イメージ名"はほぼ同義的に使われていることも多く、当記事でもあまり厳密には区別していませんが、レジストリの管理単位を意識する際は"リポジトリ名"、それ以外は"イメージ名"と記載することが多いと思います。

Dockerレジストリ:
Dockerリポジトリを集約して管理しホスティング機能を提供するサービスです。
インターネット上で利用できるサービスとしては、Docker Hub(イメージを参照する時のデフォルトの参照先として利用される)や、Red Hat Container Registry(Red Hatが管理しているレジストリー。参考) 、Quay.ioGitLab.com などがあります。
また、インターネット上で提供されるサービスではなく、セキュアな環境に独自にDockerレジストリのサーバーを立てるということも可能で、GitLab Container RegistryやSonatype Nexus Repositoryなど色々選択肢もあるようです。ちなみにOpenShift内にも内部レジストリを保持しています。


Dockerイメージを扱う場合、Dockerイメージは以下のような書式で表されます。
<レジストリ名>/<イメージ名>:<タグ名>
レジストリ名とタグ名は省略可能で、省略した場合通常はデフォルトでレジストリ名はdocker.io(DockerHub)、タグ名はlatestが使われます。例えばdockerコマンドで、
docker pull centos
というコマンドを実行した場合、centos 部分は正確にはdocker.io/centos:latestと解釈されます。(docker.io/centosコチラです。)

上の例はイメージ名がシンプルにcentosという名前でしたが、前段に名前空間が付くものもあります。例えばansible/centos7-ansible(コレ)といった感じです。このイメージをpullしたい場合は以下のような書き方になります。いずれも同義です。
docker pull ansible/centos7-ansible
docker pull ansible/centos7-ansible:latest
docker pull docker.io/ansible/centos7-ansible:latest

名前空間が無いものはDockerHubが公式に公開しているDockerイメージで、名前空間が付いているものはその名前空間のユーザーが提供しているDockerイメージです。例えばDockerHubにtomotagworkというユーザーを作ってそこにDockerイメージをアップロードすると、tomotagwork/xxxというイメージ名でアクセスすることになります。

OpenShiftクラスターにアプリケーションをデプロイする場合、どのレジストリのイメージをベースにどうやって新たなイメージをビルドして、それをどこのレジストリで管理するか、みたいなことは必ずついて回るのでこの辺りの関係性はきちんと把握しておく必要があります。

Dockerイメージのビルド

あるサービスをコンテナとして稼働させようとする場合、出来合いのDockerイメージをそのまデプロイすればOKということにはならないので、なんらかのカスタマイズを加えたりアプリケーションを組み込んだりしてDockerイメージをビルドし、それを利用することが普通です。
流れを図示するとこんな感じになると思います。

Dockerイメージのビルドの例は別の記事でシンプルな例を記載しているのでそちらもご参照ください。
参考: コンテナ型仮想化技術 Study01 / Docker基礎 - Dockerイメージ作成

Dockerイメージをビルドする場合、Dockerfileでビルド時に実行する内容を指定します。
上のリンク先の例のDockerfileを再掲します。

FROM golang:1.9

RUN mkdir /echo
COPY main.go /echo

CMD ["go", "run", "/echo/main.go"]

FROMでベースとなるDockerイメージを指定します。上の例ではDockerHub上に提供されるgolang:1.9というDockerイメージをベースとして使うことになります。これはGo言語の実行環境が提供されるDockerイメージです。
RUNやCOPYなどを使用してベースとなるDockerイメージ対するカスタマイズ内容を記載します。上の例では、Dockerイメージ内に/echoというディレクトリを作成し、ローカルの環境のmain.goというファイル(Goのソース)をDockerイメージ内の/echoディレクトリにコピーしています。
CMDでは、コンテナ起動時に実行されるコマンドを記載しておきます。

このDockerfileと動かしたいGOのソース(main.go)を作成して'docker build'コマンドを実行すると、最終目的のDockerイメージがビルドできます。それをdocker runで実行すれば、コンテナ上でmain.goが実行されるということになります。
これは非常に単純なGo言語の例でしたが、Javaなどコンパイルを行う必要があるケースや、node.jsなど依存関係のあるモジュールをダウンロードするプロセスが必要なケース、あるいは特殊なOSパッケージが必要であればそれをyumでインストールする必要があるケースなどもあります。その場合、Dockerfileがもっと複雑になったり、処理をスクリプト化して与えてあげる必要があります。

Dockerfile - ONBUILD

Dockerイメージは基本的には他のイメージを元にカスタマイズしていくものなので親子関係が生じます。この親子関係に大きく依存する機能としてDockerfileの書き方でONBUILDというコマンドがあります。これが指定されたコマンドはそのDockerfileのビルド時ではなく、そこで生成されたイメージをベースとして子のイメージをビルドする際に実行されます。

具体的にやってみます。

親のイメージをビルド

Dockerfile(親)
FROM golang:latest

RUN mkdir /test
ONBUILD COPY main.go /test

CMD ["go", "run", "/test/main.go"]
親イメージのビルド
# docker build -t goparent .
Sending build context to Docker daemon  2.048kB
Step 1/4 : FROM golang:latest
 ---> b09f7387a719
Step 2/4 : RUN mkdir /test
 ---> Using cache
 ---> 75cb335614e4
Step 3/4 : ONBUILD COPY main.go /test
 ---> Running in 2c0ac4735863
Removing intermediate container 2c0ac4735863
 ---> 2a88949a0ca8
Step 4/4 : CMD ["go", "run", "/test/main.go"]
 ---> Running in d56f92f651e8
Removing intermediate container d56f92f651e8
 ---> ef8f087ededa
Successfully built ef8f087ededa
Successfully tagged goparent:latest

イメージがビルドされました。

イメージ確認
# docker images
REPOSITORY                                      TAG                 IMAGE ID            CREATED             SIZE
goparent                                        latest              ef8f087ededa        2 minutes ago       862MB
...

docker historyで、そのイメージに対して行われた変更の内容を確認できます。ここでONBUILDコマンドも確認できます。ONBUILDで指定したコマンドはこの親イメージのビルド時には実行されていません。従って、親イメージのビルド時にはmain.goファイルは不要です。(以下の表示だとコマンドの後ろが切れてしまっていますが、--no-truncオプションを付けると省略されずに表示されます。)

イメージのhistory確認
# docker history goparent:latest
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
ef8f087ededa        3 minutes ago       /bin/sh -c #(nop)  CMD ["go" "run" "/test/ma…   0B
2a88949a0ca8        3 minutes ago       /bin/sh -c #(nop)  ONBUILD COPY main.go /test   0B
75cb335614e4        13 minutes ago      /bin/sh -c mkdir /test                          0B
b09f7387a719        9 days ago          /bin/sh -c #(nop) WORKDIR /go                   0B
<missing>           9 days ago          /bin/sh -c mkdir -p "$GOPATH/src" "$GOPATH/b…   0B
<missing>           9 days ago          /bin/sh -c #(nop)  ENV PATH=/go/bin:/usr/loc…   0B
<missing>           9 days ago          /bin/sh -c #(nop)  ENV GOPATH=/go               0B
<missing>           9 days ago          /bin/sh -c set -eux;   dpkgArch="$(dpkg --pr…   386MB
<missing>           9 days ago          /bin/sh -c #(nop)  ENV GOLANG_VERSION=1.16.5    0B
<missing>           4 weeks ago         /bin/sh -c #(nop)  ENV PATH=/usr/local/go/bi…   0B
<missing>           4 weeks ago         /bin/sh -c apt-get update && apt-get install…   182MB
<missing>           4 weeks ago         /bin/sh -c apt-get update && apt-get install…   146MB
<missing>           4 weeks ago         /bin/sh -c set -ex;  if ! command -v gpg > /…   17.5MB
<missing>           4 weeks ago         /bin/sh -c set -eux;  apt-get update;  apt-g…   16.5MB
<missing>           4 weeks ago         /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>           4 weeks ago         /bin/sh -c #(nop) ADD file:1a1eae7a82c66d673…   114MB

※後述のpodmanコマンドを使う場合、ONBUILDが含まれているDockerfileをビルドするためには--format dockerオプションを指定する必要があります(ONBUILDはOCIという標準に含まれていないため)。

子のイメージをビルド

ここでは親イメージをRegistryにはアップせずに、そのままローカルのDocker環境にあるものを使用します。FROM以外は特に何も指定しません。

Dockerfile(子)
FROM goparent:latest

親イメージビルド時に、main.goファイルをDockerイメージ内にコピーするようONBUILDで指定されているので、main.goファイルを用意しておきます。

main.go
package main

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

func main(){
        http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
                log.Println("received request")
                fmt.Fprintf(w, "Hello Docker!")
        })

        log.Println("start server")
        server := &http.Server{Addr: ":8080"}
        if err := server.ListenAndServe(); err != nil {
                log.Println(err)
        }
}

ビルドします。

# docker build -t gochild .
Sending build context to Docker daemon  3.072kB
Step 1/1 : FROM goparent:latest
# Executing 1 build trigger
 ---> d2bb1b8ee97e
Successfully built d2bb1b8ee97e
Successfully tagged gochild:latest

コンテナを稼働させて確認してみます。

# docker run -d -p 8080:8080 gochild
75f586a4717bb269aae84e0b7e2cf85afd43f4d77089b99bee6a191ab788cf84

# curl localhost:8080
Hello Docker!

きちんと親イメージのビルド時に指定されたONBUILDが子イメージのビルド時に実行されました!

ここで実施した操作を図示するとこんな感じです。

podmanコマンド

コンテナ管理用の仕組みとしてdockerに代わるpodmanというコマンドが提供されており、Red Hatはpodmanの利用を推奨しているようです。dockerと違いpodmanはデーモンを稼働させておかなくてもコンテナを扱うことができます。podman buildpodman runなどdockerと同じ体系のコマンドがおぼそのまま使えますし、一部Kubernetes/OpenShiftのPodの単位を簡易的に取り扱うこともできるので、OpenShiftを扱う場合にはpodmanを併用すると使い勝手がよいと思います。

参考: podman

skopeoコマンド

OpenShiftでコンテナを扱う場合、基本的には元になるDockerイメージはレジストリ上に管理しておく必要があります。DockerイメージのビルドはどこかのLinux環境でPodmanやDockerを使って行われるので、Dockerイメージをレジストリにアップしたりすることが必要になりますが、そこで使用できるのがskopeoコマンドです。
参考: skopeo

skopeo copyコマンドを使用すると、レジストリ-ローカル環境間でDockerイメージのコピーができます。Dockerイメージの形式はいくつかあるのでその形式ごとに指定の仕方が異なります。

認証が必要なレジストリにアクセスする場合は、事前にpodman loginでレジストリにログインしておく必要があります(docker loginでも可)。
また、レジストリ上で別のタグにコピーすることも可能です。

skopeo inspectコマンドを使用すると、Dockerイメージの情報が確認できます。

inspect実行例
# skopeo inspect docker://docker.io/library/httpd:latest
{
    "Name": "docker.io/library/httpd",
    "Digest": "sha256:48bae0ac5d0d75168f1c1282c0eb21b43302cb1b5c5dc9fa3b4a758ccfb36fe9",
    "RepoTags": [
        "2-alpine",
         ...
        "2.4",
        "2",
        "alpine",
        "latest"
    ],
    "Created": "2021-05-26T00:23:59.233513625Z",
    "DockerVersion": "19.03.12",
    "Labels": null,
    "Architecture": "amd64",
    "Os": "linux",
    "Layers": [
        "sha256:69692152171afee1fd341febc390747cfca2ff302f2881d8b394e786af605696",
        "sha256:7284b4e0cc7b197edc206f815c5b24e67b9ed29abd9bbd8ae4bfdd5540bec6ec",
        "sha256:3678b2d55ccdc6dcbe11cf1ea518ab7426ab37656d94213f637bd843dc6b6ca4",
        "sha256:aeb67982a725b5d6e8a2b3114d1bc8ca4aaadb6b6797614b6831cd6703260768",
        "sha256:06954f8169fdab8e97f3e61ee1090df58c3362b8a4d00160d3da53ef6577b131"
    ],
    "Env": [
        "PATH=/usr/local/apache2/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
        "HTTPD_PREFIX=/usr/local/apache2",
        "HTTPD_VERSION=2.4.48",
        "HTTPD_SHA256=1bc826e7b2e88108c7e4bf43c026636f77a41d849cfb667aa7b5c0b86dbf966c",
        "HTTPD_PATCHES="
    ]
}

おわりに

とりあえずOpenShift環境以外の一般的なDocker環境での基本的な考え方をおさらいでした。OpenShiftでの操作は複雑なので混乱した時に立ち返る場所として整理しておきました。
次回から本題に入っていきます。