Dev Containersのビルド時間を短縮し，サービスの再起動を可能にした

## これは何?

VSCodeのDev ContainersというDockerコンテナ内でVSCodeを開く機能を使っていて

- ビルド時間が長いこと
- サービスの再起動をするのに再ビルドが必要

の2つの問題を改善したら開発体験がよくなったので同じようなところでストレスを感じている人の助けになればと

---

## 対象リポジトリ

https://qiita.com/advent-calendar/2024/sigma_security

去年のアドベントカレンダーでも何回か登場した[OpenRestyのリバースプロキシ](https://github.com/RyosukeDTomita/GateKeeper)を対象にした。

### 変更前の状態

[変更前のリポジトリ](https://github.com/RyosukeDTomita/GateKeeper/tree/81a0ecc261b78e61bfd8f32e92cbc535fe50b0c9)

```devcontainer.json
{
  "name": "dev_container", // 任意
  "dockerComposeFile": [
    "../compose.yaml",
    "compose.yaml"
  ],
  "service": "reverse_proxy_app", // compose.yamlのサービス名
  "workspaceFolder": "/usr/local/openresty",
  // Dev Container起動時に開発ツールをインストール
  "postCreateCommand": "./.devcontainer/install-pkg.sh",
  "customizations": {
    "vscode": {
      "extensions": [
        "DavidAnson.vscode-markdownlint",
        "exiasr.hadolint",
        "oderwat.indent-rainbow",
        "ionutvmi.path-autocomplete",
        "sumneko.lua",
        "trixnz.vscode-lua",
        "gccfeli.vscode-lua"
      ]
    }
  }
}
```

```compose.yaml
# Dev Containerではこちらが優先される。
services:
  reverse_proxy_app:
    build:
      target: devcontainer
      context: ./reverse_proxy
      dockerfile: Dockerfile
    image: reverse_proxy_devcontainer:latest
    container_name: reverse_proxy-devcontainer-container
    volumes:
      - ./reverse_proxy/:/usr/local/openresty/reverse_proxy
      # NOTE: install-pkg.shをコンテナに追加する
      - ./.devcontainer:/usr/local/openresty/.devcontainer 

  redis_app:
    build:
      context: ./redis
      dockerfile: Dockerfile
    image: redis-img:latest
    container_name: redis_container
    ports:
      - 6379:6379 # localport:dockerport
  # NOTE: redis_appに初期データを投入するために一時的なコンテナを立ち上げている。
  redis_client:
    image: redis:latest
    volumes:
      - ./redis/initial_data_redis.sh:/tmp/initial_data_redis.sh
    command: >
      /bin/bash -c 'source /tmp/initial_data_redis.sh'
    depends_on:
      - redis_app
```

`"postCreateCommand": "./.devcontainer/install-pkg.sh",`の部分で必要なライブラリをインストールしている。

```install-pkg.sh
#!/bin/bash
package_list="net-tools \
  curl \
  wget \
  rsync \
  unzip \
  zip \
  vim \
  jq \
  less \
  git \
  ca-certificates \
  iputils-ping \
  dnsutils \
  iproute2 \
  tcpdump \
  procps
"
apt-get update -y
apt-get install -y --no-install-recommends ${package_list[@]}
rm -rf /var/lib/lists

# hadolint
wget -O /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/download/v2.10.0/hadolint-Linux-x86_64
chmod 755 /usr/local/bin/hadolint

# redis-cli
REDIS_VERSION=7.4.1
# NOTE: `./redis-cli -h redis_app -p 6379`で接続可能
wget https://github.com/redis/redis/archive/refs/tags/${REDIS_VERSION}.tar.gz
tar xzvf ${REDIS_VERSION}.tar.gz
cd redis-${REDIS_VERSION}
make
cp src/redis-cli /usr/local/bin
cd ..
rm ${REDIS_VERSION}.tar.gz
rm -rf redis-${REDIS_VERSION}
```

個人的に気になっていた部分は以下。

- Dev Containerでビルドすると通常のDockerビルドよりもかなり時間がかかる
- nginxの再起動をするためには再ビルドが必要: コンテナ永続化プロセスである，nginxを止めることになるのでコンテナが落ちてしまうため

---

## ビルド速度の改善のためVolumeマウントをキャッシュ代わりに使う

このinstall-pkg.shの中でredisのビルドを行って，`redis-cli`だけ抽出してパスを通すみたいなことをやっていたため，(redis-cliを単体でinstallする方法が多分ないので謎のこだわりを発揮していた)のでそこそこビルド時間がかかっていた。

:::note info
Dev Container内でのみ実行するスクリプトには(多分)Dockerのレイヤキャッシュが効かないはず
TODO: 効かせる方法がないか今度調べてみる。
:::

そこで，初回のビルド時に作成したredis-cliをvolumeマウント使ってみた。

自分はDev Container用のcompose.yamlを作っており，.devcontainer/cacheをコンテナの/cacheにvolumeマントする設定を追記した。

```compose.yaml
 # NOTE: ./install-pkg.shでインストールしたパッケージをキャッシュするためのディレクトリ
      - ./.devcontainer/cache:/cache 

```

```install-pkg.sh変更部分
# redis-cli
setup_redis_cli() {
  # 2回目以降のbuild時にはvolumeマウントしている/cacheをつかう
  cp /cache/redis-cli /usr/local/bin/redis-cli
  if command -v redis-cli > /dev/null 2>&1; then
    return 0
  fi
  REDIS_VERSION=7.4.1
  # NOTE: `./redis-cli -h redis_app -p 6379`で接続可能
  wget https://github.com/redis/redis/archive/refs/tags/${REDIS_VERSION}.tar.gz
  tar xzvf ${REDIS_VERSION}.tar.gz
  cd redis-${REDIS_VERSION}
  make
  cp src/redis-cli /usr/local/bin
  cp src/redis-cli /cache # NOTE: ビルド時間短縮のために/tmpをvolumeマウントしてキャッシュする
  cd ..
  rm ${REDIS_VERSION}.tar.gz
  rm -rf redis-${REDIS_VERSION}
}
setup_redis_cli
```

この変更により，初回ビルド以外にはredisｎビルドが走らなくなった。万歳!

---

## そもそもビルドする回数を減らす: コンテナ永続化プロセスをnginxにするのをやめる

コンテナは起動しつづけるためには永続的なプロセスが必要である。
Dockerfileの`CMD`とか`ENTRYPOINT`とかに書いてあるあれ

自分はCLIツールを作る時のような永続化プロセスを持たないコンテナでDevContainerを使う時のみ，devcontainer.jsonに`"overrideCommand": true`を使ってコンテナを永続化していた。
Dockerfileがnginxのような永続化プロセスを持っている際にはわざわざこの機能を使う意味はないなと思っていたのだが，以下の記事を見て，永続化プロセスを持っているDockerifileを扱う場合でも`"overrideCommand": true`を使うべきだと知った。

https://qiita.com/1mono2/items/5bbf91f588ab9d5cd444#typescirpt%E3%81%A7%E3%81%AEdevcontainer%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9

- `"overrideCommand": true`でコンテナを永続化させる。
- 変わりにnginxはdevcontainer.jsonの中で`"postStartCommand": "openresty",`として起動させる

これを実施したことにより，nginxを再起動してもコンテナが止まることがなくなった。
そのため，実装を変更するたびにDev Containerを再ビルドする必要がなくなった。万歳!

---

## まとめ

- Dev Container内でのみ実行するスクリプトには(多分)Dockerのレイヤキャッシュが効かないので，
    - なるべく処理を減らす
    - 重い処理はなるべく再実行しなくて済む方法を考える。
- コンテナ永続化プロセスはDev Containerの機能でOverrideしてやってもらう。変わりにサービスは通常起動することで止めてもコンテナが止まらなくなる。