webpackでTypeScriptチュートリアル環境を構築

TypeScriptをwebpackと組み合わせて動かしてみたいので、webpack + TypeScriptの環境を構築する。
流れとしては、最初は純粋にTypeScriptコンパイラを使ってtsファイルをコンパイルして動作させる環境を作り、その後でwebpackを使った環境を作る。

TypeScriptコンパイラを使った環境構築

Node.jsをインストール

公式サイト からインストーラを入手。 もしくは、nodenvを使ってインストール。

$ nodenv version
12.18.3

yarnをインストール

$ brew install yarn
$ yarn -v
1.17.3

TypeScriptをインストール

プロジェクトフォルダを作成して、TypeScriptをローカルインストール。

$ mkdir tutorial && cd tutorial
$ yarn init -y
$ yarn add typescript

以降はtutorialフォルダ下でコマンドを実行する。

TypeScript 動作確認

tsファイルを作成し、コンパイルして動かす。

$ vi index.ts

const message:string = 'Hello! TypeScript!';
console.log(message);

$ vi package.json

{
  "name": "tutorial",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "dev": "tsc"  // 追加
  },
  "dependencies": {
    "typescript": "^4.0.3"
  }
}
$ yarn dev index.ts  // コンパイル
$ node index.ts  // Hello! TypeScript!

webpackを使った環境構築

ビルドに必要なモジュールをインストール

$ yarn add -D webpack webpack-cli webpack-dev-server ts-loader --save-dev

TypeScriptコンパイラの設定ファイルを作成

設定ファイルを作成し、必要な設定を入れる。

$ ./node_modules/typescript/bin/tsc --init
$ vi tsconfig.js
{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    /* コンパイルに使用する組み込みライブラリを指定する。このオプションが設定されない場合は、targetの値に応じたデフォルトライブラリが使われる。 https://www.typescriptlang.org/docs/handbook/compiler-options.html */
    // "lib": [],
    /* ビルドファイルにコメントを含めない */
    "removeComments": true,
    /* コンパイルエラーがあれば、ビルドファイルを出力しない */
    "noEmitOnError": true,
    /* すべての厳密なタイプチェックオプションを有効にする。*/
    "strict": true,
    /* 未使用のローカル変数が存在する場合にエラーを発生させる */
    "noUnusedLocals": true,
    /* 未使用のパラメータが存在する場合にエラーを発生させる */
    "noUnusedParameters": true,
    /* 関数内のすべてのコードパスが値を返さない場合にエラーを発生させる */
    "noImplicitReturns": true,
    /* switch文のフォールスルーケースが存在する場合にエラーを発生させる。(フォールスルーケース:switch文のcase内でbreakが無い場合に、その下のcaseの処理も実行されること) */
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "exclude": [
    // デフォルトで node_modules/ は除外されるため、この設定はしなくてよい。挙動の説明のために書いているだけ。
    "./node_modules"
  ],
  "include": [
    // ./src 配下のうち、excludeオプションに含まれるものを除いたtsファイルのみコンパイル
    "./src/**/*"
  ]
}

webpackでビルドするための設定

$ vi webpack.config.js

const path = require('path');

module.exports = {
    // モード値を production に設定すると最適化された状態で、
    // development に設定するとソースマップ有効でJSファイルが出力される
    mode: 'development', // "production" | "development" | "none"

    // メインとなるJavaScriptファイル(エントリーポイント)
    entry: './src/index.ts',

    output: {
        path: path.join(__dirname, "dist"),
        filename: "index.js",
        publicPath: "/dist"
    },

    module: {
        rules: [{
            // 拡張子 .ts の場合
            test: /\.ts$/,
            // TypeScript をコンパイルする
            use: 'ts-loader'
        }]
    },
    // import 文で .ts ファイルを解決するため
    resolve: {
        modules: [
            "node_modules", // node_modules 内も対象とする
        ],
        extensions: [
            '.ts',
            '.js' // node_modulesのライブラリ読み込みに必要
        ]
    },
    devServer: {
        contentBase: './',    // 公開するリソースのドキュメントルート
    }
};

