[2023]オレオレdocker開発環境を自動的に構築してもらった

# はじめに
「俺が使う俺のための環境構築」の第3シリーズです。

## シリーズ紹介
概要の作成はChatGPTに書いてもらいました。

第1シリーズ 「[ オレオレ開発環境2017〜2019年版 ](https://qiita.com/u1and0/items/c8c447ed5e1355fec6a8)」

<details><summary>第1シリーズ の概要</summary>
ターミナル周りの開発環境の推移についてまとめられています。

2017年の開発環境では、dotfilesとGitを使用しています。インストール方法や使用しているパッケージについて詳細が記載されています。

また、bacpacというパッケージマネージャやzsh、zplug、tmux、nvimなどのツールやエディタの設定方法も紹介されています。

さらに、Python環境の構築方法やVagrantとVirtual Boxを使用した開発環境についても触れられています。
</details>

第2シリーズ 「[ オレオレdocker開発環境を作ってみた ](https://qiita.com/u1and0/items/88e3dd5efbdb0a496959)」

<details><summary>第2シリーズ の概要</summary>
この記事では、俺が使う俺のための環境構築用Dockerイメージを作成する方法について説明されています。以下は記事の要点です。

- マイナーだが、様々なパッケージの最新版をインストールしやすいArchlinuxベースのイメージを使用しています。
- 日本語環境が適用されています。
- [ 自分のdotfiles ](https://github.com/u1and0/dotfiles)も適用されています。
- ファイルの所有者の問題を解決するために、UID=1000、GID=1000、USERNAME=u1and0でイメージを再構築しました。
- Neovim、zplug、Python、Rustなどの開発環境が含まれています。
- docker-in-docker環境も作成されています。
- docker-compose.ymlも追記されています。

このDockerイメージの継承関係は以下のようになっています。

- archlinux/archlinux (716MB)
  - u1and0/archlinux (777MB)
    - u1and0/neovim (1.2GB)
      - u1and0/zplug (1.3GB)
        - u1and0/vim-go (2.4GB)
        - u1and0/Python-conda (4.4GB)
        - u1and0/rust (3.3GB)
        - u1and0/docker (1.6GB)

このDockerイメージ内では、pacmanやyayといったパッケージマネージャを使用してAURパッケージ[^1]をインストールすることも可能です。
[^1]:Arch User Repository というArchlinuxユーザーの個人的なパッケージリポジトリ。

</details>


# やること
第3シリーズとなる今回は Github actionsを使って自動化をしていきたいと思います。


# 成果物

```yaml:.github/workflows/docker-image.yml
name: Weekly Docker Build

on:
  # 手動ビルド
  workflow_dispatch:
  # mainにpushされたとき
  push:
    branches:
      - main
  # Run every Sunday at 9:00 AM
  schedule:
    - cron: '0 9 * * 0'

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        context: ["docker", "go", "deno", "python"]

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      - name: Get GitHub SHA
        id: get_sha
        run: echo "SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      # Build & Push images
      #
      # yay - archlinux - neovim - zplug は継承関係があるので
      # 直列で実行する。
      - name: Build and Push yay Image
        id: build_yay
        uses: docker/build-push-action@v5
        with:
          context: ./images/yay
          push: true
          tags: |
            u1and0/yay:latest
            u1and0/yay:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        run: echo ${{ steps.build_yay.outputs.digest }}

      - name: Build and Push Archlinux Image
        id: build_archlinux
        uses: docker/build-push-action@v5
        with:
          context: ./images/archlinux
          push: true
          tags: |
            u1and0/archlinux:latest
            u1and0/archlinux:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        run: echo ${{ steps.build_archlinux.outputs.digest }}

      - name: Build and Push Neovim Image
        id: build_neovim
        uses: docker/build-push-action@v5
        with:
          context: ./images/neovim
          push: true
          tags: |
            u1and0/neovim:latest
            u1and0/neovim:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        run: echo ${{ steps.build_neovim.outputs.digest }}

      - name: Build and Push Zplug Image
        id: build_zplug
        uses: docker/build-push-action@v5
        with:
          context: ./images/zplug
          push: true
          tags: |
            u1and0/zplug:latest
            u1and0/zplug:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        run: echo ${{ steps.build_zplug.outputs.digest }}

      # docker, go, deno, python はzplugを継承しているので
      # 並列に実行できる。
      - name: Build and Push Docker in Docker, Go, Deno, Python Image
        id: build_from_zplug
        uses: docker/build-push-action@v5
        with:
          context: ./images/${{ matrix.context }}
          push: true
          tags: |
            u1and0/${{ matrix.context }}:latest
            u1and0/${{ matrix.context }}:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        run: echo ${{ steps.build_from_zplug.outputs.digest }}

      # PythonコンテナのときだけNimをビルド
      - name: Build and Push Nim Image
        if: matrix.context == 'python'
        id: build_nim
        uses: docker/build-push-action@v5
        with:
          context: ./images/nim
          push: true
          tags: |
            u1and0/nim:latest
            u1and0/nim:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        if: matrix.context == 'python'
        run: echo ${{ steps.build_nim.outputs.digest }}
```


## Actionsの説明
以下は、指定されたYAMLファイルから読み取った内容をわかりやすく記事にまとめたものです。ChatGPTにまとめてもらって、適宜修正しています。


## 週次のDockerイメージビルドを行うGitHub Actionsの設定
この記事では、週次でDockerイメージをビルド・プッシュするGitHub Actionsのワークフロー設定について説明します。


## ビルドトリガ

- 主ブランチ(main)へのpush時
- 週毎のスケジュール(日曜日午前9時)
- ワークフローの手動実行

と3つのトリガーでビルドを行います。

```yaml
on:
  # 手動ビルド
  workflow_dispatch:
  # mainにpushされたとき
  push:
    branches:
      - main
  # Run every Sunday at 9:00 AM
  schedule:
    - cron: '0 9 * * 0'
```

## ビルドジョブ

### Dockerイメージのプッシュ前準備

- リポジトリのチェックアウト
- docker buildxを使って並列ビルドを行うためにQEMUの準備
- docker hubへのログイン

```yaml
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v4
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      # 略
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
```

### Github Actionsでのシークレット
https://docs.github.com/ja/actions/security-guides/using-secrets-in-github-actions


### Docker hub PATを作成
https://hub.docker.com/settings/security

ここからdocker hubのPAT(Personal Access Token)を作成する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/ff81d564-0ff8-2a90-2a2c-bb6e4eb5bbed.png)


### Github Secretに登録

`https://github.com/{user-name}/{repo-name}/settings/secrets/actions`

PATをコピーしてGithub secretに登録
ユーザー名も登録


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/c3134f9f-6046-3785-66ea-f20c9c2124d6.png)


これらの操作を行ってからActionを回すとログインに成功した。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/113494/f619b678-c45b-82af-a6da-194db8df33ae.png)


