Docker CLIで「コマンド置換」を活用した一括操作


## 概要

シェル上で `docker ps` や `docker kill` コマンドの実行にあたり、 `$(コマンド)` という書き方（コマンド置換）を使うと、操作が簡単になる場合があります。たとえば、複数のコンテナをまとめて停止するためには、 `docker ps` を実行後に  `docker kill 35e 1f5 ...` を実行する手順が必要ですが、これを `docker kill $(docker ps -q)` だけで済ませます。

## 前提条件

- Linux や macOS 上のシェル環境で利用できます。
- Windows は PowerShell や WSL でのみ。コマンドプロンプトでは利用できません。

## コマンド置換とは

あるコマンドを実行する時、そのコマンドの中で `$(コマンド)` の書き方をすると、それを別のコマンド出力に置き換えられる書き方が、コマンド置換（command substitution）です。

たとえば `ls` コマンドを実行すると

```bash
$ ls
file1  file2
```

このようにファイル一覧が出ます。

これを、コマンド置換を使えば、次のように書き換えできます。

```bash
$ echo $(ls)
file1 file2
```

ポイントは `$(ls)` の記述とは、 `ls` コマンドの表示結果（ `file1 file2 ` ）を `$(ls)` 文字で置き換えられ、結果的に `echo file1 file2` を実行したのと同じ結果となります。

```bash
$ echo file1 file2
file1 file2
```

なお、 `` `コマンド` `` のようにバッククォート文字で囲む書き方もあります（例： `` `docker ps -q` ``）。ただし、こちらはコマンド部分に `` ` `` を含められないレガシー（旧来形式）の書き方です。

## Docker でコマンド置換を活用

コマンド置換を `docker` コマンドで使うと、以下のような作業が簡単になります。

- 直前に作成した docker コンテナを直ちに操作する
- 実行中の Docker コンテナをすべて停止する
- Docker コンテナやイメージを全削除する

また、CI 環境などで、シェルスクリプトを利用した処理の自動化にも、このコマンド置換が利用できます。

### 直前に作成した Docker コンテナを直ちに操作する

Docker コンテナの起動直後、対象のコンテナに `docker logs`  でログ（標準出力やエラー）を見たいときや、`docker exec` を使ったり、 `docker top` で実行中のプロセスを表示したい場合があります。

通常であれば、2つのステップが必要です。

1. 何らかの方法で、コンテナ ID またはコンテナ名を知る
2. `docker` コマンドで、コンテナ ID かコンテナ名で、対象のコンテナを操作する

たとえば、 `docker logs` コマンドでログを見るには `docker logs interesting_sanderson` のようにコンテナ名（この例では `interesting_sanderson`）、あるいは、コンテナIDを指定する必要があります。つまり、コンテナの操作には、何らかの方法で事前にコンテナ ID かコンテナ名を知っておかなくてはいけません。

ちなみに、この課題を簡単に解決するのは、 `docker run --name コンテナ名` を使い、コンテナに予め名前を与える方法です。1つの Docker ホスト上で、コンテナ名を重複して実行できません。そうしておけば、シェルスクリプト上でも、間違いなくコマンドが実行できます。しかし、この方法では、同じコンテナ名で同時に処理が出来ないため、意図せず自動実行できない場合や、それを回避するための工夫が必要になる場合も考えられます。

ですが、このような方法を使わなくても、コマンド置換を使えば、直前に操作したコンテナIDを直ちに知れます。

例として、Nginx コンテナをデタッチドモード（バックグラウンド）で実行するケースを取り上げます。

```bash
$ docker run -itdP nginx
0c28d6afc0f9ba9a1f73ae7769493c437bba8583fb2c769df6ba2cb4c2202381
$ 
```

デタッチドモードであれば、コマンド実行直後に `0c28d....` で始まるコンテナ ID が表示されています。

通常、ここでログを見ようとすると `docker logs 0c28` のように入力しますが、コマンド置換を使えば `docker logs $(docker ps -lq)` という決まり切ったコマンドで表示できます。

```
$ docker logs $(docker ps -lq)
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh
/docker-entrypoint.sh: Configuration complete; ready for start up
2021/11/30 22:36:25 [notice] 1#1: using the "epoll" event method
（省略）
```

ここで何をしているかを知るには、 `$(コマンド)` にあたる `docker ps -lq` の理解が欠かせません。

まず、 `docker ps -l`  コマンドは `docker ps --lastest` の省略系で、 `-l` オプションは、直近に作成したコンテナ（状態が、起動・停止かに拘わらず）を表示します。

```bash
$  docker ps -l
CONTAINER ID   IMAGE     COMMAND                  CREATED         STATUS         PORTS                                     NAMES
0c28d6afc0f9   nginx     "/docker-entrypoint.…"   6 minutes ago   Up 6 minutes   0.0.0.0:49158->80/tcp, :::49158->80/tcp   keen_stonebraker
````

