DockerHubが無課金で自動ビルド出来なくなったのでCircleCIからDockerイメージビルドしてプッシュする

# DockerHubが無課金で自動ビルド出来なくなった

[公式ブログの記事](https://www.docker.com/blog/changes-to-docker-hub-autobuilds/)によるとDockerHubの自動ビルドが暗号通貨マイニングに悪用されているため、2021年6月18日から無料利用枠でのAutobuildを廃止したとのこと。

# CircleCIでDockerイメージビルドしてDockerHubにプッシュする

DockerHubには課金せず、CircleCIでDockerイメージをビルドしてDockerHubにプッシュする方法を考える。
CircleCIで使用する自前executorが必要なので、本末転倒だがCirleCIでビルドしてDockerHubに置くことにする。
手順は以下の通り

1. GitHubにレポジトリを作って`.circleci/config.yml`と`Dockerfile`を置く
1. DockerHubにレポジトリを用意
1. CirleCIに環境変数と登録しビルド〜プッシュを実行

## .circleci/config.ymlの構成

GitHubのレポジトリにアップした`config.yml`は[こちら](https://github.com/horidaisuke/awscli-executor/blob/main/.circleci/config.yml)。
再利用性を高めるため以下の3つのブロックで構成している。

### commands

`commands`は5つ用意した。

1. `command-docker_checksum`はコンテキストディレクトリからキャッシュ用のチェックサムを生成する
1. `command-docker_build`はビルドを実行する
1. `command-docker_load`はキャッシュをロードする
1. `command-docker_save`はキャッシュを保存する
1. `command-docker_push`はプッシュを実行する

```yaml:commands
commands:
  command-docker_checksum:
    parameters:
      context:
        type: string
    steps:
      - run:
          name: generate checksum.txt
          command: |
            rm -f /tmp/checksum.txt
            cat $(find << parameters.context >> -type f | sort) >> /tmp/checksum.txt
  command-docker_build:
    parameters:
      context:
        type: string
      dockerfile:
        type: string
        default: Dockerfile
      target:
        type: string
        default: ""
      image:
        type: string
      tag:
        type: string
        default: build
    steps:
      - run:
          name: docker login
          command: |
            docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
      - run:
          name: docker build
          command: |
            if [ -z "<< parameters.target >>" ]; then
              docker build --tag=<< parameters.image >>:<< parameters.tag >> --file=<< parameters.context >>/<< parameters.dockerfile >> << parameters.context >>
            else
              docker build --tag=<< parameters.image >>:<< parameters.tag >> --file=<< parameters.context >>/<< parameters.dockerfile >> --target << parameters.target >> << parameters.context >>
            fi
  command-docker_load:
    parameters:
      workdir:
        type: string
        default: /tmp/docker
      image:
        type: string
      tag:
        type: string
        default: build
    steps:
      - run:
          name: docker load
          command: |
            if [ -f "<< parameters.workdir >>/$(basename '<< parameters.image >>')_<< parameters.tag >>.tgz" ]; then
              gunzip -c << parameters.workdir >>/$(basename '<< parameters.image >>')_<< parameters.tag >>.tgz | docker load
              docker images << parameters.image >>:<< parameters.tag >>
            else
              echo "no cache"
            fi
  command-docker_save:
    parameters:
      workdir:
        type: string
        default: /tmp/docker
      image:
        type: string
      tag:
        type: string
        default: build
    steps:
      - run:
          name: docker save
          command: |
            if [ -n "$(docker images -q << parameters.image >>:<< parameters.tag >>)" ]; then
              mkdir -p << parameters.workdir >>
              docker save << parameters.image >>:<< parameters.tag >> $(docker history -q << parameters.image >>:<< parameters.tag >> | tail -n +2 | grep -v \<missing\> | tr '\n' ' ') | gzip > << parameters.workdir >>/$(basename '<< parameters.image >>')_<< parameters.tag >>.tgz
            else
              exit 1
            fi
  command-docker_push:
    parameters:
      image:
        type: string
      tag:
        type: string
        default: build
      repo:
        type: string
    steps:
      - run:
          name: docker login
          command: |
            docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
      - run:
          name: docker push
          command: |
            if [ -n "$(docker images -q << parameters.image >>:<< parameters.tag >>)" ]; then
              if [ -n "${CIRCLE_TAG}" ]; then
                docker tag << parameters.image >>:<< parameters.tag >> ${DOCKER_LOGIN}/<< parameters.repo >>:${CIRCLE_TAG}
                docker images ${DOCKER_LOGIN}/<< parameters.repo >>:${CIRCLE_TAG}
                docker push ${DOCKER_LOGIN}/<< parameters.repo >>:${CIRCLE_TAG}
              else
                docker tag << parameters.image >>:<< parameters.tag >> ${DOCKER_LOGIN}/<< parameters.repo >>:latest
                docker images ${DOCKER_LOGIN}/<< parameters.repo >>:latest
                docker push ${DOCKER_LOGIN}/<< parameters.repo >>:latest
              fi
            else
              exit 1
            fi
```

### executors

`docker build`と`docker push`を使いたいのでDocker公式イメージを`executors`として利用する。

```yaml:executors
executors:
  docker:
    docker:
      - image: library/docker:latest
        auth:
          username: $DOCKER_LOGIN
          password: $DOCKER_PASSWORD
```

### jobsとworkflows

`commands`に渡す`context`と`image`はGitHubレポジトリと同じ。キャッシュ名も同様。

```yaml:jobs
jobs:
  docker_build:
    executor:
      name: docker
    steps:
      - setup_remote_docker
      - checkout
      - command-docker_checksum:
          context: awscli-executor
      - restore_cache:
          keys:
            - awscli-executor-v1-{{ checksum "/tmp/checksum.txt" }}
            - awscli-executor-v1
      - command-docker_load:
          image: awscli-executor
      - command-docker_build:
          context: awscli-executor
          image: awscli-executor
      - command-docker_save:
          image: awscli-executor
      - save_cache:
          key: awscli-executor-v1-{{ checksum "/tmp/checksum.txt" }}
          paths:
            - /tmp/docker
      - persist_to_workspace:
          root: /tmp
          paths:
            - docker
  docker_push:
    executor:
      name: docker
    steps:
      - setup_remote_docker
      - checkout
      - command-docker_checksum:
          context: awscli-executor
      - restore_cache:
          keys:
            - awscli-executor-v1-{{ checksum "/tmp/checksum.txt" }}
            - awscli-executor-v1
      - command-docker_load:
          image: awscli-executor
      - command-docker_push:
          image: awscli-executor
          repo: awscli-executor
```

`workflows`は`main`ブランチは`latest`としてタグはタグ名でDockerHubにDockerイメージがプッシュされる。

```yaml:workflows
workflows:
  version: 2
  default:
    jobs:
      - docker_build:
          filters:
            tags:
              only: /.*/
      - docker_push:
          requires:
            - docker_build
          filters:
            tags:
              only: /.*/
            branches:
              only: main
```

## Dockerfile

AWS CLIのexecutorの`Dockerfile`を`config.yml`に書いた`context`と同じ名前のディレクトリに配置する。

```Dockerfile:Dockerfile
FROM library/docker:19.03.15
ENV AWS_CLI_VERSION 2.0.63
ENV GLIBC_VER 2.31-r0
RUN apk update \
 && apk add --no-cache \
    bash \
    curl \
 && curl -sL https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub -o /etc/apk/keys/sgerrand.rsa.pub \
 && curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-${GLIBC_VER}.apk \
 && curl -sLO https://github.com/sgerrand/alpine-pkg-glibc/releases/download/${GLIBC_VER}/glibc-bin-${GLIBC_VER}.apk \
 && apk add --no-cache \
    glibc-${GLIBC_VER}.apk \
    glibc-bin-${GLIBC_VER}.apk \
 && apk add --virtual .build-deps \
    binutils \
    unzip \
    zip \
 && curl -sL https://awscli.amazonaws.com/awscli-exe-linux-x86_64-${AWS_CLI_VERSION}.zip -o awscliv2.zip \
 && unzip -q awscliv2.zip \
 && ./aws/install \
 && rm -f awscliv2.zip \
 && apk del .build-deps \
 && rm -rf /var/cache/apk/*
```

## DockerHubにレポジトリを用意

`config.yml`に書いた`image`名と同じ名前の空のレポジトリを作成するだけ。

## CirleCIの設定

作成したGitHubレポジトリを取り込んでDockerHubログインの情報を環境変数（`DOCKER_LOGIN`と`DOCKER_PASSWORD`）として登録する。

![20211010153010.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61005/3b717ec6-0be0-4784-0dc2-bf394edd7282.png)

## DockerHubにプッシュしたイメージの確認

これでGitHubへのプッシュすれば今までと同じようにDockerHubにビルドイメージが自動でアップされる。

![20211010161427.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61005/9018cb23-8968-638d-dcb1-1fdbb102e6a2.png)