$ vi package.json

  "scripts": {
    "dev": "webpack-dev-server --config webpack.config.js",  // 開発環境用ビルドコマンドを追加
    "build": "webpack --config webpack.config.js --mode production"  // 本番環境用ビルドコマンドを追加
  },

実行ファイルを作成

$ vi ./src/index.ts

const message:string = 'Hello! TypeScript!';
console.log(message);

$ vi inde.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
Hello! TypeScript!
<script src="./dist/index.js"></script>  // ビルドしたjsファイルを読み込み
</body>
</html>

ビルド・動作確認

開発環境用にビルド
$ yarn dev
webpack-dev-serverが動作しているアドレス http://localhost:8080/ にアクセス。すると、index.html の内容が表示されてコンソールにHello! TypeScript!が表示され、動作していることが確認できる。
また、webpack-dev-serverのホットリロードが有効になっているので、index.ts を編集するとブラウザが自動でリロードされる。

本番環境では、
$ yarn build
を実行し、ビルドファイル ./dist/index.js を作成することで、index.html絶対パスでブラウザアクセスすると、同じように動作することが確認できる。

DIとDIコンテナ、Laravelでの適用

まずはDIとDIコンテナについて。

DI(Dependency injection)とは

  • あるオブジェクトが依存している他のオブジェクトを受け取るデザインパターン
  • DIを使うことで、クライアントはオブジェクトがどう作られるのか、どのクラスが具象化されるのかを気にせずに済む。

DIを使わない例

class Client
{
    /**
     * @var ServiceA
     */
    private $serviceA;
    /**
     * @var ServiceB
     */
    private $serviceB;

    public function __construct()
    {
        // ServiceA、ServiceBの生成方法を知らなければならない
        $this->serviceA = new ServiceA();
        $this->serviceB = new ServiceB();
    }

    public function doSomething(): void
    {
        $this->serviceA->doSomethingA();
        $this->serviceB->doSomethingB();
    }
}

$client = new Client();
$client->doSomething();

Clientクラスは依存するServiceA, ServiceB を自身で生成している状態で、以下のような問題が生じる。

  • 別のサービスに置き換えたい時に、コンストラクタを修正しなければならない
  • ServiceA, もしくはServiceBの生成方法が複雑になった時、コンストラクタも修正しなければならない
  • テストを書く時にモックに差し替えられない

DIを使った例

class Client
{
    /**
     * @var ServiceAInterface
     */
    private $serviceA;
    /**
     * @var ServiceBInterface
     */
    private $serviceB;

    // 渡されたServiceA、ServiceBの具象クラスを使用する。
    public function __construct(ServiceAInterface $serviceA, ServiceBInterface $serviceB)
    {
        $this->serviceA = $serviceA;
        $this->serviceB = $serviceB;
    }

    public function doSomething(): void
    {
        $this->serviceA->doSomethingA();
        $this->serviceB->doSomethingB();
    }
}

$client = new Client(new ServiceA(), new ServiceB());
$client->doSomething();

利用者側で具体的なServiceA、ServiceBクラスを生成して渡すようにしているため、Clientクラスは抽象に依存するだけで済み、上記の問題を解消できた。
しかし、このような手動DIは利用するたびに具象クラスを生成しているため、依存箇所が増えメンテが大変になりスケールしない。
この問題を解決するのがDIコンテナ。

DIコンテナとは

  • DIの機能を提供するフレームワーク
  • 具象クラスの生成をDIコンテナ側の1箇所に集約することで、DIのメンテをしやすくする。

DIコンテナを使った例

シンプルなDIフレームワークであるPimpleをDIコンテナとして使用。

use Pimple\Container;

$container = new Container();

// Clientクラスの生成方法を定義
$container['service_a'] = function ($c) {
    return new ServiceA();
};
$container['service_b'] = function ($c) {
    return new ServiceB();
};
$container['client'] = function ($c) {
    return new Client($c['service_a'], $c['service_b']);
};

$client = $container['client'];  // Clientクラスの利用者側は生成方法を気にせず使える
$client->doSomething();

DIコンテナを使えば、利用者側が使いたいクラスの生成方法を意識せずに済む。

LaravelではDIをどうやるか

