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"';
ブロック付きディレクティブ
ディレクティブには有効範囲(コンテキスト)があり、http
やserver
ディレクティブのブロックで指定されたコンテキストや、どのコンテキストにも所属しない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種類あり、イメージ名から判断できる。
user名前空間
tanaka/apache-php
というように、文字列と / でプレフィックスされた名前は、'user'の名前空間に属している。
これらのイメージはユーザによってDocker hub上にアップロードされたもの。root名前空間
プレフィックスや / を持たない名前(debianやphpなど)は、'root' の名前空間に属している。 この名前空間はDocker Inc. が管理しており、Docker hubから入手できる、広く使われているソフトウェアやディストリビューションの公式イメージのために予約されている。サードパーティ名前空間
ホスト名もしくはIPがプレフィックスになっているイメージは、サードパーティのレジストリがホストしているイメージ。(gcr.ioなど)
この名前空間の構成によって、ユーザはイメージがどこから来たものなのか、混乱せずに済む。
イメージを公開する
今回は、この記事で作った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は最上位の読み書き可能なレイヤだけ見れば、実行中のファイルシステムに対して行われた変更を見つけることができる。