Windows Server 2019でWindowsコンテナを動かすまでのチュートリアル

## この記事の目的

Windows Server 2019環境で、とにかくてっとり早くdockerコンテナを使えるようにするための手順をお伝えします。

MSDNの公式ページにもチュートリアルがあり、こちらも併せて参照いただくと理解が深まるかと思います。[^1]
https://docs.microsoft.com/ja-jp/virtualization/windowscontainers/quick-start/quick-start-windows-server

[^1]: 記事を書いた後で気づきましたが、前に閲覧したときよりかなり充実していて、本記事要らないかもとも思いました。。


## Windows Server 2019のインストール～dockerが使えるまでの手順

### インストール手順

1. Windows Server 2019をインストール (下記はすべてデスクトップエクスペリエンスで動作を確認）
2. 管理者モードでPowerShell起動 (以降すべてのPowerShellコマンドは管理者モードで実行）
3. コンテナ機能の有効化

    ```Powershell
    Install-WindowsFeature containers
    ```
4. サーバーの再起動

    ```Powershell
    Restart-Computer -Force
    ```

5. Dockerのインストール

    ```Powershell
    # パッケージ管理のプロバイダを追加
    Install-Module -Name DockerMsftProvider -Repository PSGallery -Force
    # dockerをインストール
    Install-Package -Name docker -ProviderName DockerMsftProvider
    ```

6. サーバーの再起動

    ```Powershell
    Restart-Computer -Force
    ```

7. Proxyの設定

    ```PowerShell
    setx HTTP_PROXY http://proxy.example.com:8888/ /M
    setx HTTPS_PROXY http://proxy.example.com:8888/ /M
    ```

### 動作確認

1. dockerのバージョン確認コマンドを実行

    ```Powershell
    docker version
    ````

2. 下記のように結果が返ってくればOK

    ```cmd
    Client: Docker Engine - Enterprise
     Version:           18.09.5
     API version:       1.39
     Go version:        go1.10.8
     Git commit:        be4553c277
     Built:             04/11/2019 06:44:52
     OS/Arch:           windows/amd64
     Experimental:      false
    
    Server: Docker Engine - Enterprise
     Engine:
      Version:          18.09.5
      API version:      1.39 (minimum version 1.24)
      Go version:       go1.10.8
      Git commit:       be4553c277
      Built:            04/11/2019 06:43:04
      OS/Arch:          windows/amd64
      Experimental:     false
    ```

3. 使ってみよう

    ```Powershell
    docker container run hello-world:nanoserver
    ````

    hello-world:nanoserverという名前のイメージをコンテナとして起動する命令です。

    ただし、初回は当該の環境にhello-world:nanoserverという名前のイメージがまだ無いので、最初にDocker Hubからのダウンロードが行われます。

    ```
Unable to find image 'hello-world:nanoserver' locally
nanoserver: Pulling from library/hello-world
b22999bfb02f: Pull complete
c8353a1aacae: Pull complete
8c51178a6c10: Pull complete
Digest: sha256:347e0c5e55751d2ee2470f1a57463c26e81515583838286b67ae7d140d0480da
Status: Downloaded newer image for hello-world:nanoserver
    ```

    上記の後、コンテナが起動し、コンソールに下記の文字列を返します。

    ```cmd
    Hello from Docker!
    This message shows that your installation appears to be working correctly.
    
    To generate this message, Docker took the following steps:
     1. The Docker client contacted the Docker daemon.
     2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
        (windows-amd64, nanoserver-1809)
     3. The Docker daemon created a new container from that image which runs the
        executable that produces the output you are currently reading.
     4. The Docker daemon streamed that output to the Docker client, which sent it
        to your terminal.
    
    To try something more ambitious, you can run a Windows Server container with:
     PS C:\> docker run -it mcr.microsoft.com/windows/servercore powershell
    
    Share images, automate workflows, and more with a free Docker ID:
     https://hub.docker.com/
    
    For more examples and ideas, visit:
     https://docs.docker.com/get-started/
    ```

#### ちょっと解説
- コンテナのライフサイクル

    上記のDockerイメージは、起動すると文字列を上記のようにコンソールに印字するプロセスが立ち上がり、このプロセスが終わるとコンテナそのものも停止します。Dockerでは、コンテナ化したときにコンテナ内で開始されるプロセスとライフサイクルを共にしています。

- イメージの取得
    上記では、イメージのダウンロードが暗黙的に実行されましたが、`docker image pull`コマンドを実行すると、イメージのダウンロードのみを実行することも可能です。

    イメージは、Docker Hub上のものだけではなく、これらのイメージをベースにして自分で独自のイメージを作成することができます。この作業をBuildと呼びます。（作り方は後述）

- イメージの名前

    イメージの名前には命名規則があります。先ほどの例では、`hello-world:nanoserver`という名前でしたが、これは`イメージ名:タグ名`という2つの要素で一意にイメージを特定しています。
    タグの役割は、イメージのバージョンを明示したり、ベースとなるOSを区別する目的などで使われています。

- LinuxコンテナとWindowsコンテナ

    コンテナ技術は、OSの仮想化と表現されます。1つしかないOSを、まるで独立した複数のOSが存在しているかのように振舞わせることで分離された環境でコンテナのプロセスを立ち上げています。
    このとき、コンテナ上のカーネルのプロセスはホスト側となるOSのカーネルを共有します。このため、Windows Serverをコンテナのホストとした場合、基本的にはWindows Serverのイメージしかコンテナとして実行することができません。加えて、Windows Server 2019[^1]は、OSのメジャー・マイナー・ビルドの3つのバージョンまでがホスト側と一致している必要があります（[参照:コンテナのバージョンの互換性](https://docs.microsoft.com/ja-jp/virtualization/windowscontainers/deploy-containers/version-compatibility)）

[^1]: Windows Server 2016では、メジャー・マイナー・ビルド・リビジョンの4つのバージョンがホストと一致している必要があり、かつリビジョンは、Windows UpdateなどのHotfixを適用するたびにインクリメントされるため、ホストとの一致を保つのが難しく、このためWindows Server 2016でのコンテナの実運用はかなり限定的になります。

### 自分のイメージを作ってみよう

#### 一般的なイメージの作成について
開発したアプリケーションおよび依存モジュールをまとめて一つのDockerイメージを作成することができます。
アプリケーションおよび依存モジュールを一つにパッキングできるため、アプリケーションはコンテナのホスト側の環境に依存せずに動作することができます。

イメージをビルドするためにはDockerfileというdockerに対するイメージ作成の指示書のようなものを作成します。

概ね次のような手順でDockerfileを記述していきます。

1. 空のテキストファイルを作成しDockerfileという名前で保存。
2. ベースとなるイメージを決定する。
    補足）Windows Server 2019をコンテナホストにする場合、純粋にOSのみをベースにしたいときは下記が選択肢になります。
    - Windows Server 2019 Server Coreイメージ (mcr.microsoft.com/windows/servercore:ltsc2019)
    - Windows Server 2019 Nano Serverイメージ (mcr.microsoft.com/windows/nanoserver:1809)
3. ベースとなるイメージに、アプリケーションやその依存モジュールなどを配置。
4. ソフトウェアのインストール、ファイルとディレクトリの作成、環境構成の作成など必要に応じて実施。

#### Dockerfileの作成

本チュートリアルでは、MSDNのドキュメント（前述）に倣って、IISで静的なページを返すイメージを作成します。

1. 空のテキストファイルを作成しDockerfileという名前で保存。
2. 同じフォルダにindex.htmlという名前のファイルを作成し、HellowWorldと記述して保存する。
3. Dockerfileに次のように記述。

    ```Dockerfile
    FROM mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019
    ADD index.html c:/inetpub/wwwroot
    ```




#### Dockerfileのビルドとコンテナ化

Dockerfileができたらビルドを行いイメージを作成します。

1. Dockerfileと同じディレクトリに移動。

2. 下記のコマンドを実行しビルド実行。

    下記の例では、`tutorial:latest`という名前でイメージが生成される。

    ```bash
    docker build -t tutorial:latest .
    ```

3. ビルドが成功したイメージをコンテナとして実行する。

    先の例と同じように、イメージ名として`tutorial`を指定して実行する。(コンテナの実行を指示する時は、どのディレクトリからでも可能）

    ```bash
    docker container run -d -p 8888:80 tutorial:latest
    ```

    上記は、コンテナホスト側のポート8888番をコンテナ側のポート80にフォワードするように指定しています。

4. ブラウザでlocalhost:8888にアクセスし、Hello Worldと表示されれば成功。
![HelloWorld_Browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296577/51b0c8bf-b22b-d2d1-106a-78e74efe9700.png)

5. コンテナホストで、`docker container ls` を実行し、コンテナが実行されていることを確認してみる。

    ```bash
C:\Users\Administrator\Desktop\tutorial>docker container ls
CONTAINER ID        IMAGE                                                                 COMMAND                   CREATED       STATUS              PORTS                    NAMES
d2527cb41c15        tutorial:latest                                                       "C:\\ServiceMonitor.e…"   11 minutes ago      Up 11 minutes       0.0.0.0:8888->80/tcp     hopeful_yalow
    ```

    このとき、上記のCONTAINER IDとNAMESは勝手に付与されます。（NAMESについては明示的に指定することも可能）

    はじめに起動させた`hello-world:nanoserver`は、コンソールに文字を打刻してすぐに停止してしまいました。しかし、上記のコンテナは、ずっと起動したままになっています。

    この理由は、コンテナ化したときに起動するプロセスが、停止しないようなプロセス(`ping /t localhost`のように)であるためです。
    今回の例では、ServiceMoniter.exeというプロセスが生存し続けているために、コンテナが起動したままになっています。


6. `docker exec`で動いているコンテナに入ってみる。

    ```cmd
    docker exec -it %実際のCONTAINER ID値% cmd
    ```

    コンテナでシェルが起動し、次のような画面が返ってくる。

    ```cmd
    Microsoft Windows [Version 10.0.17763.503]
    (c) 2018 Microsoft Corporation. All rights reserved.
    
    C:\>
    ```

    色々、打ち込んでみる。試しにdirコマンドを実行すると、コンテナのシステムドライブ(C:\)直下のファイル・ディレクトリが返ってくる。

    ```cmd
    C:\>dir
     Volume in drive C has no label.
     Volume Serial Number is 4084-DF79
    
     Directory of C:\
    
    05/16/2019  06:20 AM    <DIR>          inetpub
    09/15/2018  06:42 PM             5,510 License.txt
    05/13/2019  01:25 PM    <DIR>          Program Files
    05/13/2019  01:24 PM    <DIR>          Program Files (x86)
    05/16/2019  06:21 AM           172,328 ServiceMonitor.exe
    05/13/2019  01:26 PM    <DIR>          Users
    05/16/2019  06:20 AM    <DIR>          Windows
                   2 File(s)        177,838 bytes
                   5 Dir(s)  21,092,855,808 bytes free
    ```

    inetpub\wwwrootに移動し、index.htmlを見てみると、、あたりまえだがDockerfileで格納(ADD)したindex.htmlがちゃんと入っている。

    ```
    C:\>cd inetpub\wwwroot
    
    C:\inetpub\wwwroot>dir
     Volume in drive C has no label.
     Volume Serial Number is 4084-DF79
    
     Directory of C:\inetpub\wwwroot
    
    06/06/2019  02:39 PM    <DIR>          .
    06/06/2019  02:39 PM    <DIR>          ..
    05/16/2019  06:20 AM               703 iisstart.htm
    05/16/2019  06:20 AM            99,710 iisstart.png
    06/06/2019  02:38 PM                11 index.html
                   3 File(s)        100,424 bytes
                   2 Dir(s)  21,092,855,808 bytes free

    C:\inetpub\wwwroot>type index.html
    Hello World
    C:\inetpub\wwwroot>
    ```


7. コンテナの状態を変更してみる。
    
    `docker exec`コマンドの続きで、下記のように実行。

    ```
    C:\inetpub\wwwroot>echo %COMPUTERNAME%
    D2527CB41C15

    C:\inetpub\wwwroot>echo %COMPUTERNAME% > index.html
    ```

    この状態で、もう一度ブラウザでlocalhost:8888にアクセスしてみると、次のようにコンピュータ名が表示されるようになります。

    ![COMPUTERNAME_Browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/296577/e9786525-12c2-d276-50b6-2d5fbb568275.png)


8. コンテナを止めてみる。

    `docker exec`コマンドを一旦終了するため、exitを入力します。

    ```
    C:\inetpub\wwwroot>exit

    C:\Users\Administrator\Desktop\tutorial>
    ```
    
    ホスト側のシェルに戻ってきました。ここで`docker container stop %実際のCONTAINER ID値%`と入力すると、起動していたコンテナが停止します。（ブラウザでlocalhost:8888にアクセスし、ページが返ってこないことを確認します。）

    この後、`docker container start %実際のCONTAINER ID値%`と入力すると、停止したコンテナが再び起動します。（ブラウザでlocalhost:8888にアクセスし、再びページが表示されることを確認します。）


    この状態で、次のコマンドを実行してみます。

    ```bash
    docker container run -d -p 8889:80 tutorial:latest
    ```

    今度は、別のコンテナがイメージから新たに生成されます。ブラウザでlocalhost:8889にアクセスすると「Hello World」と表示されるはずです。先ほどindex.htmlをCOMPUTERNAMEで置き換えた変更は、当該のコンテナのみに反映されますが、イメージとしては「Hello World」のままなので、新たにコンテナを立ち上げるとこのようになります。

    ```bash
    docker container run -d -p 8890:80 tutorial:latest
    docker container run -d -p 8891:80 tutorial:latest
    docker container run -d -p 8892:80 tutorial:latest
    docker container run -d -p 8893:80 tutorial:latest
    docker container run -d -p 8894:80 tutorial:latest
    ...
    docker container run -d -p 8899:80 tutorial:latest
    ```

    調子に乗っていくつもコンテナを立ち上げることができます。上記のように実行すると10台のコンテナが立ち上がります。

    それぞれのコンテナのリソース消費量は、`docker stats %実際のCONTAINER ID%`コマンドで確認できます。

    ```
    docker stat 32755b73a12c        

    CONTAINER ID        NAME                CPU %               PRIV WORKING SET    NET I/O             BLOCK I/O
    32755b73a12c        silly_mestorf       0.00%               58.06MiB            12.7kB / 5.09kB     17MB / 5.84MB
    ```

    上記の通り、このコンテナの例では、1台あたり60MB程度のメモリーしか消費していないことがわかります。10台立ち上げても600MB程度で済みます。
    また、環境にもよりますが、上記のコマンドを実行してコンテナが起動するのに掛かる時間は1秒～数秒程度です。
    ESXiやHyper-Vで仮想マシンを立ち上げる際に必要となるリソースや起動時間と比較して、少ないメモリー量かつ高速に起動することを体感することができます。



### ここまでできた方へ


まとまった規模のシステムになると、複数のコンテナ（DB/Web/Cache/SearchEngine...etc)を組み合わせて一つのシステムを作り上げたくなります。このようなときにもDocker-Composeというツールを使うと、複数のコンテナをまとめて立ち上げたり、落としたりすることができ、コンテナ間の名前解決もよろしくしてくれますので、Docker-Composeについて慣れていくと良いかと思います。

詳しい手順は下記が参考になります。
https://docs.docker.com/compose/install/#install-compose

かなりざっくりしていますが、ざあっと、下記のコマンドでDocker-Composeが使えるようになります。(Powershell)

```Powershell
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

Invoke-WebRequest "https://github.com/docker/compose/releases/download/1.22.0/docker-compose-Windows-x86_64.exe" -UseBasicParsing -OutFile $Env:ProgramFiles\docker\docker-compose.exe

# docker Serviceの再起動
Restart-Service Docker

```