ご無沙汰しております。GameWith でエンジニアマネージャーをしている @serima です。
8 月は弊社エンジニアが代わる代わる技術ブログを執筆してくれたので、出番が少なかったです。(とても嬉しい…)
まだ、読んでない方は是非こちらの記事もご覧ください。
- 入社して1ヶ月でiOSアプリをリリースした話 - GameWith Developer Blog
- Google オフィスで開催されたKubernetes/GKE セミナーに参加してきました! - GameWith Developer Blog
- GameWithアプリ と GameWithアプリチームについてのご紹介 - GameWith Developer Blog
CircleCI のレクイエム
さて、いよいよ 2018 年 8 月 31 日を迎えましたが、本日は何の日か覚えてますでしょうか?
はい、そうです。CircleCI 1.0 の終了日です。
こちらのサイトでは、CircleCI 1.0 のビルド終了までの時刻がカウントダウンされています...。
ちなみに GameWith には約 50 個のプライベートリポジトリが存在するのですが、プロジェクトで利用している CircleCI は先週ですべて CircleCI 2.0 への移行が完了しました。
もちろん全プロジェクトで CircleCI を使っているわけではないので、移行作業自体はそこまで大変ではありませんでした。
移行完了までの時系列
ここでは、最も主となるプロジェクトにおいての移行話を書きたいと思います。
2017 年 10 月頃
そもそもビルドに 3 分半〜 4 分ほどかかっていて、高速化したいねという話は以前から上がっていました。
また、CircleCI 2.0 にいずれ移行しなければならないのであれば、ギリギリになって対応するのではなく多少余裕を持って移行しておきたいという気持ちもあり、このタイミングで移行を行うことにしました。
以前、CircleCI 2.0 の resource_class で CPU と RAM のリソースを変更してみるという記事を公開しましたが、ちょうどその頃に得た知見でした。
Ansible + Packer で AMI を構築しているのですが、本来であれば Docker image はその資源を再利用して作成するのが良かったはずです。
ですが、このタイミングでは私自身が Docker を触るのがほぼ初めてだったこともあり、経験を積むという事も兼ねて Dockerfile からイメージを作成してみることにしました。
その際の Dockerfile がこちらです。 基本的に、依存関係はすべてひとつのコンテナに載せてしまう方法をとっています。 主に MySQL や memcached のコンテナを別で立てた場合の unit test 実行時のレイテンシを懸念しました。
また、弊社サービスでは一部 zstd を利用して圧縮を行っている箇所があることから、zstd のインストールも行っています。
(当時は NodeJS のバージョンが古かったのですが、現在は 8.x 系へバージョンが上がっています)
FROM ubuntu:14.04 RUN apt-get -y update && \ apt-get install -y --no-install-recommends \ apt-file \ software-properties-common RUN apt-file update && \ LC_ALL=C.UTF-8 add-apt-repository -y ppa:ondrej/php RUN echo 'mysql-server-5.6 mysql-server/root_password password' | debconf-set-selections && \ echo 'mysql-server-5.6 mysql-server/root_password_again password' | debconf-set-selections RUN apt-get -y update && \ apt-get install -y --no-install-recommends \ build-essential \ php5.6 \ php5.6-mcrypt \ php5.6-mbstring \ php5.6-curl \ php5.6-cli \ php5.6-mysql \ php5.6-memcached \ php5.6-redis \ php5.6-xml \ php5.6-dev \ memcached \ redis-server \ mysql-server-5.6 \ git \ unzip \ nodejs \ npm \ && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* RUN npm cache clean && \ npm install n -g && \ n 4.3.0 RUN apt-get purge -y nodejs npm RUN git clone -b 0.4.5 --recursive --depth=1 https://github.com/kjdev/php-ext-zstd.git && \ cd php-ext-zstd && phpize && ./configure && \ make && make test NO_INTERACTION=1 && \ make install && \ echo "extension=zstd.so" > /etc/php/5.6/apache2/conf.d/25-zstd.ini && \ echo "extension=zstd.so" > /etc/php/5.6/cli/conf.d/25-zstd.ini && \ cd - && rm -rf php-ext-zstd RUN curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer COPY my.cnf /etc/my.cnf
[mysqld] pid-file = /var/run/mysqld/mysqld.pid socket = /var/run/mysqld/mysqld.sock datadir = /var/lib/mysql log-error = /var/log/mysql/error.log bind-address = 127.0.0.1 innodb_flush_log_at_trx_commit = 2 sync_binlog = 0 innodb_use_native_aio = 0 character-set-client-handshake = FALSE character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci [client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4
そして、こちらが .circleci/config.yml
です。
この時点では、Workflows を利用した並列実行は主に下記 2 つの理由から行いませんでした。
- Workflow 化した場合、Slack への通知がそれぞれのジョブ実行が別々で通知され、ビルド全体が pass したのか fail したのかが若干分かりづらかった
- 2 並列実行で 2 連続 commit & push が行われた場合に、3 コンテナが利用できる課金プランだと、結果的には後から実行されたビルドの 1 job は待ち状態になってしまうため、スループット的にはあまり旨味がない
defaults: &defaults working_directory: ~/gamewith docker: - image: 305431254543.dkr.ecr.us-west-1.amazonaws.com/gamewith/ci:php5.6 aws_auth: aws_access_key_id: XXXXX # ecr-docker-puller aws_secret_access_key: $ECR_AWS_SECRET_ACCESS_KEY checkout: &checkout run: name: Checkout repository command: | echo "machine github.com login $GITHUB_TOKEN" > ~/.netrc repository_https_url=$(echo $CIRCLE_REPOSITORY_URL | sed -e "s|git@github.com:|https://github.com/|") git clone --depth=1 --branch $CIRCLE_BRANCH --single-branch $repository_https_url . git reset --hard $CIRCLE_SHA1 || (echo "The branch was updated after the build was executed. Please rebuild." 1>&2; false) version: 2 jobs: build: <<: *defaults steps: - <<: *checkout - run: name: Start services command: /bin/bash .circleci/shell/start_services.sh - run: name: Ping to services command: | mysqladmin ping nc -v -w 1 localhost -z 11211 redis-cli ping - restore_cache: keys: - v1-composer-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "composer.lock" }} - v1-composer-dependency-{{ .Environment.COMMON_CACHE_KEY }}- - run: composer install --prefer-dist - save_cache: key: v1-composer-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "composer.lock" }} paths: - fuel/vendor - restore_cache: keys: - v1-npm-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "package.json" }} - v1-npm-dependency-{{ .Environment.COMMON_CACHE_KEY }}- - run: npm install - save_cache: key: v1-npm-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "package.json" }} paths: - node_modules - run: name: php oil refine db:setup command: FUEL_ENV=test php oil refine db:setup - run: name: php oil test --group=App command: FUEL_ENV=test php oil test --group=App - run: name: ./node_modules/.bin/jshint js/ command: ./node_modules/.bin/jshint js/ when: always # 現状、php oil test は意図せず fail することもあるため、常に実行するように - run: name: ./tools/test/asset.sh command: ./tools/test/asset.sh when: always
また、以下が .circleci/shell/start_services.sh
でして、MySQL などの service が running になるまで施行するというスクリプトになっています。
#!/bin/bash -eo pipefail MAX_WAIT_TIME=4 # MySQL WAIT_TIME_MYSQL=0 until [ $WAIT_TIME_MYSQL -eq $MAX_WAIT_TIME ]; do /etc/init.d/mysql start if mysqladmin ping >/dev/null; then echo "MySQL is running!" break fi echo "MySQL not running, trying to kick start it" sleep $WAIT_TIME_MYSQL let WAIT_TIME_MYSQL=WAIT_TIME_MYSQL+1 done # start memcached WAIT_TIME_MEMD=0 until [ $WAIT_TIME_MEMD -eq $MAX_WAIT_TIME ]; do /etc/init.d/memcached start if nc -v -w 1 localhost -z 11211 >/dev/null; then echo "memcached is running!" break fi echo "memcached not running, trying to kick start it" sleep $WAIT_TIME_MEMD let WAIT_TIME_MEMD=WAIT_TIME_MEMD+1 done # start redis-server WAIT_TIME_REDIS=0 until [ $WAIT_TIME_REDIS -eq $MAX_WAIT_TIME ]; do /etc/init.d/redis-server start if redis-cli ping >/dev/null; then echo "Redis is running!" break fi echo "Redis not running, trying to kick start it" sleep $WAIT_TIME_REDIS let WAIT_TIME_REDIS=WAIT_TIME_REDIS+1 done if [ $WAIT_TIME_MYSQL -eq $MAX_WAIT_TIME ] || [ $WAIT_TIME_MEMD -eq $MAX_WAIT_TIME ] || [ $WAIT_TIME_REDIS -eq $MAX_WAIT_TIME ]; then echo "Maximum wait time has been exceeded" exit 1 fi
これで移行は完了です。約 3 分でビルドが完了するようになりました。
2018 年 3 月頃
その後しばらくして、CircleCI の課金プランを変更したこともあり、そろそろ並列化してもよいのではという機運がやってきました。
その際の .circleci/config.yml
がこちらです。
defaults: &defaults working_directory: ~/gamewith docker: - image: 305431254543.dkr.ecr.us-west-1.amazonaws.com/gamewith/ci:0.0.1 aws_auth: aws_access_key_id: XXXXX # ecr-docker-puller aws_secret_access_key: $ECR_AWS_SECRET_ACCESS_KEY checkout: &checkout run: name: Checkout repository command: | echo "machine github.com login $GITHUB_TOKEN" > ~/.netrc repository_https_url=$(echo $CIRCLE_REPOSITORY_URL | sed -e "s|git@github.com:|https://github.com/|") git clone --depth=1 --branch $CIRCLE_BRANCH --single-branch $repository_https_url . git reset --hard $CIRCLE_SHA1 || (echo "The branch was updated after the build was executed. Please rebuild." 1>&2; false) version: 2 jobs: build_php: <<: *defaults steps: - <<: *checkout - run: name: Start services command: /bin/bash .circleci/shell/start_services.sh - run: name: Ping to services command: | mysqladmin ping nc -v -w 1 localhost -z 11211 redis-cli ping - restore_cache: keys: - v1-composer-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "composer.lock" }} - v1-composer-dependency-{{ .Environment.COMMON_CACHE_KEY }}- - run: ./composer.phar install --prefer-dist - save_cache: key: v1-composer-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "composer.lock" }} paths: - fuel/vendor - run: name: php oil refine db:setup command: FUEL_ENV=test php oil refine db:setup - run: name: php oil test --group=App command: FUEL_ENV=test php oil test --group=App - run: name: php oil test --group=NativeApp command: FUEL_ENV=test php oil test --group=NativeApp build_js: <<: *defaults steps: - <<: *checkout - restore_cache: keys: - v1-npm-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "package.json" }} - v1-npm-dependency-{{ .Environment.COMMON_CACHE_KEY }}- - run: npm install - save_cache: key: v1-npm-dependency-{{ .Environment.COMMON_CACHE_KEY }}-{{ checksum "package.json" }} paths: - node_modules - run: name: ./node_modules/.bin/jshint js/ command: ./node_modules/.bin/jshint js/ - run: name: ./tools/test/asset.sh command: ./tools/test/asset.sh workflows: version: 2 build_php_and_js: jobs: - build_php - build_js
これによって、2 分〜 2 分半ほどでビルドが完了するようになりました。
2018 年 6 月頃
さて、次は脱 CI 専用 Dockerfile です。
既存の資源を活かし、Ansible で Docker image を作成するように変更しました。 ビルド時間の削減には効きませんでしたが、本番環境と同じ足回りでビルドを実行するというあるべき姿になりました。
また副次的な効果として開発環境の Docker 化の一歩目が始まりました。
苦労・工夫した点
- DockerHub を利用せず、下記理由から Amazon ECS を利用します
- 弊社では基本的に AWS を利用しています
- DockerHub の複数人利用には organization を利用するのがベストですが、 private repo を作成するには課金が必要 (この理由が一番大きい)
- pull するアカウントとして共通アカウントのようなものを用意しないといけない
- ECS のリージョンは、
ap-northeast-1
でなく、us-west-1
を利用しています- CircleCI 自体の実行環境が展開されているのがおそらく米国なので、
ap-northeast-1
だと docker pull の速度が遅い - 有意な回数の検証は出来てないのでもしかしたら偶然かもしれません
- CircleCI 自体の実行環境が展開されているのがおそらく米国なので、
- リポジトリのチェックアウトは、shallow clone で行っています。CircleCI が提供する checkout ステップは純粋に git clone & fetch -> キャッシュ化しているだけなので、リポジトリが巨大だと非常に遅いです
- クラウドワークスさんのやり方を真似させて頂きました
- 速度改善のため、ssh でなく、https で clone してます
- 環境変数
COMMON_CACHE_KEY
はこれまたクラウドワークスさんのやり方を真似たものです- キャッシュに万が一不整合が起きた時に、こちらの環境変数を動的に変更することで、全ブランチのキャッシュリフレッシュを可能にします
- my.cnf をカスタマイズしないと php oil test が 1.0 の頃より 2 倍弱遅くなってしまいました
- https://discuss.circleci.com/t/tests-run-slower-on-2-0/12438/5 を参考に変更してみました
- (こちらは Ansible で Docker image を作成するようになって設定が不要になりました)
- うまくビルドが通らないときなどは config.yml を修正 -> commit -> push を繰り返してしまいがちですが、思い切って最初から CircleCI の job に ssh してコマンドの実行手順を固めましょう
- 何が原因でうまくパスしないのか、原因の切り分けをしていくのが結果的には早いです
- CircleCI の環境依存なのか?
- 実行に必要なミドルウェアがインストールされていない?
- たとえば database への接続をするための config 自体が誤っているのか?
- 何が原因でうまくパスしないのか、原因の切り分けをしていくのが結果的には早いです
移行のちょっとしたヒント
まだ 2.0 への移行が済んでいないプロジェクトのオーナーの方々へ。
公式ドキュメントがとても充実しているので、まずはそこをあたるのが良いでしょう。 各言語ごとに Getting Started が用意されていたりするので、そこを足がかりに少しずつやってみると良いでしょう。
なお、job に ssh する方法はこちらです。
2.0 が出始めた当初、日本語のドキュメントや知見の公開は主にクラウドワークスさんや SmartHR さん、Tokyo Otaku Mode さんの技術ブログで行われていました。 しかし、今では日本語での移行体験談や tips もずいぶん多く公開されています。
壁にぶち当たったら、その都度愚直にググっていけば参考文献は見つかるはずです。
エピローグ
CircleCI は 2.0 の public beta の公開から約1年半という期間を経て、本日をもって 1.0 の終了となります。 運営側は、かなり前から事前告知を行い、我々開発者に対して寄り添ってきてくれました。
基本的には 2.0 への移行はメリットしかないと思うので、どこか早いタイミングでリソースを捻り出して、恩恵を受けられると良いのではないかと思っています。
今回紹介した 2.0 移行への軌跡は GameWith の主となるプロジェクトについてのお話でした。 GameWith のネイティブアプリや、サブプロジェクト、新規事業でも CircleCI を使い倒しているのですが、そちらの話は今回は割愛させていただきました。
GameWith では業務効率化のために CI/CD 周りにも力を割いています。
お決まり文句で恐縮ですが、興味のある方は以下からでも良いですし、私に直接 DM でもよいのでご連絡いただけると嬉しいです。