LaravelではサービスコンテナがDIコンテナに当たり、Illiminate\Foundation\Application クラスで実装されていて、コンテナへの登録は、サービスプロバイダの中で行う。

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        $this->app->bind(ServiceAInterface::class, ServiceA::class);
        $this->app->bind(ServiceBInterface::class, ServiceB::class);
        $this->app->bind(
            Client::class,
            function ($app) {
                return new Client($app->make(ServiceAInterface::class), $app->make(ServiceBInterface::class));
            }
        );
    }
}

$client = App::make(Client::class);  // サービスコンテナに登録されたClientインスタンスを返す
$client->doSomething();

App::make(Client::class) でDIコンテナにて登録方法を定義したClientインスタンスを取得できる。
これがLaravelでDIコンテナを使う分かりやすい例だが、Laravelの場合はControllerやCommandクラスのオブジェクトがサービスコンテナにより生成されるため、サービスコンテナを明示的に使ってインスタンスを取得するよりも、タイプヒントを指定することで取得するケースが多い。

class ClientController extends Illuminate\Routing\Controller
{
    private $client;

    public function __construct(Client $client)  // Clientクラスがサービスコンテナにより自動で解決されてインスタンスが渡される
    {
        $this->client = $client;
    }

    public function doSomething()
    {
        $this->client->doSomething();
    }
}

サービスプロバイダで依存関係を登録しておけば、使う側はタイプヒントで指定するだけで済むため非常に楽になる。

nginxで静的コンテンツを表示する

目的

nginxのDockerコンテナを使って静的コンテンツを表示するための基本的な設定を抑える。
使用するnginxのバージョンは1.13.12。

デフォルトの設定

nginx:1.13-alpine のDockerイメージでは、/etc/nginx/nginx.conf/etc/nginx/conf.d/default.conf がデフォルトの設定として使われている。

nginx.conf

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}

default.conf

