Docker Hubの自作イメージを自動アップデートしてリリースする

先日こんなtweetを見かけました｡

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">作った / “awscli-allを作った - くりにっき” <a href="https://t.co/7t1YIK9GX2">https://t.co/7t1YIK9GX2</a></p>&mdash; sue445 (@sue445) <a href="https://twitter.com/sue445/status/1139158119001407490?ref_src=twsrc%5Etfw">2019年6月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

｢AWSの公式イメージは無いし､野良イメージはメンテされてないことが多いので自作した｣というような内容です｡
そしてブログ本文余談2に､以下のようなことが書いてありました｡
>この手の野良イメージは全然メンテされずに古くなっていくのが常ですが、awscli-allを含めて僕がメンテしてるDockerイメージは全部CircleCIで定期ビルドして新しいバージョンが出たらtagをpushして全自動で新しいイメージをリリースしているので、CircleCIが死なない限りは勝手にイメージがリリースされていくので安心してください。

CircleCIで定期的に最新バージョンを確認して､新しくなっていたらそのバージョンでDockerfileを書き換え､tagを付けてDocker Hubでビルドしているようです｡

ちょっと興味があったので`aws-all`のリポジトリを参考に､自作イメージをDocker Hubにアップしてみました｡

<a href="https://github.com/KeisukeKudo/docker-elm"><img src="https://github-link-card.s3.ap-northeast-1.amazonaws.com/KeisukeKudo/docker-elm.png" width="400px"></a>