### コンテキストのマトリックス設定

ビルド対象のコンテキスト("docker", "go", "deno", "Python")をマトリックスとしてまとめます。

```yaml
    strategy:
      matrix:
        context: ["docker", "go", "deno", "python"]

```

### Dockerイメージのビルド順序

- yay -> archlinux -> neovim -> zplug
の順にビルドしていきます。子イメージが親イメージを継承しているため順次ビルドが必要です。
- docker, go, deno, Pythonはzplugを継承しているので並列にビルドできます。

```yaml
      # Build & Push images
      #
      # yay - archlinux - neovim - zplug は継承関係があるので
      # 直列で実行する。
      - name: Build and Push yay Image
        id: build_yay
        uses: docker/build-push-action@v5
        with:
          context: ./images/yay
          push: true
          tags: |
            u1and0/yay:latest
            u1and0/yay:${{ steps.get_sha.outputs.SHA }}
          # 略

      - name: Build and Push Archlinux Image
        id: build_archlinux
        uses: docker/build-push-action@v5
        with:
          context: ./images/archlinux
          # 略

      - name: Build and Push Neovim Image
        id: build_neovim
        uses: docker/build-push-action@v5
        with:
          context: ./images/neovim
          # 略

      - name: Build and Push Zplug Image
        id: build_zplug
        uses: docker/build-push-action@v5
        with:
          context: ./images/zplug
          # 略
```


