My External Storage

Apr 21, 2019 - 7 minute read - Comments - go circleci

[Go] CicleCI2.1でgo modのデータを共有しながら複数ジョブを実行する

GitHub上のGoのリポジトリに対して継続的インテグレーション(CI)を行なう場合、CircleCIやTravisCIを使うのが一般的だろう。
CicrcleCI2.1でGo Modulesを使いながらマルチJobを定義したWorkflowを定義する。
attach_workspaceを使ってジョブ間のデータ共有をするのにひと手間必要だった。

TL;DR

  • CircleCI2.0はWorkflowを使って複数Jobを平行実行することができる
  • save_cacheを使うとWorkflowを実行するたびにModuleをダウンロードせずに済む
  • persist_to_workspaceを使うと各ジョブで毎回Moduleをダウンロードせずに済む
    • attach_workspaceするときにuser:rootの設定が必要な場合がある
  • めんどくさいときは@__timakin__さんのCircleCI Orbを使えば簡単

  • timakin/go-module | CircleCI Orb Registry

文中で部分参照している.circleci/config.ymlの全文やCircleCIの実行結果は以下になる。

CircleCIでGoのCIを定義する

まずCircleCIでGoを実行するときの基本的な設定方法は公式ページで参照することができる。

GO111MODULE=onとしていた場合、)Go1.11からGo Moudlesが利用できるようになっているので、 テストなどを実行する前に依存パッケージをダウンロードする必要がある。

また、CIはなるべく短時間で実行を終わらせたいので、サンプルの設定とは別に以下の設定をして実行時間を短くしていく。

Workflowを利用して処理を平行化する。

CircleCI2.0から、1回のCIの実行の中にWorkflowと言う概念と、Jobという概念が加わった。

一回のCI実行がWorkflowという単位になり、Workflowの中に複数のJobというステージを作成することができる。
そして、「ジョブBはジョブAが完了したあとに実行する」、「ジョブBとジョブCは同時に実行できる」などの設定ができるようになる。
今回はWorkflowの中に以下のジョブを定義する。

  • setupジョブ
    • go mod dwonloadを行なって外部ライブラリのコードをインストールしておくジョブ
  • e2eジョブ
    • go testを実行するジョブ
  • lintジョブ
    • go lintなどを実行して静的解析を行なうジョブ

setupジョブでgo mod downloadなどの他のジョブが実行するために必要な処理を全て済ませておく(他のジョブにダウンロードしたデータの共有などをするには、後述するpersist_to_workspaceの設定が必要)。 e2eジョブとlintジョブは互いの実行結果を必要としないので、同時に実行することができる。 並行に実行できるようにしておくことで、Workflow全体の実行時間を短縮することができる。

3つのジョブを定義してジョブ間の依存関係を定義すると.circleci/config.ymlは以下のような設定になる。

version: 2.1
executors:
  default:
    docker:
      # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/
      - image: circleci/golang:1.12.2
    working_directory: /go/src/github.com/budougumi0617/caww
    environment:
      - GO111MODULE: "on"

jobs:
  # 事前にもろもろのダウンロードなどを済ませておくジョブ
  setup:
    executor:
      name: default
    steps:
      - checkout
      - run:
          name: Vendoring
          command: go mod download

  # e2eテストを実行するジョブ
  e2e:
    executor:
      name: default
    steps:
      # テストの実行、結果のアップロードなど...

  # linterを実行するジョブ。e2eジョブとは並行で実行することができる
  lint:
    executor:
      name: default
    steps:
      # 静的解析の実行など...

workflows:
  build-and-test:
    jobs:
      - setup
      - e2e:
          # e2e jobはsetupジョブが完了後に実行される
          requires:
            - setup
      - lint:
          # lint jobはsetupジョブが完了後に実行される
          requires:
            - setup

save_cacheを使ってgo mod downloadの結果を保存しておく

毎回Worlflowを実行するたびにmodulesのダウンロードをするのは時間がかかる。ここで、CircleCIには前回のWorkflowで使ったデータをキャッシュしておく機能がある。

go.modファイルの内容が同じ(checksumした結果が同じ)だったら前回のWorkflowでダウンロードした結果を再利用してダウンロード時間の短縮を行なう。

jobs:
  setup:
    executor:
      name: default
    steps:
      - checkout
      # もし前回のWorkflow時に保存したキャッシュが利用できるなら再利用する
      - restore_cache:
          name: Restore go modules cache
          keys:
              - v1-mod-{{ .Branch }}-{{ checksum "go.mod" }}
      - run:
          name: Vendoring
          command: go mod download
      # ダウンロードしたmoduleをCircleCIにキャッシュとして保存しておく
      - save_cache:
          name: Save go modules cache
          key: v1-mod-{{ .Branch }}-{{ checksum "go.mod" }}
          paths:
              - /go/pkg/mod/cache

