Dockerについて調べてみた


## TL; DR
この記事では、DockerとDocker Composeの使い方についてまとめました。以下の観点から話を進めていきます。
* Dockerの仮想化について
* Docker（コマンドや設定ファイル）について
* Docker Composeについて
* 環境ごとの差分について

なお、この記事で使うコードはGit Hubリポジトリにプッシュしておりますので、[ここ](https://github.com/hiseumn/docker-sample-app)からダウンロードして参考にしてください。 


## Dockerの仮想化について
Dockerは、アプリケーションをコンテナという軽量な仮想化環境で実行するためのプラットフォームです。コンテナは、アプリケーションとその依存関係を一緒にパッケージ化し、どこでも一貫して動作するように設計されています。
以下に、Dockerの主要な利点をまとめました。

### 一貫性のある環境
Dockerコンテナは、アプリケーションとそのすべての依存関係を含む環境を一つのパッケージとして提供します。これにより、開発環境、テスト環境、本番環境の間で一貫した動作を保証できます。異なる環境での「動く/動かない問題」を解決し、デプロイメントの信頼性を向上させます。

### 軽量で効率的
コンテナは、従来の仮想マシンに比べて非常に軽量です。仮想マシンは、オペレーティングシステム全体を含むため、大量のリソースを消費しますが、コンテナはホストOSのカーネルを共有し、必要最低限のリソースしか使用しません。これにより、高いパフォーマンスと効率性を実現することができます。

### スピードとアジリティ
コンテナの起動は非常に高速で、数秒以内に立ち上げることができます。これにより、開発者は迅速にテストとデプロイメントを行うことができ、アジリティを向上させます。また、コンテナ化されたアプリケーションは容易に移植可能で、異なる環境間で迅速に展開することができます。

### スケーラビリティ
アプリケーションを複数の小さなサービスに分割し、それぞれを個別のコンテナとして実行できます。これにより、個々のコンテナをスケールアップまたはスケールダウンでき、リソースの効率的な利用が可能です。つまり、Dockerはマイクロサービスアーキテクチャに最適です。

### 開発から本番へのスムーズな移行
Dockerイメージは一度ビルドすればどこでも動作するため、開発環境で作成したコンテナをそのまま本番環境にデプロイできます。これにより、開発から本番への移行がスムーズになり、デプロイメントの信頼性が向上します。
環境ごとに発生する差分の吸収方法は後ほど説明します。

### 効率的なリソース利用
コンテナはホストOSのカーネル(システムのCPUやメモリ、デバイスなどを管理し、アプリケーションを効率的・安全に利用できるようにするもの)を共有するため、同じホスト上で多くのコンテナを効率的に実行できます。これにより、サーバーのリソースを最大限に活用でき、コスト効率が向上します。

### 簡単なバージョン管理とロールバック
Dockerイメージはバージョン管理が可能で、特定のバージョンにタグ付けできます。これにより、特定のバージョンにロールバックするのが容易になり、問題が発生した際の対応が迅速に行えます。

### セキュリティの向上
コンテナはアプリケーションを他のアプリケーションやシステムから隔離するため、セキュリティが向上します。また、Dockerには、イメージのセキュリティスキャンやサンドボックス機能があり、セキュリティリスクを低減するためのツールが豊富に用意されています。


## Dockerについて
### Dockerのインストール手順
### Mac
[Docker Desktop for Mac](https://www.docker.com/ja-jp/products/docker-desktop/)の公式サイトにアクセスし、インストーラーをダウンロードします。

ダウンロードした .dmg ファイルを開き、Dockerアイコンをアプリケーションフォルダにドラッグします。

アプリケーションフォルダからDockerを起動します。このとき、初回起動時に必要なコンポーネントの設定が行われます。

### Windows
[Docker Desktop for Windows](https://www.docker.com/ja-jp/products/docker-desktop/)の公式サイトにアクセスし、インストーラーをダウンロードします。

ダウンロードしたインストーラーを実行し、インストールウィザードに従います。

インストールが完了したら、Docker Desktopを起動します。このとき、初回起動時に必要なコンポーネントの設定が行われます。

### インストールの確認
下記のコマンドが実行できることを確認します。
```zsh:terminal
docker --version
```
下記の内容が表示されたら正常にインストールができています。
```zsh:terminal
Docker version 24.0.6, build ed223bc
```
### 設定ファイル
flaskを使ったDockerfileを作成する方法を記載します。

#### Dockerfileの作成
以下は、Flaskアプリケーション用のDockerfileです。
```Dockerfile:Dockerfile
# ベースイメージを指定
FROM python:3.12-slim

# 作業ディレクトリを設定
WORKDIR /app

# 必要なパッケージをインストール
RUN pip install --upgrade pip

# 依存関係をインストール
COPY ./src/docker_sample_app /app
COPY requirements.lock requirements.lock
COPY pyproject.toml pyproject.toml
COPY README.md README.md

RUN pip install -r requirements.lock

# アプリケーションコードを追加
COPY . /app

# ポートを公開
EXPOSE 5000

# コマンドを実行
CMD ["python", "app.py"]
```

#### Dockerイメージのビルドとコンテナの実行
Dockerイメージをビルドし、コンテナを実行します。

##### イメージのビルド
```terminal:terminal
docker build -t my-flask-app .
```
-tはタグを意味し、最後の.はDockerfileのあるディレクトリがカレントディレクトリであることを意味しています。

##### コンテナの実行
```terminal:terminal
docker run -p 5000:5000 my-flask-app
```
先ほどmy-flask-appとタグ付けしたコンテナを実行します。-pはDockerの内部でポート番号5000番で待ち受けているサービスを、Dockerの外部でポート番号5000番で待ち受けるという意味です。つまり、ブラウザで`http://localhost:5000`にアクセスすると、「Hello, World!This page has been visited ⚪︎⚪︎(アクセスした分の数字) times.」というメッセージが表示されます。

### よく使われるDockerコマンド

#### Dockerボリュームの一覧表示
Dockerボリュームの一覧を表示します。
```zsh:terminal
docker volume ls
```

#### 新しいDockerボリュームの作成
新しいDockerボリュームを作成します。
```zsh:terminal
docker volume create <ボリューム名>
```

#### Dockerボリュームの削除
Dockerボリュームを削除します。
```zsh:terminal
docker volume rm <ボリューム名>
```

<details><summary>以下のコマンドはDocker Composeを使用する場合、あまり使うことがないので省略</summary>

#### よく使われるDockerコマンドとライフサイクル
以下のライフサイクルを見ると、コマンドの使用タイミングが理解しやすくなります。
![Docker Container Status.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3802241/a4d62b79-ae3d-5a3c-fa7d-702f24874e1b.png)
[Dockerコンテナのステータスとライフサイクルから引用](https://overworker.hatenablog.jp/entry/2020/07/03/001509)

#### Dockerfileを使用したイメージのビルド
Dockerfileを使用してイメージをビルドします。
```zsh:terminal
docker build [オプション] <ビルドコンテキスト>
```

#### Dockerイメージの削除
ローカルのDockerイメージを削除します。
```zsh:terminal
docker rmi <イメージIDまたはイメージ名>
```

#### イメージの取得
リモートリポジトリ（例：Docker Hub）からイメージを取得します。
```zsh:terminal
docker pull <イメージ名>:<タグ>
```

#### イメージ一覧の表示
ローカルに存在するDockerイメージの一覧を表示します。
```zsh:terminal
docker images
```


#### 指定されたイメージの実行
新しいコンテナを作成し、指定されたイメージを実行します。
```zsh:terminal
docker run [オプション] <イメージ名>
```

#### 実行中のコンテナの一覧を表示
実行中のコンテナの一覧を表示します。
```zsh:terminal
docker ps
```
`-a`オプションを追加すると、停止しているコンテナも含めた全てのコンテナを表示します。
```zsh:terminal
docker ps -a
```

#### 実行中のコンテナの停止
実行中のコンテナを停止します。
```zsh:terminal
docker stop <コンテナIDまたはコンテナ名>
```

#### 停止しているコンテナの開始
停止しているコンテナを開始します。
```zsh:terminal
docker start <コンテナIDまたはコンテナ名>
```

#### 停止しているコンテナの削除
停止しているコンテナを削除します。
```zsh:terminal
docker rm <コンテナIDまたはコンテナ名>
```

#### 実行中のコンテナ内でのコマンド実行
実行中のコンテナ内でコマンドを実行します。
```zsh:terminal
docker exec [オプション] <コンテナIDまたはコンテナ名> <コマンド>
```



#### Dockerネットワークの一覧を表示
Dockerネットワークの一覧を表示します。
```zsh:terminal
docker network ls
```

#### 新しいDockerネットワークの作成
新しいDockerネットワークを作成します。
```zsh:terminal
docker network create <ネットワーク名>
```

#### Dockerネットワークの削除
Dockerネットワークを削除します。
```zsh:terminal
docker network rm <ネットワーク名>
```

</details>

## Docker Compose
### Docker Composeの基本的な使い方
Docker Composeを使用すると、複数のDockerコンテナを一括して管理することができ、開発環境の構築やアプリケーションのデプロイが簡単になります。`docker-compose.yaml`ファイルにサービスの設定を記述し、docker-compose コマンドを使用することで、容易にサービスの起動・停止・管理が行えます。
以下に、Docker Composeの主要な利点をまとめました。

#### 複数コンテナの管理
Docker Composeを使うと、複数のDockerコンテナを一括して管理できるため、アプリケーションの複雑な依存関係を簡単に扱えます。一つの設定ファイル（docker-compose.yaml）で、全てのサービスを定義し、まとめて操作することができます。

####  一貫した開発環境
Docker Composeを使用することで、開発、テスト、本番環境で一貫した設定を保つことができます。開発者全員が同じ環境で作業できるため、環境による問題を減らし、バグの再現性を高めることができます。

#### 簡単な環境設定
docker-compose.yaml ファイルにより、環境設定がコードとして管理されるため、設定変更の履歴を追跡でき、設定の共有や再利用が簡単です。

#### 依存関係の管理
Docker Composeは、サービス間の依存関係を明確に定義することができ、例えばデータベースが起動するまでアプリケーションのコンテナを待機させるといったことが可能です。

#### スケーラビリティ
サービスを簡単にスケールアウトできます。例えば、docker-compose up --scale web=3 とするだけで、webサービスを3つのインスタンスに増やせます。

#### ネットワーキング
デフォルトでサービス間のネットワーキングを管理し、サービス名で他のコンテナにアクセスできるようにします。これにより、複雑なネットワーク設定が不要になります。

#### 効率的なリソース管理
複数のコンテナを効率的に起動・停止できるため、開発中のリソース管理が容易です。必要な時にだけサービスを立ち上げ、不要になったらすぐに停止できます。

#### 容易なCI/CDの統合
Docker Composeは、CI/CDパイプラインと簡単に統合できます。例えば、テスト環境をセットアップするために、docker-compose up コマンドをCIジョブに追加するだけで、テストに必要な全てのサービスを起動できます。

#### バックアップと復元
ボリュームを使用することで、データの永続化が容易になります。また、ボリュームを使ったバックアップやリストアも簡単に行うことができます。

#### プラットフォーム間の互換性
Docker Composeファイルは、Windows、macOS、Linuxのどのプラットフォームでも同じように動作します。これにより、プラットフォーム間の差異を気にせず開発が行えます。

#### 簡単なデバッグとロギング
Docker Composeを使うと、全てのサービスのログを簡単に収集・表示できます。docker-compose logs コマンドを使えば、各サービスのログを確認して、デバッグに役立てることができます。

### Docker Composeのインストール手順
Docker Desktopをインストールしている場合、Docker Composeは自動的にインストールされています。以下のコマンドでインストールされているかを確認します。
```zsh:terminal
docker-compose --version
```
下記の内容が表示されたら正常にインストールができています。
```zsh:terminal
Docker Compose version v2.23.0-desktop.1
```
### docker-compose.yamlファイルの作成
`docker-compose.yaml`ファイルは、マルチコンテナアプリケーションのサービス、ネットワーク、およびボリュームを定義するためのYAMLファイルです。
以下は、基本的な構造の例です。
```yaml:docker-compose.yaml
version: '3'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - redis
  redis:
    image: "redis:alpine"
```
このファイルは、2つのサービス（`web`、`redis`）を定義しています。

web: カスタムアプリケーション、5000番ポートを公開し、redisサービスに依存
redis: Redis(インメモリデータベース)


### よく使われるコマンド
以下に、Docker Composeでよく使われるコマンドを記載します。

#### プロジェクトの立ち上げ
`docker-compose.yaml`に定義されたすべてのサービスを一括で起動します。
```zsh:terminal
docker-compose up
```

バックグラウンドで実行する場合は、`-d`オプションを追加します。
```zsh:terminal
docker-compose up -d
```

#### プロジェクトの停止
実行中のすべてのサービスを停止します。
```zsh:terminal
docker-compose down
```

フォアグラウンドで実行している場合は`ctrl + c`で停止できます。

#### サービスの再構築
変更されたサービスのイメージを再構築します。
```zsh:terminal
docker-compose build
```

#### サービスのステータス確認
実行中のサービスのステータスを確認します。
```zsh:terminal
docker-compose ps
```

#### ログの表示
サービスのログを表示します。
```zsh:terminal
docker-compose logs
```

#### 特定のサービスの再起動
特定のサービスを再起動します。
```zsh:terminal
docker-compose restart <service_name>
```

### シンプルなWebアプリケーションの構築
以下に、シンプルなPython FlaskアプリケーションとRedisを使用したDocker Composeプロジェクトの例を記載します。

#### ディレクトリ構造
```
my_project/
├── docker-compose.yaml
├── Dockerfile
├── docker_sample_app/
│   └── app.py
```

#### docker-compose.yaml
```yaml:docker-compose.yaml
version: '3'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - redis
  redis:
    image: "redis:alpine"
```

#### app/Dockerfile
```Dockerfile:Dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY ./src/docker_sample_app /app
COPY requirements.lock requirements.lock
COPY pyproject.toml pyproject.toml
COPY README.md README.md

RUN pip install -r requirements.lock

CMD ["python", "app.py"]
```

#### app/app.py
```python:python
from flask import Flask
import redis

app = Flask(__name__)
client = redis.StrictRedis(host='redis', port=6379, decode_responses=True)

@app.route('/')
def hello():
    count = client.incr('hits')
    return f'Hello World! This page has been visited {count} times.'

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)
```

#### プロジェクトの起動
プロジェクトディレクトリに移動し、`docker-compose up`コマンドを実行します。
```zsh:terminal
cd docker-sample-app
docker-compose up
```
ブラウザで `http://localhost:5000`にアクセスすると、Flaskアプリケーションが動作していることを確認できます。

### 環境ごとの差分について
開発から本番へスムーズに移行するためには、環境ごとの差分を適切に設定および呼び出す必要があります。
ここではその方法を例示します。なお、下記で紹介する方法以外に設定ファイルを使用するやり方もありますが、ここではDockerの話から離れるので割愛します。 

#### 差分の発生する箇所を環境変数から読み取るように修正する
ここで使用しているFlaskのコードでは、Redisのホストがローカル開発環境・ステージング環境・商用環境とで接続先が変わる恐れがあります。そこで、Redisのホストを環境変数から読み込むように修正します。 

```python:app.py
from flask import Flask
import os
import redis

app = Flask(__name__)
client = redis.StrictRedis(host=os.getenv('REDIS_HOST'), port=6379, decode_responses=True)

@app.route('/')
def hello():
    count = client.incr('hits')
    return f'Hello World! This page has been visited {count} times.'

if __name__ == "__main__":
    app.run(host="0.0.0.0", debug=True)
```

#### ローカル開発環境で環境変数を設定する 

##### docker Compose.yamlを使用する
Dockerfileで環境変数を設定する方法などもありますが、ここではdocker-compose.yamlで設定する方法を記載します。

```yaml:docker-compose.yaml
version: '3'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - redis
    environment:
      - REDIS_HOST=redis
  redis:
    image: "redis:alpine"
```

##### .envファイルを使用する
docker-compose.yamlファイルを作成し、env_fileオプションを使用して適切な.envファイルを指定します。

```yaml:docker-compose.yaml
version: '3'
services:
  web:
    build: .
    ports:
      - "5000:5000"
    depends_on:
      - redis
    env_file:
      - .env
  redis:
    image: "redis:alpine"
```

#### ステージング・商用環境で環境変数を設定する
ここではDockerの話からはなられるので割愛しますが、AWSであればECSのタスク定義で環境変数を設定することになります。また、その値はSystems ManagerのParameter StoreやSecrets Managerに登録して、そこから環境変数経由で読み込みます。

#### 注意事項
ここで紹介した環境変数に設定する値は必ずローカル開発環境での設定のみとし、ステージング・本番環境等の設定情報を記述してGitHub等にプッシュしないでください。セキュリティインシデントにつながりかねないからです。ステージング・本番環境等の設定は上記の通りインフラに設定すべきものです。

## まとめ
Dockerは、アプリケーションの開発からデプロイまでのプロセスを効率化し、環境の一貫性、軽量性、高速性、スケーラビリティ、移植性といった多くの利点があります。
また、Docker Composeは、複数のコンテナを簡単に定義、管理、実行するための強力なツールで、これによってマルチコンテナアプリケーションの開発とデプロイが効率化されます。

## 参考資料

https://overworker.hatenablog.jp/entry/2020/07/03/001509