`{{ matrix.context }}` には docker, go, deno, Python いずれかの文字列が入ります。

```yaml
      # docker, go, deno, python はzplugを継承しているので
      # 並列に実行できる。
      - name: Build and Push Docker in Docker, Go, Deno, Python Image
        id: build_from_zplug
        uses: docker/build-push-action@v5
        with:
          context: ./images/${{ matrix.context }}
          push: true
          tags: |
            u1and0/${{ matrix.context }}:latest
            u1and0/${{ matrix.context }}:${{ steps.get_sha.outputs.SHA }}
      - name: Image Digest
        run: echo ${{ steps.build_from_zplug.outputs.digest }}
```

### SHAタグ付け
GitのコミットSHAを取り出し、イメージにタグ付けします。

ここでDockerfileを管理しているリポジトリのSHAを取得します。

```yaml
      - name: Get GitHub SHA
        id: get_sha
        run: echo "SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
```

次のようにしてSHAをイメージタグにします。

```yaml
      - name: Build and Push yay Image
        id: build_yay
        uses: docker/build-push-action@v5
        with:
          context: ./images/yay
          push: true
          tags: |
            u1and0/yay:latest
            u1and0/yay:${{ steps.get_sha.outputs.SHA }}
```


### Python時のみNimイメージもビルド
Pythonコンテキストのときのみ、追加でNimイメージもビルドします。

```yaml
      # PythonコンテナのときだけNimをビルド
      - name: Build and Push Nim Image
        if: matrix.context == 'python'
        id: build_nim
        uses: docker/build-push-action@v5
```

以上で、より整然としたDockerイメージのビルドパイプラインが構成できるようになっています。


## Actionsの状態確認
ここから確認できます。

