circleciのbuild/test/deployをgithub actions(beta)に移行した

まだ機能的に足りないところもあるが、頑張ったら使える感覚だった。
githubにもfeedbackが送れる所があれば送ろうと思う。

circleciでやっていたことはざっくり書くと以下。

  • test系
    • golangのbuild/lint/test
    • helm chartのlint
    • helm templateで吐き出されたyamlのlint
  • build系(only master)
    • base imageのbuild & push
    • k8s上で動かすprod imageのbuild & push
  • deploy系(only master)
    • GKE上にhelm secrets upgrade

これをgithub actionsに移行した際にcircleciとの差分を感じた機能は以下。

  • slack通知
  • filter機能
    • yml単位でしかbranchのfilter機能が存在しない
      • 1つのworkflowに複数jobを定義して、jobの依存関係を持たせているときに困る
      • e.g. featureブランチではtest jobだけ走らせるが、masterブランチではtest/buildを並列して走らせ、両方とも成功したらdeployを行う
    • circleciのようにjob単位でbranch filterが行えるようになっていてほしい
      • workflow(yml)を分けた場合は別workflowのjobの状態検知ができないことは確認
  • cache機能
    • azure pipelineの方には一応あるようだが、github actionsにはドキュメントを読んだ感じだと見当たらなかった
    • circleciはfreeかつprivateだと並列に走らせられないが、github actionsは並列実行できるので結果的にトントンぐらいの速度で終わっている
      • circleciではbuild/testが直列だったが、github actionsになって並列実行になったためトータルの待ち時間が減ったがキャッシュがないのでjob単位の実行速度は伸びた
    • dirty hackをすればおそらくキャッシュも可能
  • artifacts
  • environment variable
    • step単位でしか指定できないので全てのstepで指定したいenvironment variableがあると記述が冗長になる
  • secrets
    • env keyでのみ指定できる模様
      • circleciはenvとsecretが同一のような扱い(envは管理画面でもfilterされる)だが、github actionsはやや扱いが異なる
    • 複数回使うsteps.ifの比較値をsecrets経由で一元管理にしようとしたが、使えないらしく弾かれた
  • dockerのprepare
    • circleciでは明示的に指定する必要があったが、github actionsでは特にしていなく使えるのはDXがよかった
  • merge時の挙動
    • github actionsはmergeした際は2回actionが走る模様
      • github.event.afterが0000000000000000000000000000000000000000だったり、github.event.commitsが空配列のようなactionを確認
    • これは不具合かも?と思ったが何か理由があるのかもしれない、自分で使う際は2回走ると完全に無駄なのでsteps.ifでfilterしている
      • twitterで教えてもらったがbranch delete eventを拾っていた、github contextを見てみると確かにgithub.event.deleted.がtrueになっていた*4

yml自体の書き味の違いはざっくりと書くと以下の感じ。

circleci

version: 2.1
orbs:
  gcr: circleci/gcp-gcr@0.6.1
  gke: circleci/gcp-gke@0.2.0
  docker: circleci/docker@0.5.13