そして `docker ps -l -q` は `docker ps --lastest --quiet` の省略系で、 `-q` オプションはコンテナ ID しか表示しません。

```bash
$ docker ps -l -q
0c28d6afc0f9
```

つまり、 `docker logs $(docker ps -lq)` の実行とは、この例における `docker logs 「コマンドdocker ps -lqの実行結果」` であり、 `$( )` で囲まれた部分は実行した出力に置換されますから、結果的に `docker logs 0c28d6afc0f9` を実行するのと、同じ処理を意味します。

この方法を使えば、直前に操作したコンテナ ID が分からなくても、手動でコマンドを実行して確認しなくても操作できます。さらに、シェルスクリプトなど、自動的にコンテナ操作をしたい場合にも有用です。

### 実行中の Docker コンテナをすべて停止する

次のように、複数の Docker コンテナが起動している状態があるとします。

```bash
$ docker ps
CONTAINER ID   IMAGE     COMMAND                  CREATED          STATUS          PORTS                                     NAMES
0c28d6afc0f9   nginx     "/docker-entrypoint.…"   12 minutes ago   Up 12 minutes   0.0.0.0:49158->80/tcp, :::49158->80/tcp   keen_stonebraker
35ecc5c7355b   nginx     "/docker-entrypoint.…"   3 months ago     Up 3 months     0.0.0.0:49157->80/tcp, :::49157->80/tcp   inspiring_merkle
e6b88055e48c   nginx     "/docker-entrypoint.…"   3 months ago     Up 3 months     0.0.0.0:49156->80/tcp, :::49156->80/tcp   quirky_mirzakhani
1f51ba1a4dfd   nginx     "/docker-entrypoint.…"   3 months ago     Up 3 months     0.0.0.0:49155->80/tcp, :::49155->80/tcp   determined_shaw
703b701ae8d3   nginx     "/docker-entrypoint.…"   3 months ago     Up 3 months     0.0.0.0:49154->80/tcp, :::49154->80/tcp   interesting_sanderson
7f5d7a4bd06e   nginx     "/docker-entrypoint.…"   3 months ago     Up 3 months     0.0.0.0:49153->80/tcp, :::49153->80/tcp   youthful_ramanujan
```

ここでは6つの Nginx コンテナが起動中です。これらをまとめて停止(kill)する場合を考えましょう。通常であれば、次の手順が必要です。

1.  `docker ps` でコンテナ一覧を表示する（既に実行済み）
2. `docker kill` で、停止したいコンテナのIDを指定する

この例であれば `docker kill 0c2 35e e6b 1f5 715` と入力する必要があります。

ですが、これもコマンド置換であれば、次のコマンドで済みます。

```
$ docker kill $(docker ps -q)
0c28d6afc0f9
e6b88055e48c
1f51ba1a4dfd
703b701ae8d3
7f5d7a4bd06e
```

### Docker コンテナをまとめて削除する

停止中のコンテナを全削除するには `docker container prune` コマンドがあります。個人で利用しているPC上で、雑に停止中のコンテナ（正確には、コンテナ用のイメージレイヤやログや設定情報）を一括削除するには楽ですが、社内や学内の共用環境で実行するのは躊躇します。