go modを使ってダウンロードされるmoduleは$GOPATH/pkg/mod/cacheに保存される。CircleCI公式のGo実行イメージの$GOPATH/goなので/go/pkg/mod/cacheをキャッシュすることになる。

persist_to_workspaceを使ってジョブ間でデータを共有する

e2eジョブの前にsetupジョブを実行してgo mod downloadをしておくだけでは、e2eジョブでsetupジョブがダウンロードした情報を利用することができない。 ジョブ間でデータを共有するためにはpersist_to_workspaceattach_workspaceを利用する。

jobs:
  setup:
    executor:
      name: default
    steps:
      # 前述したgo mod downloadなどの処理
      - persist_to_workspace:
          root: /go
          paths:
            - src
            - bin
            - pkg/mod/cache

  e2e:
    executor:
      name: with_mysql
    steps:
      - attach_workspace:
          at: /go
      # テストの実行、結果のアップロードなど...

setupジョブにpersist_to_workspaceを設定し、ジョブ間で共有したいデータの場所を設定しておく。e2eジョブ(lintジョブ)にattach_workspaceを設定し、共有したいデータをマウントする場所を設定する。 前述したとおり$GOPATH/goなので、/go配下のデータを全て共有する。


2019/04/22追記
go modのキャッシュだけを再利用するだけならばrestore_cacheだけすればよいという指摘を頂いた。たしかに…!

追記ここまで。


attach_workspaceしようとするとError applying workspace layer for job …: Error extracting tarball …: exit status 2

これでうまくいくかと思いきや、実際にCircleCIを実行してみるとattach_workspaceがうまくいかなかった。

Downloading workspace layers
  workflows/workspaces/2a7d4f28-c4f5-477a-90ab-190809d6e4f9/0/c5acd0fa-dc09-4e94-83b8-5e1f2e8a0e96/0/109.tar.gz - 171 MB
Applying workspace layers
  c5acd0fa-dc09-4e94-83b8-5e1f2e8a0e96

Error applying workspace layer for job c5acd0fa-dc09-4e94-83b8-5e1f2e8a0e96: Error extracting tarball /tmp/workspace-layer-c5acd0fa-dc09-4e94-83b8-5e1f2e8a0e96985056964: exit status 2

ググっても、データ量が多すぎるとこのようなエラーになる、というような解説が出てくるが、171MBくらいでなることはないらしい。

ジョブをrootユーザで実行するようにすれば良い

結論から言うと、単純にcircleciユーザで解凍したデータをrootユーザ所有の/goディレクトリ以下に展開しようとしていたのが原因だった。 circleci/golangイメージでは、/gorootユーザ所有らしい。

dockerの設定にuser: rootとしておくと、各Jobの実行ユーザがrootユーザになるのでattach_workspaceが成功するようになった。

version: 2.1
executors:
  default:
    docker:
      # CircleCI Go images available at: https://hub.docker.com/r/circleci/golang/
      - image: circleci/golang:1.12.2
        user: root
    working_directory: /go/src/github.com/budougumi0617/caww
    environment:
      - GO111MODULE: "on"

完成した.circleci/config.yml

以上の設定を全て行った.circleci/config.ymlは以下のようになる。

version: 2.1
executors:
  default:
    docker:
      - image: circleci/golang:1.12.2
        user: root
    working_directory: /go/src/github.com/budougumi0617/caww
    environment:
      - GO111MODULE: "on"

jobs:
  setup:
    executor:
      name: default
    steps:
      - checkout
      - restore_cache:
          name: Restore go modules cache
          keys:
              - v1-mod-{{ .Branch }}-{{ checksum "go.mod" }}
      - run:
          name: Vendoring
          command: go mod download
      - save_cache:
          name: Save go modules cache
          key: v1-mod-{{ .Branch }}-{{ checksum "go.mod" }}
          paths:
              - /go/pkg/mod/cache
      - persist_to_workspace:
          root: /go
          paths:
            - src
            - bin
            - pkg/mod/cache

  e2e:
    executor:
      name: default
    steps:
      - attach_workspace:
          at: /go
      # 以下テスト実行など

  # lint
  lint:
    executor:
      name: default
    steps:
      - attach_workspace:
          at: /go
      # 以下静的解析実行など

workflows:
  build-and-test:
    jobs:
      - setup
      - e2e:
          requires:
            - setup

実際に動いているの結果を見たい場合は以下を確認すること(GitHub上の設定にはMySQLを使う設定なども含まれている)。

終わりに

publicなGitHubリポジトリにコードをプッシュしておけば、CircleCIなど多くのサービスを利用してOSS開発ができる。
積極的に利用していきたい。なお、「上記の設定をいろいろなリポジトリで毎回するのがめんどくさい」というような方には@__timakin__さんのCircleCI Orbを使えば簡単に設定できる。

参考

関連記事