jobs:
  test-job:
    working_directory: /go/src/github.com/8398a7/app
    docker:
      - image: circleci/golang:1.12.6-stretch
        environment:
          CI: "true"
          GO111MODULE: "on"
    steps:
      - checkout
      - restore_cache:
          key: prepare-ci-tools-{{ checksum "scripts/prepare-ci-tools.sh" }}
      - run:
          name: prepare ci tools
          command: scripts/prepare-ci-tools.sh
      - save_cache:
          key: prepare-ci-tools-{{ checksum "scripts/prepare-ci-tools.sh" }}
          paths:
          - ./bin
      - run:
          name: helm-lint
          command: scripts/helm-lint.sh
      - run:
          name: kubeval
          command: scripts/kubeval.sh
      - restore_cache:
          key: go-mod-{{ checksum "go.mod" }}
      - run:
          name: go mod download
          command: go mod download
      - save_cache:
          key: go-mod-{{ checksum "go.mod" }}
          paths:
          - /go/pkg/mod/cache
      - run:
          name: golangci-lint run
          command: bin/golangci-lint run
      - run:
          name: test
          command: make test
      - run:
          name: build
          command: make build
      - store_artifacts:
          path: /go/src/github.com/8398a7/app/cover.html
  build-job:
    working_directory: /go/src/github.com/8398a7/app
    docker:
      - image: google/cloud-sdk
    steps:
      - checkout
      - setup_remote_docker
      - docker/check
      - run:
          name: pull app-base
          command: docker pull 8398a7/app-base:latest
      - docker/build:
          image: 8398a7/app-base
          dockerfile: build/Dockerfile
          extra_build_args: --cache-from 8398a7/app-base:latest
          tag: latest
      - docker/push:
          image: 8398a7/app-base
          tag: latest
      - gcr/build-image:
          image: app
          dockerfile: build/app/Dockerfile
          tag: $(get tag)
      - gcr/gcr-auth
      - gcr/push-image:
          image: app
          tag: $(get tag)
  deploy-job:
    machine: true
    environment:
        HELM_HOME: /home/circleci/.helm
        GOOGLE_APPLICATION_CREDENTIALS: /home/circleci/gcloud-service-key.json
    steps:
      - checkout
      - restore_cache:
          key: prepare-cd-tools-{{ checksum "scripts/prepare-cd-tools.sh" }}
      - run:
          name: prepare cd tools
          command: scripts/prepare-cd-tools.sh
      - save_cache:
          key: prepare-cd-tools-{{ checksum "scripts/prepare-cd-tools.sh" }}
          paths:
          - ./bin
      - gke/install
      - gke/init
      - run:
          name: get credentials
          command: gcloud container clusters get-credentials $GKE_CLUSTER
      - run:
          name: upgrade app
          command: bin/helm secrets upgrade prod-app ./deployments/app --install --wait --namespace app -f ./deployments/values.yaml -f ./deployments/secrets.yaml --set app.image.tag=$(get tag)
workflows:
  version: 2
  test-build-deploy-workflow:
    jobs:
     - test-job
     - build-job:
         filters:
           branches:
             only: master
     - deploy-job:
         requires:
            - test-job
            - build-job
         filters:
           branches:
             only: master

github actions

name: CI