server {
    listen       80;
    server_name  localhost;
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

公開ディレクトリが /usr/share/nginx/html に設定されていて、 /test.html でリクエストすると /usr/share/nginx/html/test.html ファイルが表示されるようになっている。

ディレクティブの解説

ディレクティブとは

設定ファイルのそれぞれの項目はディレクティブと呼ばれ、ディレクティブ名とパラメータはスペースまたはタブ文字で区切り、最後にセミコロンを付ける。
各行の # 以降は行末まで全てコメントとして扱われる。
ディレクティブには ; で終わるシンプルなディレクティブと、{...} でブロックをとるディレクティブがある。

シンプルなディレクティブ

  • パラメータが1つだけ
    worker_processes 1;

  • パラメータが複数 error_page 500 502 503 504 /50x.html;
    各パラメータの間は空白文字で区切る。

  • 複数パラメータを改行で区切って指定可能

log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

ブロック付きディレクティブ

ディレクティブには有効範囲(コンテキスト)があり、httpserverディレクティブのブロックで指定されたコンテキストや、どのコンテキストにも所属しないmainコンテキストがある。mainコンテキストで指定された値はブロック内でも有効だが、ブロック内で指定されると値は上書きされる。

root   /usr/share/nginx/html;  # ①
server { # A
    server_name  a.example;
    root   /usr/share/nginx/html/a; # ②
}
server { # B
    server_name  b.example;
}

ここでは2つのrootディレクティブ(①, ②)が定義されている。①はA, Bどちらのブロックにも含まれていないため、どのコンテキストでも有効。
しかし、Aのブロックには別のrootディレクティブ②が定義されているため、Aのコンテキストでは②が優先される。
Bのブロックにはrootディレクティブが記述されていないため、①のrootディレクティブが有効。

user

ワーカプロセスの実行ユーザを指定する。デフォルトではnobodyユーザで起動する。
user ディレクティブを使用することで、特定のユーザでワーカを動作させることができる。

http

HTTPサーバに関連する設定を記述するにはhttpディレクティブを用いてhttpコンテキストを定義する。
nginx本体に関する設定を除き、ほとんどのHTTPサーバの動作に関連する設定は、このhttpディレクティブのブロック内に記述する。

include

共通設定を使用する場合や複数のサーバを設定する場合、設定ファイルを複数に分割することで管理しやすくなります。includeディレクティブを用いることで、複数に分割した設定ファイルを読み込んで使用できる。
パラメータにはファイル名またはファイルマスクを指定できる。(include /etc/nginx/conf.d/*.conf など)
ファイルは絶対パスあるいはnginx.confが配置されているパスからの相対パスで指定できる。

server

nginxでは使用するIPアドレス、ポート、ホスト名ごとに別々の設定を持つ複数のHTTPサーバを動作させることができ、これらはバーチャルサーバと呼ばれる。
バーチャルサーバは、それぞれ別々のHTTPサーバであるかのように動作し、それぞれ独立した設定を持っている。
バーチャルサーバはserverディレクティブで定義し、ブロック内に記述した設定がバーチャルサーバの設定として扱われる。

複数のバーチャルサーバを定義している場合、nginxは次の順番でどのバーチャルサーバが使用されるか決定します。
1. listenディレクティブのアドレスとポートに一致するバーチャルサーバを検索する
2. リクエストのHostヘッダがserver_nameディレクティブで指定したホストに一致したバーチャルサーバにリクエストを振り分ける
3. どのサーバにも一致しない場合デフォルトサーバにリクエストを振り分ける

ホスト名の一致は、完全一致、ワイルドカード正規表現の順番に評価されます。
どのバーチャルサーバにも一致しない場合はデフォルトサーバにリクエストが振り分けられます。
デフォルトサーバは設定ファイルの一番上に記述したバーチャルサーバが使用されますが、listenディレクティブにdefault_serverパラメータを使用することで明示的に指定できる。

server { # ①
    listen       80 default_server;
}
server { # ②
    listen       80;
    server_name  www.example.com;
}

上の場合、www.example.com 以外へのリクエストは①のサーバコンテキストにマッチする。

listen

バーチャルサーバが使用するアドレス、ポートはlistenディレクティブで指定する。
listenディレクティブではアドレス、ポートまたはUNIXドメインソケットファイルを指定できる。

listen *:8080;   #すべてのアドレスの8080番ポート
listen 8080;     #*:8080と同じ
listen 127.0.0.1:8080;    #ローカルアドレスの8080番ポート
listen localhost:8080;   #ホスト名で指定することもできる
listen unix:/var/run/nginx.sock;      #UNIXドメインソケットを指定

server_name

バーチャルサーバで使用するホスト名を指定するにはserver_nameディレクティブを使用する。
ホスト名を指定することで、80番ポートなど同じポート、アドレスで動作する複数のサーバを定義できる。

server_name example.com *.example.com;

server_nameディレクティブには複数のホスト名を指定できます。また、ホスト名の指定にはワイルドカードも使用できます。

server_name ~img\d+\.example\.com$;

ホスト名の指定には正規表現を使用することもできる。正規表現を使用する場合はチルダ(~)を先頭に付けて指定する。

root

公開するディレクトリを指定する。 nginxではrootディレクティブで指定したディレクトリのパスがそのURIのルートにマッピングされる。

root/var/www/html;

上の設定で http://www.example.com/images/example.png にリクエストされた場合、URIにおける絶対パス/images/example.png
ルートディレクトリは /var/www/html なので、ファイルシステム上で参照されるファイルは /var/www/html/images/example.png になる。
このようにファイルシステム上の絶対パスは、URIにおける絶対パスの前方にrootディレクティブに指定されたパスを結合したものになる。

nginxのDockerコンテナを使って静的コンテンツを表示する

Dockerfile

FROM nginx:1.13-alpine
COPY nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p /var/www/html \
 && touch /var/www/html/index.html \
 && echo 'Hello' > /var/www/html/index.html

nginx.conf

user  nginx;
worker_processes  1;
error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;
events {
    worker_connections  1024;
}
http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    server {
        listen 80;
        server_name www.nginx-example;
        root /var/www/html;
    }
}

Dockerfileをビルドし、nginxコンテナを8080番ポートで立ち上げる。

docker build -t $(REPOSITORY):$(VERSION) -f $(PWD)/Dockerfile $(PWD)
docker run -p 8080:80 $(REPOSITORY):$(VERSION)

/test.html でアクセスすると、Hello が表示される!

Docker DesktopでKubernetesを使ってnginxを動作させる

環境

Docker Desktop fo Mac: 2.3.0.2
Docker Engine: 19.03.8
Kubernetes: v1.16.5

目的

Docker DesktopでKubernetesを使ってnginxコンテナを動かせるようになる。

流れ

  • Docker DesktopでKubernetesを有効化する
  • Podの作成
  • Serviceの作成
  • 作成したnginxにアクセス

Docker DesktopでKubernetesを有効化する

Docker DesktopでKubernetesを有効にすると、Kubernetesクラスタとマスターノードが構築される。

// クラスタ上の全てのノードを表示
$ kubectl get nodes
NAME             STATUS   ROLES    AGE   VERSION
docker-desktop   Ready    master   97s   v1.16.6-beta.0

Podの作成

nginxコンテナ1つを含むPodを作成。

$ kubectl run my-nginx --image nginx:latest
kubectl run --generator=deployment/apps.v1 is DEPRECATED and will be removed in a future version. Use kubectl run --generator=run-pod/v1 or kubectl create instead.
deployment.apps/my-nginx created

my-nginxというDepoloymentリソースが作成される。 通常はマニフェストファイルでPodを作成するが、kubectl run でも作成できる。

// 作成されたDeploymentを確認
$ kubectl get deployment
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
my-nginx   1/1     1            1           2m36s

// 作成されたPodを確認
$ kubectl get pod
NAME                        READY   STATUS    RESTARTS   AGE
my-nginx-667764d77b-8krjp   1/1     Running   0          3m25s

// 作成されたコンテナを確認。
$ docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS               NAMES
5d69c57dbef0        nginx               "/docker-entrypoint.…"   3 minutes ago       Up 3 minutes                            k8s_my-nginx_my-nginx-667764d77b-8krjp_default_56654cce-42f3-4314-a1e8-cad388f85a40_0

Serviceの作成

Podに対してクラスタ外部からアクセスするためにはServiceを作成する必要がある。

$ kubectl expose deployment my-nginx --port 80 --type NodePort
service/my-nginx exposed

deploymentにmy-nginx、ポート番号に80、--typeオプションにNodePortを指定している。 Serviceの一覧を見ると、my-nginxが作成され、ポート番号31916でアクセスできることが分かる。

$ kubectl get service
NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1      <none>        443/TCP        25m
my-nginx     NodePort    10.103.77.36   <none>        80:31916/TCP   5s

作成したnginxにアクセス

http://localhost:31916 にアクセスするとnginxのホーム画面が表示される!

今回作ったリソースが不要になったらDeployment, Serviceを削除する。

$ kubectl delete deployment my-nginx
deployment.apps "my-nginx" deleted
$ kubectl delete service my-nginx
service "my-nginx" deleted

dockerイメージを公開する

目的

Dockerイメージの公開方法と、その周辺用語を理解する

用語の整理

  • レジストリ
    イメージのホスティングと配布を受け持つサービス。 デフォルトのレジストリはDocker hub。プライベートレジストリとしてGCPのContainer Registryなどがある。

  • リポジトリ
    関連するイメージの集合(通常は、同じアプリケーションもしくはサービスの様々なバージョンを提供する)

  • タグ
    リポジトリ内のイメージに与えられる識別子(7.4.6やlatestなど)

docker pull tanaka/apache-php:latest
というコマンドは、レジストリであるDocker hubのtanaka/apache-phpリポジトリから、latestというタグのついたイメージを取得している。

イメージの名前空間

Dockerイメージが属する名前空間には3種類あり、イメージ名から判断できる。

この名前空間の構成によって、ユーザはイメージがどこから来たものなのか、混乱せずに済む。

イメージを公開する

今回は、この記事で作ったDockerイメージを公開する。
公開するにはレジストリの認証が必要なので、Docker hubの場合は docker login コマンドで事前にログインを行う。

$docker login

認証に成功したら公開するために docker push コマンドを実行する。
docker push [オプション] 名前[:タグ]

$ docker push repo/cowsay-fortune
The push refers to repository [docker.io/repo/cowsay-fortune]
304429cbf295: Pushed 
10782bf6eac8: Pushed 
8c02234b8605: Mounted from library/debian 
latest: digest: sha256:8fd21c04afa00c95d392afea5deec89702c492f2974518a6abe681bcc912d2ad size: 948

ここでは、リポジトリ名の後にタグを指定していないので、自動的にlatestというタグが割り当てられる。 アップロードが完了したら docker pull コマンドでイメージをダウンロードできる。

docker pull [オプション] 名前[:タグ] | [レジストリ・ホスト[:レジストリ・ポート]/]名前[:タグ]
手元のイメージを削除して pull してみる。

$docker pull repo/cowsay-fortune
Using default tag: latest
latest: Pulling from repo/cowsay-fortune
376057ac6fa1: Already exists 
99b463bdca15: Pull complete 
2588e1ff2246: Pull complete 
Digest: sha256:8fd21c04afa00c95d392afea5deec89702c492f2974518a6abe681bcc912d2ad
Status: Downloaded newer image for repo/cowsay-fortune:latest
docker.io/repo/cowsay-fortune:latest

Dockerイメージを構築する(Dockerfileを使う方法と使わない方法を比較しながら)

目的

イメージを作るのにDockerfileを使う方法が一般的だが、今回はDockerfileを使う方法と使わない方法を試して比較することで、Dockerfileの有用性を理解する。

今回構築するイメージは、歴史上の偉人や有名人の格言を牛がつぶやくアスキーアートを出力するプログラム。

______________________
< You are always busy. >
 ----------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

格言を出力するfortuneと、出力を牛がつぶやくアスキーアートに変換するcowsayを組み合わせて作る。

Dockerfileを使わずにイメージ構築

まずはDockerfileを使わずにイメージを作る。流れとしてはこんな感じ。

  • ベースイメージからコンテナを起動する
  • コンテナに入って変更を加える
  • コンテナの内容を新たなイメージとして保存する
$ docker run -it --name cowsay -h cowsay debian bash
$ apt-get update && apt-get install -y cowsay fortune
# /usr/games/fortune | /usr/games/cowsay
 _________________________________________
/ You will be recognized and honored as a \
\ community leader.                       /
 -----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

上記のコマンドでコンテナができたので、そのコンテナの内容をdocker commitで新しいイメージに焼く。 コンテナ名(cowsay)、リポジトリ名(repo)、イメージ名(cowsay-fortune)を指定する。

docker commit [オプション] コンテナ [リポジトリ[:タグ]]

$ docker commit cowsay repo/cowsay-fortune
sha256:bf300800043047c8566b9221344f3414bd45dcd6c4949ea14f752ae2fc999e57
$ docker images
REPOSITORY                   TAG                   IMAGE ID            CREATED             SIZE
repo/cowsay-fortune   latest                bf3008000430        4 seconds ago       180MB

これで作ったイメージを使って、手軽に何回でもcowsayを実行できるようになった!

$ docker run repo/cowsay-fortune /usr/games/cowsay "Moo"
 _____
< Moo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

ただ、この方法だと変更を加えようとするともう一度同じ手順を繰り返さないといけないし、繰り返し行う場合にその手順を覚えておいておかなければならない。
Dockerfileはイメージを自動構築することによって、この問題を解決する。

Dockerfileを使ってイメージ構築

Dockerfileにはイメージを生成するための一連の操作を記述し、それをdocker buildコマンドで構築する。

# 使用するベースイメージ
FROM debian:latest

# イメージ内で実行するシェルコマンド
RUN apt-get update && apt-get install -y cowsay fortune

# docker runに渡す引数(実行ファイル)を指定
ENTRYPOINT ["/usr/games/cowsay"]

上記の内容をDockerfileに記述しdocker build コマンドを実行すれば、イメージを構築できる。

docker build [オプション] パス | URL | -

イメージ名とタグ(repo/cowsay-fortune2:latest)、使用するDockerfileのパス(./Dockerfile)、ビルドコンテキスト(.)を指定。
*ビルドコンテキスト: Dockerfile内の命令(ADDやCOPYなど)から参照できるファイルやディレクトリの集合。ここでは、カレントディレクトリ以下にある全てのファイルとディレクトリで構成され、ビルドプロセスの一部としてDockerデーモンに送られる。
ビルドコンテキストから不要なファイルを除くには .dockerignore ファイルを使う。

$ docker build -t repo/cowsay-fortune2 -f ./Dockerfile .
Sending build context to Docker daemon  2.048kB
Step 1/3 : FROM debian:latest
 ---> 5971ee6076a0
Step 2/3 : RUN apt-get update && apt-get install -y cowsay fortune
 ---> Running in c5448328bf90
Get:1 http://deb.debian.org/debian buster InRelease [121 kB]
[中略]
Successfully built 479c6b5f70dc
Successfully tagged repo/cowsay-fortune2:latest
$ docker images 
REPOSITORY                    TAG                   IMAGE ID            CREATED             SIZE
repo/cowsay-fortune2   latest                479c6b5f70dc        6 seconds ago       180MB

このイメージを使って、cowsayを実行する。

$ docker run repo/cowsay-fortune2 "Moo"
 _____
< Moo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

これでもし変更が入ってもDockerfileの内容を変更して、buildするだけで良くなった!
イメージの構築手順はDockerfileに記述されているので、覚えておく必要もない。
イメージを作るのにDockerfileが欠かせないのが分かりますね。

最後にcowsayにfortuneコマンドの結果を渡せるように改良しよう。
ENTRYPOINTに独自に作成したスクリプトを渡せば修正できる。これはDockerfileを作成するときの一般的なパターンらしい。
まず、cowsayに渡す引数がなければfortuneの結果を、引数があればそのままcowsayに渡すシェルスクリプトを作成し、実行権限を付与しておく。

#! /bin/bash
if [ $# -eq 0 ];then
    /usr/games/fortune | /usr/games/cowsay
  else
    /usr/games/cowsay "$@"
fi

これをENTRYPOINTで実行するようにDockerfileを変更する。

FROM debian:latest
RUN apt-get update && apt-get install -y cowsay fortune

# ファイルをホストからイメージのファイルシステムにコピー
COPY entrypoint.sh /

ENTRYPOINT ["/entrypoint.sh"]

これでイメージを再構築し、コンテナを起動させる。

$ docker build -t repo/cowsay-fortune3:latest -f ./Dockerfile .
Sending build context to Docker daemon  3.584kB
Step 1/4 : FROM debian:latest
 ---> 5971ee6076a0
[中略]
$ docker run repo/cowsay-fortune3 
 ________________________________________
/ Your best consolation is the hope that \
| the things you failed to get weren't   |
\ really worth having.                   /
 ----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
$ docker run repo/cowsay-fortune3  "Moo"
 _____
< Moo >
 -----
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Dockerコンテナの基本的な起動、操作

今の職場がkubernetesでproduction環境を構築していて、アプリケーション開発の基盤をこれから触るにあたってDockerの周辺知識が必須なので、基本的なことからまとめながら理解していく。

コンテナの起動

以下のコマンドではDebianのコンテナを起動してHello Worldを出力している。

$ docker run debian echo "Hello World"
Unable to find image 'debian:latest' locally
latest: Pulling from library/debian
376057ac6fa1: Pull complete 
Digest: sha256:4ab3309ba955211d1db92f405be609942b595a720de789286376f030502ffd6f
Status: Downloaded newer image for debian:latest
Hello World

ここで起きたことを追っていく。 docker runは指定したイメージのコンテナを起動し、指定されたコマンドを実行するコマンド。

docker run [オプション] イメージ [コマンド] [引数...]

ここで指定しているイメージは最新バージョンのDebianイメージ、コマンドはecho、引数は"Hello World"となる。

Unable to find image 'debian:latest' locally
latest: Pulling from library/debian
376057ac6fa1: Pull complete 
Digest: sha256:4ab3309ba955211d1db92f405be609942b595a720de789286376f030502ffd6f
Status: Downloaded newer image for debian:latest

最新バージョンのDebianイメージのローカルコピーがないため、DockerはDocker Hubをオンラインでチェックし最新バージョンのDebianイメージをダウンロードする。

Hello World

ダウンロードが完了すると、コンテナを起動しその中で指定されたechoコマンドを実行する。

再度同じコマンドを実行すると、今度はイメージのダウンロードなしですぐにコンテナが起動される。

$ docker run debian echo "Hello World"
Hello World

コンテナの一覧・削除

先ほど作ったコンテナを一覧で見てみる。

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED              STATUS                          PORTS               NAMES
91f05417bc1b        debian              "echo 'Hello World'"   About a minute ago   Exited (0) About a minute ago                       angry_leakey
e711e9698233        debian              "echo 'Hello World'"   15 minutes ago       Exited (0) 15 minutes ago                           sad_hermann

同じイメージだが、コマンドを打った分コンテナが作られていて、statusがexitedとありコンテナは停止している。

今度はコンテナ内でシェルを使えるように起動してみる。

$ docker run -it debian /bin/bash
root@ead650c481f3:/# echo "Hello World"
Hello World
root@ead650c481f3:/# 

-itオプションでコンテナのSTDINにアタッチして擬似ターミナルを割り当てている。 /bin/bash コマンドでbashシェルが立ち上がる。 シェルを終了すると、コンテナも停止する。コンテナが動作するのは、そのコンテナのメインプロセスが動作している間だけ。

ここまでで作ったコンテナは不要なので削除する。

docker rm [オプション] コンテナ [コンテナ...]

コンテナ名もしくはコンテナIDを指定して削除できる

$ docker ps -a
CONTAINER ID        IMAGE               COMMAND                CREATED             STATUS                       PORTS               NAMES
ead650c481f3        debian              "/bin/bash"            14 minutes ago      Exited (130) 2 seconds ago                       mystifying_elbakyan
9ff7a951dfdc        debian              "echo 'Hello World'"   22 minutes ago      Exited (0) 22 minutes ago                        quirky_tesla
91f05417bc1b        debian              "echo 'Hello World'"   24 minutes ago      Exited (0) 24 minutes ago                        angry_leakey
e711e9698233        debian              "echo 'Hello World'"   38 minutes ago      Exited (0) 38 minutes ago                        sad_hermann
$ docker rm mystifying_elbakyan
mystifying_elbakyan
$ docker rm 9ff7a951dfdc
9ff7a951dfdc

また、以下コマンドで停止した全てのコンテナを削除できる。

$ docker rm -v $(docker ps -aq -f status=exited)
91f05417bc1b
e711e9698233
$ docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES

コンテナの詳細情報

今度はコンテナにホスト名・コンテナ名をつけて起動する。

$ docker run -h test-host --name="test-name" -it debian /bin/bash
root@test-host:/# 

新しいターミナルを開いてこのコンテナの情報をdocker inspectで確認する。

docker inspect [オプション] コンテナ|イメージ|タスク [コンテナ|イメージ|タスク...]

$ docker inspect test-name
[
    {
        "Id": "a56167ee8d64950c793ded1950362a7f815e82fcbd23ae2004985a3629deb016",
        "Created": "2020-05-23T07:24:13.3995891Z",
        "Path": "/bin/bash",
        "Args": [],
        "State": {
            "Status": "running",
[省略]

詳細な情報がJSON配列で表示されるが情報量が多すぎるのでgrep か formatオプションで必要な情報をフィルタリングする。

$ docker inspect --format {{.Config.Hostname}} test-name
test-host

$ docker inspect --format {{.NetworkSettings.IPAddress}} test-name
172.17.0.2

コンテナのログ

先ほど起動したコンテナの中をいじってみる。

/# mv /bin /basket
root@test-host:/# ls -al
bash: /bin/ls: No such file or directory

いじるというか壊しているが、コンテナ内で操作をした記録を確認することができる。 コンテナで実行したコマンドとその出力をdocker logsで見れる。

$ docker logs test-name
# mv /bin /basket
root@test-host:/# ls -al
bash: /bin/ls: No such file or directory

また、docker diff でコンテナのファイルシステム上で、変更したファイルやディレクトリの一覧を見れる。

$ docker diff test-name
A /basket
A /basket/date
A /basket/mknod
[中略]
D /bin

mvコマンドによって /bin が削除され、/basket ディレクトリに追加されていることが分かる。 これらのコマンドでコンテナで起きたことを後から確認することができる。

イメージのファイルシステムはリードオンリーのレイヤとしてマウントされており、実行中のコンテナへの全ての変更は、その上にマウントされている読み書き可能なレイヤに対して行われる。 そのため、Dockerは最上位の読み書き可能なレイヤだけ見れば、実行中のファイルシステムに対して行われた変更を見つけることができる。