そういう場合でも、コマンド置換と `docker ps` のフィルタによって問題を解決できます。

 `doker ps` では `-f` または `--filter` オプションによって（ [リファレンス](https://docs.docker.com/engine/reference/commandline/ps/#filtering) ）、status や 元イメージの情報によるフィルタができます。

たとえば、 alpine:latest イメージを使ったコンテナ一覧は次のようになります（ `-a` オプションの追加により、起動・停止中に拘わらず、すべてを表示）。

```bash
$ docker ps -a -f ancestor=alpine:latest
CONTAINER ID   IMAGE     COMMAND       CREATED        STATUS                     PORTS     NAMES
6873bcaf4879   alpine    "/bin/sh"     2 hours ago    Exited (137) 2 hours ago             hardcore_tu
b5c731e558f7   alpine    "/bin/sh"     2 hours ago    Exited (137) 2 hours ago             clever_antonelli
fa14d1f00ae7   alpine    "/bin/ash"    3 months ago   Exited (0) 3 months ago              crazy_lehmann
c9b814f42f7c   alpine    "/bin/bash"   3 months ago   Created                              pedantic_merkle
```

これらだけを削除するには、これまで見てきた通りコマンド置換を使えば簡単です。

```bash
$ docker rm -f $(docker ps -a -f ancestor=alpine:latest -q)
6873bcaf4879
b5c731e558f7
fa14d1f00ae7
c9b814f42f7c
```

実際に運用している環境では、フィルタはイメージ名だけでなく、状態など組みあわせるなど、工夫が必要になる場合もあるでしょう。

### Docker イメージをまとめて削除

以前であれば、このコマンド置換を使い `docker rmi -f  $(docker image ls -q)` のように実行していましたが、最近では便利なコマンドがあります。

親子関係を持たない、不要と思われる中間イメージ（dangling images）を一括で消すコマンドは `docker image prune` です（ [リファレンス](https://docs.docker.com/engine/reference/commandline/image_prune/) ）。このコマンドを実行しても、Docker Hub のようなリポジトリからダウンロードしたイメージは残しておけます。

```bash
$ docker image prune
WARNING! This will remove all dangling images.
Are you sure you want to continue? [y/N] y
Total reclaimed space: 0B
$ docker images
REPOSITORY    TAG       IMAGE ID       CREATED        SIZE
nginx         latest    dd34e67e3371   3 months ago   133MB
alpine        latest    021b3423115f   3 months ago   5.6MB
ubuntu        latest    1318b700e415   4 months ago   72.8MB
hello-world   latest    d1165f221234   9 months ago   13.3kB
```

この環境では、もともと余分なイメージはなかったようです。

便利なのは、`-a` オプションの付与です。Docker コンテナで使われていないイメージを削除します。さらに、 `-f` オプションも付ければ確認のプロンプトも出ませんので、シェルスクリプトでの処理にも向いています。

```
$ docker image prune -a -f
Deleted Images:
untagged: hello-world:latest
untagged: hello-world@sha256:0fe98d7debd9049c50b597ef1f85b7c1e8cc81f59c8d623fcb2250e8bec85b38
deleted: sha256:d1165f2212346b2bab48cb01c1e39ee8ad1be46b87873d9ca7a4e434980a7726
deleted: sha256:f22b99068db93900abe17f7f5e09ec775c2826ecfe9db961fea68293744144bd
untagged: alpine:latest
untagged: alpine@sha256:eb3e4e175ba6d212ba1d6e04fc0782916c08e1c9d7b45892e9796141b1d379ae
deleted: sha256:021b3423115ff662225e83d7e2606475217de7b55fde83ce3447a54019a77aa2
deleted: sha256:bc276c40b172b1c5467277d36db5308a203a48262d5f278766cf083947d05466

Total reclaimed space: 5.609MB

$ docker images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
nginx        latest    dd34e67e3371   3 months ago   133MB
ubuntu       latest    1318b700e415   4 months ago   72.8MB
```

この環境では nginx と ubuntu のコンテナが実行中のため残っていますが、それ以外の不要なイメージは削除されています。


## ポイント

docker で  `$(コマンド)` によるコマンド置換を使えば、通所の作業が楽になる場合や、シェルスクリプトでのコマンド実行にも使えます。

## 参考

- 「Man page of BASH」の「コマンド置換」
  - https://linuxjm.osdn.jp/html/GNU_bash/man1/bash.1.html
- Command Substitution (Bash Reference Manual)
  - https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html