on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
    - uses: actions/setup-go@v1
      with:
        version: 1.12.6
      id: go
    - run: scripts/prepare-ci-tools.sh
    - run: scripts/helm-lint.sh
      env:
        CI: true
    - run: scripts/kubeval.sh
      env:
        CI: true
    -  run: go mod download
    - run: bin/golangci-lint run
    - run: make test
    - run: make build
    - uses: actions/upload-artifact@master
      with:
        name: coverage
        path: cover.html
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@master
      if: contains(github.ref, 'master')
    - name: docker login
      run: echo $DOCKER_PASSWORD | docker login -u 8398a7 --password-stdin docker.io
      if: contains(github.ref, 'master')
      env:
        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
    - name: pull app-base
      run: docker pull 8398a7/app-base:latest
      if: contains(github.ref, 'master')
    - name: build app-base
      run: docker build -t 8398a7/app-base:latest --cache-from 8398a7/app-base:latest -f build/Dockerfile .
      if: contains(github.ref, 'master')
    - name: push app-base
      run: docker push 8398a7/app-base:latest
      if: contains(github.ref, 'master')
    - name: build gcr.io/gcp_project/app
      run: docker build -t gcr.io/gcp_project/app:$(get tag) -f build/app/Dockerfile .
      if: contains(github.ref, 'master')
    - name: install gcloud sdk
      run: |
        export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
        echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
        curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
        sudo apt-get update && sudo apt-get install -y google-cloud-sdk
      if: contains(github.ref, 'master')
    - name: initialize gcloud sdk
      run: |
        echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
        gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
        gcloud --quiet config set project gcp_project
        gcloud --quiet config set compute/zone gcp_zone
        gcloud auth configure-docker --quiet --project gcp_project
      env:
       GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}
      if: contains(github.ref, 'master')
    - name: push gcr.io/gcp_project/app
      run: docker push gcr.io/gcp_project/app:$(get tag)
      if: contains(github.ref, 'master')
  deploy:
    runs-on: ubuntu-latest
    needs: [test, build]
    steps:
    - uses: actions/checkout@master
      if: contains(github.ref, 'master')
    - name: install gcloud sdk
      run: |
        export CLOUD_SDK_REPO="cloud-sdk-$(lsb_release -c -s)"
        echo "deb http://packages.cloud.google.com/apt $CLOUD_SDK_REPO main" | sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list
        curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
        sudo apt-get update && sudo apt-get install -y google-cloud-sdk
      if: contains(github.ref, 'master')
    - name: initialize gcloud sdk
      run: |
        echo $GCLOUD_SERVICE_KEY > ${HOME}/gcloud-service-key.json
        gcloud auth activate-service-account --key-file=${HOME}/gcloud-service-key.json
        gcloud --quiet config set project gcp_project
        gcloud --quiet config set compute/zone gcp_zone
      env:
       GCLOUD_SERVICE_KEY: ${{ secrets.GCLOUD_SERVICE_KEY }}
      if: contains(github.ref, 'master')
    - run: scripts/prepare-cd-tools.sh
      env:
        HELM_HOME: /home/runner/.helm
      if: contains(github.ref, 'master')
    - name: get credentials
      run: gcloud container clusters get-credentials gke_cluster
      if: contains(github.ref, 'master')
    - name: upgrade app
      run: bin/helm secrets upgrade prod-app ./deployments/app --install --wait --namespace app -f ./deployments/values.yaml -f ./deployments/secrets.yaml --set app.image.tag=$(get tag)
      env:
        HELM_HOME: /home/runner/.helm
        GOOGLE_APPLICATION_CREDENTIALS: /home/runner/gcloud-service-key.json
      if: contains(github.ref, 'master')

見ての通り、circleciのjob単位branch filter機能がないのでそれに該当するものは全てにsteps.ifでブランチのfilterをかけている。

  • また、masterマージ時の2回発火を防ぐためgithub.event.afterでも条件を書いている。
    • また、master時にbranch delete eventでjobを走らせないためにgithub.event.deletedを見て条件を書いている。
    • 8/17頃からbranch delete eventでは発火しなくなったようなので条件を書く必要がなくなった

正直全てのstepで同じifを何度も書くのは見栄えもメンテナンス性も悪いので、job単位branch filterはほしいなぁと感じた。
ちなみにworkflowをrerunさせたときにはgithubで取得できる値がちょっと違うようで、deploy系がうまく動作しなかった。

  "ref": "refs/heads/master",
  "sha": "dd47c626ef90ca4ee193d02a1cc0a253a5ba53a6",
  "repository": "8398a7/app",
  "repositoryUrl": "git://github.com/8398a7/app.git",
  "actor": "8398a7",
  "workflow": "CI",
  "head_ref": "",
  "base_ref": "",
  "event_name": "push",
  "event": ***
    "action": "rerequested",
    "check_suite": ***
      "after": "dd47c626ef90ca4ee193d02a1cc0a253a5ba53a6",
      "app": ***

event.check_suiteの下にafterが生えるようになっていた。
"description": "Powers your .github/main.workflow.", とか書かれてるし、まだHCLのものの挙動を引きずってたりするのかもしれない…。

上記のような感じで移行してみたが、circleciのfree plan上限が割と厳しい月もあるのでgithub actionsに移行してこのあたりは解消されそうなのがよかった。
GAになるまでに改良されたり変更されたりすると思うので継続的に更新していきたいと思う。