[actions/workflows](https://github.com/u1and0/my-docker-dev/actions/workflows/docker-image.yml)

pushするたびに、あるいは毎週日曜日、あるいは手動でActionを走らせます。


# 記事にするまでの経緯

- Dockerの[自動ビルドが無料で使えなくなった](https://docs.docker.com/docker-hub/builds/)りした。[^2]
- docker v19.03以降から[ docker buildx ](https://matsuand.github.io/docs.docker.jp.onthefly/buildx/working-with-buildx/)が推奨されるようになった。

[^2]:Automated builds require a Docker Pro, Team, or Business subscription. 2021.1.5には無料でAutomate buildができないことを確認済み。

無料でAutomate buildが使えなくなった2021年からはPythonコマンドを駆使してワンコマンドで複数イメージをビルドするシステムを構築したりしていました。

<details><summary>複数イメージを構築するPythonスクリプト</summary>

```python:automate_build.py
#!/usr/bin/env python3
"""Auto build my docker images"""

import sys
from datetime import datetime
import subprocess

BASE_IMAGE = "archlinux/archlinux:base-devel"
USER = "u1and0"
IMAGES = {
    # [IMAGE NAME]:DIRECTORY NAME
    "yay": "docker_archlinux_yay",
    "archlinux": "docker_archlinux_env",
    "neovim": "docker_neovim_env",
    "zplug": "docker_zplug",
    "docker": "docker_docker",
    "deno": "docker_deno",
    "vim-go": "docker_vim-go",
    "python-conda": "docker_python-conda",
    # "rust": "docker_rust_env",
    "nim": "docker-nim",
}
DAY = datetime.strftime(datetime.today(), '%Y%m%d')

def init():
    """mirrorlist initialize"""
    cmd = [
            "reflector",
            "--verbose",
            "--country",
            "Japan",
            "--age",
            "48",
            "--protocol",
            "https",
            "--protocol",
            "rsync",
            "--sort",
            "rate",
            "--save",
            "../docker_archlinux_env/mirrorlist",
           ]
    try:
        run_command(cmd)
    except subprocess.CalledProcessError as err:
        sys.exit(err)

def main():
    """Build docker images"""
    success_count:int = 0
    for image, dirname in IMAGES.items():
        cmds = [
            # PULL
            f"sudo docker pull {BASE_IMAGE}",
            # BUILD
            f"sudo docker build --no-cache -t {USER}/{image} ../{dirname}",
            # TAG
            f"sudo docker tag {USER}/{image}:latest {USER}/{image}:{DAY}",
            # PUSH
            f"sudo docker push {USER}/{image}:latest",
            f"sudo docker push {USER}/{image}:{DAY}",
        ]
        for i, cmd in enumerate(cmds):
            try:
                run_command(cmd.split())
                if i == 2:  # `docker tag` command succeed message
                    print(f"Successfully tagged {USER}/{image}:{DAY}")
            except subprocess.CalledProcessError as err:
                sys.exit(err)
        success_count += 1
    print(f"Succeeded build images ({success_count}/{len(IMAGES)})")


def run_command(cmd: list) -> subprocess.CompletedProcess:
    """Run docker command"""
    res = subprocess.run(cmd,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.STDOUT,
                         check=True)
    sys.stdout.buffer.write(res.stdout)  # Write stdout
    res.check_returncode()  # Unless code 0 => raise CalledProcessError


if __name__ == "__main__":
    init()
    main()
```

</details>

このPythonスクリプトもやや凝っていますが、単純に`docker build u1and0/{イメージ名}`と`docker tag u1and0/{イメージ名}`と`docker push u1and0/{イメージ名}`を`subprocess`を使って実行しているだけです。PythonでなくてもShellscriptでもなんでもできますが、書きやすさでPythonを選択しています。
当時Go言語にハマっていたので、init()とmain()という書き方が無駄にGoっぽいです。


2021年頃からあらゆる場所でCI/CDやら Github Actions という仕組みを見聞きして、興味は持ちつつも優先順位が低くてなかなか触れられずにいました。

優先順位が低いのは、とりあえず今のdockerコマンドでもビルドできてましたし、古いイメージのままでも1年くらいは余裕で開発できていますので、「困ることがなかった」というのが正直なところです。

何十年も昔のLinuxイメージでもパッケージマネージャを使って新しくしていけば十分に扱える環境に仕上げられる、というのがLinuxの良いところです。
そして本当に困ったときはイメージをリセットすればまたスタートから始められる、これが仮想環境の良いところです。
当然ながら、新たな技術を取得するには腰を据える必要がありました。
これはCI/CD, Github Actionsに関わらずどんな技術でも共通して言えることです。 自動環境構築するための術を学ぶための時間と労力が必要でした。

これらをあわせると、「現状維持」でも困ることが本当になかったのです。 「新たな技術取得」のプライオリティが十分低くなるほどに、LinuxというシステムとDockerという仮想環境の組み合わせの相性が良すぎるのです。

年末年始にようやく手を付けられる時間が僅かにできたものの、結局ショートカットすることを考えてやったことが「ChatGPTにGithub Actionsのyamlファイルを書いてもらう」ことでした。
これでほぼ十分に動きましたし、足りないところは適宜勉強して1日で自動ビルドシステムは完成しました。

# まとめ
開発環境を構築するのは時間と労力が必要です。システムのクラッシュやOSの移行など、開発環境の移行は何度か経験する出来事です。 これを容易にするため、DockerとGithub Actionsを使用したコード化されたインフラ環境が重要です。日常的に開発環境を整理し、いつでも移行できる準備をしておくことが重要です。