# 事前準備
- 以下サービスのアカウントを取得
  - [GitHub](https://github.com/)
  - [CircleCI(GitHubアカウントでログイン)](https://circleci.com/)
  - [Docker Hub](https://hub.docker.com/)


# Dockerfile作成
まずDockerfileを作ります｡
なんでもいいのですが､最近ちょくちょく名前を見かける[Elm](https://elm-lang.org/)をインストールしたイメージにしたいと思います｡

```dockerfile:Dockerfile
FROM node:lts-alpine

ENV ELM_VERSION 0.19.0

RUN apk --update --no-cache add bash \
  && wget https://github.com/elm/compiler/releases/download/${ELM_VERSION}/binaries-for-linux.tar.gz \
  && tar -xzf binaries-for-linux.tar.gz \
  && rm binaries-for-linux.tar.gz \
  && mv elm /usr/local/bin/
```
何の変哲もないDockerfileです｡
ベースイメージはDocker公式の`Alpine Linux`な`Node.js(LTS)`
`Elm`は`npm`で入れても良かったのですが､うまくインストールできなかったりするケースがあるようなので､バイナリをインストールします｡

とりあえず動くか検証

```bash
$ docker build --rm -t elm .
$ docker run -it --rm elm bash -c "elm"
# bashをインストールしない場合は以下
# docker run -it --rm elm ash -c "elm"

^@^@Hi, thank you for trying out Elm 0.19.0. I hope you like it!

-------------------------------------------------------------------------------
I highly recommend working through <https://guide.elm-lang.org> to get started.
It teaches many important concepts, including how to use `elm` in the terminal.
-------------------------------------------------------------------------------

The most common commands are:

    elm repl
        Open up an interactive programming session. Type in Elm expressions like
        (2 + 2) or (String.length "test") and see if they equal four!
    
    elm init
        Start an Elm project. It creates a starter elm.json file and provides a
        link explaining what to do from there.
    
    elm reactor
        Compile code with a click. It opens a file viewer in your browser, and
        when you click on an Elm file, it compiles and you see the result.

There are a bunch of other commands as well though. Here is a full list:

    elm repl    --help
    elm init    --help
    elm reactor --help
    elm make    --help
    elm install --help
    elm bump    --help
    elm diff    --help
    elm publish --help

Adding the --help flag gives a bunch of additional details about each one.

Be sure to ask on the Elm slack if you run into trouble! Folks are friendly and
happy to help out. They hang out there because it is fun, so be kind to get the
best results!
```
つらつらとヘルプが表示されればOKです｡

確認が終わったら､GitHubに新規リポジトリを作成し､上記のDockerfileを`push`してください｡
今回は`docker-elm`というリポジトリを作成しました｡

# Docker Hub
Docker Hubにリポジトリを作成します｡
アカウントを持っていない場合は簡単に登録できるので以下からどうぞ
https://hub.docker.com/signup

## GitHubと連携する
Docker Hubにログインし､[マイページ > Account Settings](https://cloud.docker.com/settings)へアクセスします｡
ページ中程にある`Linked Accounts > GitHub > Connect`をクリックしてDocker HubとGitHubを連携させます｡
![cloud.docker.com_settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/dc0d04b9-5af2-02f6-5716-2b7fda422e52.png)

## Docker HubのリポジトリとGitHubのリモートリポジトリを連携する
次に､[Repositories](https://cloud.docker.com/repository/list)にアクセス
`Create Repository`をクリック
![cloud.docker.com_repository_list.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/3e91603d-681a-12b3-98ec-3be215d98220.png)

リポジトリ名は`アカウント名/elm`とします｡
![cloud.docker.com_settings (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/8bd2a3fe-c20d-88be-427a-f2b41fe16979.png)

次にGitHubのリモートリポジトリを設定します｡
GitHubの`Connected`をクリック
アカウント名と対象リモートリポジトリを選択
`BUILD RULES`の`+`ボタンをクリックして以下を追加してください｡

|Source Type|Source|Docker Tag|
| --- | --- |---|
|Tag|/^[0-9.]+/|{sourceref} |

- `git tag v1.0.0`などにしたい場合は`/^v[0-9.]+/`に変更してください｡
- Docker Hubに表示されるタグ名にプレフィックスを付けたい場合は､`prefix-{sourceref}`などに変更してください｡
![cloud.docker.com_settings (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/2d319452-0f66-0480-54ff-2b4286c2f106.png)

## イメージをビルドする
上記の入力が完了したら`Create & Build`
少しすると`Repositories > user_name/repository_name > Builds`にビルド結果が表示されます｡
https://cloud.docker.com/repository/docker/user_name/repository_name/builds
![cloud.docker.com_repository_docker_leoi7_elm_builds.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/a3840431-8651-e022-7ffe-ddcf976eb7fc.png)
上記スクショでは`{sourceref}`のビルドステータスが`SUCCESS`になっていますが､現時点ではビルドは実行されないはずなので､`latest`のみが`SUCCESS`になっていればOKです｡

`Repositories > user_name/repository_name > Tags`も見てみましょう｡
https://cloud.docker.com/repository/docker/user_name/repository_name/tags
![cloud.docker.com_repository_docker_leoi7_elm_tags.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/d4d62127-e3db-5cf8-59f5-c884e3809bd6.png)
こちらも`latest`のみが表示されていると思います｡

## Tagを付けてビルドする
適当にタグを付けて`push`します｡

```bash
$ git tag -a "0.0.0" -m "Elm 0.0.0"
$ git push origin --tags
```
`Repositories > user_name/repository_name > Builds`の`{sourceref}`のビルドステータスがSUCCESSに､
`Repositories > user_name/repository_name > Tags`に`0.0.0`が追加されているはずです｡


# Circle CI
Circle CIを設定します｡
アカウントを持っていない場合はGitHubアカウントでログインしてください｡
https://circleci.com/vcs-authorize/

## .circleci/config.ymlを作成
Dockerfileと同じ階層に`.circleci/config.yml`を作成します｡
内容は[sue445/dockerfile-awscli-all](https://github.com/sue445/dockerfile-awscli-all/blob/master/.circleci/config.yml)を大いに参考にさせていただきました､ありがとうございます｡

```.circleci/config.yml
version: 2.1

commands:
  build_with_cache:
    parameters:
      image_name:
        type: string

      cache_key_prefix:
        type: string
        default: "v1"

    steps:
      - restore_cache:
          keys:
            - << parameters.cache_key_prefix >>-{{ .Branch }}-
            - << parameters.cache_key_prefix >>-
          paths:
            - /caches/image.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/image.tar | true
      - run:
          name: Build Docker image
          command: docker build --cache-from=image -t << parameters.image_name >> .
      - run:
          name: Save Docker image layer cache
          command: |
            mkdir -p /caches
            docker save -o /caches/image.tar << parameters.image_name >>
      - save_cache:
          key: << parameters.cache_key_prefix >>-{{ .Branch }}-{{ epoch }}
          paths:
            - /caches/image.tar
      - run:
          name: Show docker image size
          command: |
            docker images
            ls -l /caches/image.tar
jobs:
  build:
    working_directory: /app
    docker:
      - image: docker:stable
    steps:
      - checkout
      - setup_remote_docker
      - build_with_cache:
          image_name: elm
          cache_key_prefix: v1
      - run:
          name: Run tests
          command: docker run -it --rm elm bash -c "elm"
  update_version:
    working_directory: /app
    docker:
      - image: alpine
    steps:
      - run:
          name: Install package
          command: apk --update --no-cache add bash git openssh-client jq curl

      - checkout

      - run:
          name: Setup git
          command: |
            git config push.default current
            git config user.name "CircleCI"
            git config user.email ${EMAIL}
      - run:
          name: Elm version up
          command: |
            VERSION=`curl -s https://api.github.com/repos/elm/compiler/tags | jq --raw-output .[0].name`
            echo "Elm version => ${VERSION}"
            count=`curl https://github.com/elm/compiler/releases/download/${VERSION}/binary-for-linux-64-bit.gz -o /dev/null -w '%{http_code}\n' -s | grep -i '302' | wc -l`
            if [ ${count} == "0" ]; then
              echo "${VERSION} is not released yet"
              exit 0
            fi
            # Edit Dockerfile
            sed -ie "s/ENV ELM_VERSION .*$/ENV ELM_VERSION ${VERSION}/g" Dockerfile
            git add Dockerfile
            set +o pipefail
            git commit -m "update: circleci has updated elm version to ${VERSION}" | true
            git tag -a ${VERSION} -m "Elm ${VERSION}" | true
            set -o pipefail
            git push origin --tags
            git push origin
workflows:
  version: 2

  build:
    jobs:
      - build

  daily_update:
    triggers:
      - schedule:
          # START UTC 21:00(JST 6:00)
          cron: "0 21 * * *"
          filters:
            branches:
              only: master
    jobs:
      - update_version
```

順番に見ていきたいと思います｡


### Config version: 2.1
`version: 2.1`を設定します｡
2.1では`commands`や`wordflows`などの便利な記述方法を使えるようになります｡
詳細は以下
[CircleCI-Public](https://github.com/CircleCI-Public/config-preview-sdk/tree/754df2258d293775722362694d7fefa8fda5d7a0/docs)
[CircleCI 2.1 の新機能を使って冗長な config.yml をすっきりさせよう！](https://tech.recruit-mp.co.jp/dev-tools/post-14868/)

### workflows

```.circleci/config.yml
workflows:
  version: 2

  build:
    jobs:
      - build

  daily_update:
    triggers:
      - schedule:
          # START UTC 21:00(JST 6:00)
          cron: "0 21 * * *"
          filters:
            branches:
              only: master
    jobs:
      - update_version
```

#### buildキー
- `git push`された際に実行する`job`設定です｡
- `jobs`には任意の関数名を指定します｡
  - 今回は`build`とします([後述](https://qiita.com/KeisukeKudo/items/22eabb259e0c5f68c925#jobs))｡ 

#### daily_updateキー
- `daily_update`は固定ではなく､任意のキーを設定できます｡
- `triggers`には`schedule`を設定します｡
  - cronで実行時間を指定します｡
  - Circle CIのタイムゾーンは`UTC`なので`日本時間 - 9h`した値を設定
- `filters > branches`にはビルドしたいGitブランチを指定します(今回はmasterをビルドしたいので`only: master`)｡
- `jobs`には任意の関数名を指定します｡
  - 今回は`update_version`とします([後述](https://qiita.com/KeisukeKudo/items/22eabb259e0c5f68c925#jobs))｡


### jobs

#### build(関数名)
`workflows > build > jobs`に指定した値です｡

```circleci/config.yml
jobs:
  build:
    working_directory: /app
    docker:
      - image: docker:stable
    steps:
      - checkout
      - setup_remote_docker
      - build_with_cache:
          image_name: elm
          cache_key_prefix: v1
      - run:
          name: Run tests
          command: docker run -it --rm elm bash -c "elm"
```
- ベースイメージを指定します｡
  - dockerを実行したいのでdockerが含まれるイメージを使います｡
- ソースをチェックアウト
- [dockerを実行できるように`setup_remote_docker`を指定します｡](https://circleci.com/docs/2.0/building-docker-images/)
- 実行したい処理を書きます｡
  - `commands`で関数化した関数名を指定しています([後述](https://qiita.com/KeisukeKudo/items/22eabb259e0c5f68c925#commands))｡
- `Elm`が正しくインストールされているか確認します｡

#### update_version(関数名)
`workflows > update_version`です｡

```circleci/config.yml
jobs:
  # 省略
  update_version:
    working_directory: /app
    docker:
      - image: alpine
    steps:
      - run:
          name: Install package
          command: apk --update --no-cache add bash git openssh-client jq curl

      - checkout

      - run:
          name: Setup git
          command: |
            git config push.default current
            git config user.name "CircleCI"
            git config user.email ${EMAIL}
      - run:
          name: Elm version up
          command: |
            VERSION=`curl -s https://api.github.com/repos/elm/compiler/tags | jq --raw-output .[0].name`
            echo "Elm version => ${VERSION}"
            count=`curl -sI https://github.com/elm/compiler/releases/download/${VERSION}/binaries-for-linux.tar.gz | grep -i 'status: 302' | wc -l`
            if [ ${count} == "0" ]; then
              echo "${VERSION} is not released yet"
              exit 0
            fi
            # Edit Dockerfile
            sed -ie "s/ENV ELM_VERSION .*$/ENV ELM_VERSION ${VERSION}/g" Dockerfile
            git add Dockerfile
            set +o pipefail
            git commit -m "update: circleci has updated elm version to ${VERSION}" | true
            git tag -a ${VERSION} -m "Elm ${VERSION}" | true
            set -o pipefail
            git push origin --tags
            git push origin
```
- ベースイメージを指定します｡
- 処理に必要なパッケージをインストールします｡
- ソースをチェックアウト
- Gitを設定します｡
- `Elm`をインストールします｡
  - [GitHub API](https://developer.github.com/v3/git/tags/)を叩いてリリースされているタグ一覧を取得､末尾(最新)のタグ名を変数に代入
  - 上記タグ名を元に､リモートリポジトリのバイナリダウンロードURLを作成､疎通を確認
  - Dockerfileの`ENV ELM_VERSION XXX`を`ENV ELM_VERSION ${VERSION}`で上書き
  - `git commit`
  - `git tag`
  - `git push`

もし`npm install -g elm`でDockerfileを書いている場合は､`npm info elm versions --json`で取得したほうがいいかもしれません｡
※ 2019/06/17 現在､最新版は`0.19.0`なのに`1.2.0~2.0.0`が取得できる......なんでや......


### commands

#### build_with_cache

`workflows > daily_update > jobs`に指定した値です｡

```circleci/config.yml
commands:
  build_with_cache:
    parameters:
      image_name:
        type: string

      cache_key_prefix:
        type: string
        default: "v1"

    steps:
      - restore_cache:
          keys:
            - << parameters.cache_key_prefix >>-{{ .Branch }}-
            - << parameters.cache_key_prefix >>-
          paths:
            - /caches/image.tar
      - run:
          name: Load Docker image layer cache
          command: |
            set +o pipefail
            docker load -i /caches/image.tar | true
      - run:
          name: Build Docker image
          command: docker build --cache-from=image -t << parameters.image_name >> .
      - run:
          name: Save Docker image layer cache
          command: |
            mkdir -p /caches
            docker save -o /caches/image.tar << parameters.image_name >>
      - save_cache:
          key: << parameters.cache_key_prefix >>-{{ .Branch }}-{{ epoch }}
          paths:
            - /caches/image.tar
      - run:
          name: Show docker image size
          command: |
            docker images
            ls -l /caches/image.tar
```

##### parametersキー
- 引数を設定しています｡
  - `image_name`, `cache_key_prefix`は任意の名前に変更できます｡

##### stepsキー
- キャッシュをリストアします｡
  - `keys`はキャッシュのキーです｡
  - 複数指定する理由は､他のブランチやcommitのキャッシュを探すためです｡
  - `{{ .Branch }}`の意味や他のテンプレートは[公式ドキュメント](https://circleci.com/docs/ja/2.0/caching/)を参照してください｡
- `docker save`したイメージキャッシュを使ってビルドします｡
- ビルド完了後､作成したイメージをキャッシュします｡
  - `save_cache > key`は一意にするために`{{ epoch }}`を付与
- イメージのサイズを表示

上記を作成したら､`commit`してリモートリポジトリへ`push`してください｡

## Circle CIを設定
[Circle CIのダッシュボード](https://circleci.com/dashboard)へアクセスし､GitHubのリモートリポジトリを設定します｡
`ADD PROJECTS`をクリックし､対象リモートリポジトリの`Set Up Project`をクリック
![circleci.com_add-projects_gh_KeisukeKudo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/f1191328-252d-dfa4-af7d-5503ac53d7ea.png)
最下部にある`Start Building`をクリック
![circleci.com_add-projects_gh_KeisukeKudo (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/c32a0d3b-4834-9f75-28eb-c106a9dee53d.png)
Jobsページを確認すると､ビルドが開始されているはずです｡
https://circleci.com/gh/user_name/repository_name

ですが､初回ビルドは失敗すると思います｡
続けて以下の設定をします｡

### SSH Keyを設定する
`update_version`の処理の中で､`git push`していますが､`SSH Key`が無いため`push`できません｡
`git push`を実行するために､`SSH Key`を設定します｡
`Settings > PERMISSONS > Checkout SSH Keys`の`Create and user_name user key`をクリック
`SSH Key`が生成､設定されます｡
https://circleci.com/gh/user_name/repository_name/edit#checkout
![circleci.com_gh_KeisukeKudo_docker-elm_edit (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/a829b397-da9e-bd12-2cb1-288214aa5db7.png)
![github.com_settings_keys.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/fff798ac-81c4-2d5a-c648-e9487b6be47a.png)

### 環境変数を設定する
`update_version`の処理の中で､`git config`を設定していますが､`email`は環境変数から取得しているため､環境変数を設定します｡
`Settings > BUILD SETTINGS > Environment Variables`の`Add Variable`をクリック
`Name`に`EMAIL`､`Value`に任意のメールアドレスを設定して`Add Variable`

### 再度ビルドする
失敗したビルドをクリックして､右上の`Rerun workflow`をクリック
再度ビルドが走ります｡
※下記スクショはエラー内容が違いますが気にしないでください｡
![circleci.com_add-projects_gh_KeisukeKudo (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/48b6f527-7d90-8056-7ede-e3e7f2e75b58.png)
今度は成功しているはずです｡
以下のように表示されればOK
![circleci.com_gh_KeisukeKudo_docker-elm_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/7dd5225a-94f6-5557-41bd-fbc5db75992c.png)

### 日時処理
`daily_update`のスケージュールで設定した時間になると`update_version`が開始されます｡
![circleci.com_dashboard (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/7c297ff0-6d35-8eba-1ced-6facf7978e04.png)
Docker Hubも確認してみましょう｡
![cloud.docker.com_repository_docker_leoi7_elm_tags (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74711/3e912a12-29c2-4b64-6394-baea031de908.png)
`0.19.0`のタグが増えているはずです｡

今後毎日指定した時間に`update_version`が走り､`Elm`のリモートリポジトリに新規バージョンがプッシュされていれば自動でバージョンを書き換え､タグ付けして自身のリモートリポジトリに`push`､Docker Hubでビルドされることになります｡
これで少なくとも自分都合でDockerfileのバージョンアップがされない､という状況にはならないですね!

# 参考
[くりにっき: awscli-allを作った](https://sue445.hatenablog.com/entry/2019/06/13/221004)
[https://github.com/sue445/dockerfile-awscli-all](https://github.com/sue445/dockerfile-awscli-all)
[CircleCI Documentation](https://circleci.com/docs/)
[CircleCI-Public](https://github.com/CircleCI-Public/config-preview-sdk/tree/754df2258d293775722362694d7fefa8fda5d7a0/docs)
[CircleCI 2.1 の新機能を使って冗長な config.yml をすっきりさせよう！](https://tech.recruit-mp.co.jp/dev-tools/post-14868/)