くりにっき

フルスタックキュアエンジニアです

Slackからのリクエストを処理するwebhookを1回だけ実行したい

最近そういうことを社内で何回か書いたのでメモ

前提

Slackには特定のイベントが発生した時に任意のwebhookにリクエストを飛ばすことができます。

f:id:sue445:20191017143638p:plain

具体的には下記のような使い方ができます

sue445.hatenablog.com

しかし普通にやると1回しかemojiを登録してないのにSlackに何回も投稿されて困ることがありました

なぜ重複投稿されるか?

公式ドキュメントを見つけきれてないので推測ですが、そういう仕様な気がしてます。

(Herokuだとdyno起動に時間がかかる時に重複リクエストになりやすい傾向があったけど、GASやLambdaでも同じことになる)

重複実行対策

Heroku + sinatra + puma構成の場合下記を行いました

1. 実行内容を一定時間キャッシュする

前述のemoy_webhookだとSlackの投稿内容を5分間redisにキャッシュしてたけど、Slackからのリクエストに含まれているevent_idを使った方がよかったかもしれないです

参考:

teppay.hatenablog.com

2. 排他ロックをする

redis-objectsにredisで排他ロックする機能があるのでそれを使ってました。(ruby以外にも同じようなライブラリがあるかは不明)

https://github.com/nateware/redis-objects#locks

3. サーバ側での並列処理を捨てる

1と2だけでは不完全で、完全同時にリクエストが飛んでくるとどうしようもなかったので多重実行されてしまいました。。。

そのためHerokuのdynoを1にしつつ、pumaのスレッド数を1にしてサーバ側で絶対に並列でリクエストを処理できない状態にしたら多重実行問題は解決しました。(普通のウェブアプリだと完全にアンチパターンなんだけどwebhookが叩かれる頻度は少ないので許容できる範囲内)

【CVE 2019-16892】rubyzip v1.3.0にアプデする時は絶対validate_entry_sizesを設定してください

tl;dr;

タイトルが全てです

前置き

GitHubのSecurity Advisoryで下記のようなPRがきていると思います

f:id:sue445:20191001205324p:plain

このPRをマージすればリポジトリのアラートも消えて問題なさそうに見えるのですが、実際にはgemのアップデートだけでは不十分です

CVE 2019-16892について

zipのヘッダを改ざんして展開時にクソデカメモリを喰わせる、いわゆるzip bombです(雑説明)

なんでgemのupdateだけでは不十分なのか?

zip bomb対策には Zip.validate_entry_sizes = true が必要なのですが、 v1.3.0の時点ではデフォルト値が false になっていて有効になっていないためです

詳しくはこのPRを見てください

github.com

ではどうすればいいか?

明示的に Zip.validate_entry_sizes = true してください。

Railsであれば config/initializers/CVE-2019-16892-rubyzip-patch.rb のようなファイルを置いておけばいいでしょう

v2.0.0ではデフォルトでONになっていてこのパッチは不要になるので、v2.0.0で使おうとしたらエラーになるようにしています。(モンキーパッチの賞味期限)

FAQ

Q. だったら最初から2.0.0にアプデすればいいのでは?

依存gemのdependency~> 1.3 と書かれていて2系に素直に上げられないケースも多々あるので、rubyzipを直接使ってない限りいきなり2.0.0に上げるのは難しいのではと思います。

circleci-bundle-update-prをGitHub Actionsで動かせるようにした

投げたPRはこちら

github.com

動機

とある事情でCI用途のパーソナルアクセストークンを撲滅したかった件の一環です。

解説

GitHub Actionsはジョブ実行時に自動でアクセストークンをセットしてくれて便利なんですが、API実行の許可は付与されていても git push に対する許可が与えられていませんでした。

具体的には下記がGitHub Actionsだとエラーになります。

https://github.com/masutaka/circleci-bundle-update-pr/blob/v1.16.1/lib/circleci/bundle/update/pr.rb#L104-L111

そこでシステムの gitgit commitgit push してる部分をAPIで置き換えた感じです。

circleci-bundle-update-prをGitHub Actionsで動かす方法

https://github.com/masutaka/circleci-bundle-update-pr#run-on-github-actions にも書いたようにGitHub Actionsの変数をCircleCIの変数でexportしなおしてあげるだけでGitHub Actionsで動くようになります。

実際にGitHub Actionsで動かした図がこちら

f:id:sue445:20190928003300p:plain

補足

執筆時点では

steps:
  - name: Set up Ruby
    uses: actions/setup-ruby@v1
    with:
      ruby-version: v2.6.4

のように書いてもRuby 2.6.4をインストールできないため *1Gemfileruby "2.6.4" を書いているとかでどうしてもRuby 2.6.4を使いたい場合には

container:
  image: ruby:2.6.4

のようにDockerイメージを使うのがいいと思います。

circleci-bundle-update-prをGitHub Actionsで動かすメリット

  • パーソナルアクセストークンの発行が不要になる
  • CircleCIがorg単位での並列数に対してGitHub Actionsがリポジトリ単位での並列数なので、複数リポジトリで同時にジョブが実行されても詰まらない

circleci-bundle-update-prをGitHub Actionsで動かすデメリット

  • GitHub Actionsにはキャッシュがないため、毎回フルでbundle installが実行されて遅い
    • schedulerで定期的に動かすジョブなので少々遅いのは個人的には許容できる
    • Betaなのでキャッシュがないと割り切っています(楽観)

余談1

gitlabci-bundle-update-mr で同じ方針でやったので余裕かと思ってたら、GitLabだとAPI 1回で済んだことがGitHubだとAPI 7回もかかって意外に手ごわかった、、、

余談2

dependabotでええやんっていうのもごもっともなんですが、dependabotだと1 bundle update 1 PRになっている関係でPRが大量にきた時にCIが詰まるという問題があるので個人的には circleci-bundle-update-pr くらいの粒度が好きです。

あと、circleci-bundle-update-prだと下記のようにすることで週替りでbundle update当番を割り当てることができるんですが、dependabotだと同じことができない(assigneesが固定値しか渡せない)ためcircleci-bundle-update-prを使い続けてるというのもあります。

circleci-bundle-update-pr --assignees $(ruby -rtime -e 'a=%w[foo bar baz]; print a[Time.now.to_date.cweek% a.size]') 

【CVE-2019-10429】 Jenkins GitLab Logo Plugin v1.0.4をリリースした

作ったきり特にメンテしていなかった Jenkins GitLab Logo Plugin ですが、Jenkinsチームから脆弱性の報告を受けたので修正しました。

jenkins.io

https://jenkins.io/security/advisory/2019-09-25/#SECURITY-1575 にも書いてますが、GitLabのprivate tokenを暗号化せずにそのまま保存していたので暗号化して保存するようにしています

バージョンアップ時の注意点

Global configをsaveするだけで暗号化されて保存されます

日本語の関連記事

blog.trendmicro.co.jp

その他

CVEは採番されてないですが Jenkins ChatWork plugin も同様の問題を抱えていたためv1.0.9で修正しています

GitLab CIでTerraformを動かす

GitLab CIでTerraformをいくつか動かしてるのですがGitLab CI独自の機能をうまく使うといい感じにCIができるようになったのでメモ

前提

  • AWSの各アカウントをTerraformで管理している
  • アカウントごとにTerraformのリポジトリを作成
  • masterブランチ以外では terraform plan を実行し、masterブランチで terraform apply を実行
    • terraform plan があまり信用できないから別アカウントへの terraform apply のパターンもあるかもしれないですが、 terraform plan の方が基本形かなと思ってます

ベースになるジョブをincludeで別リポジトリから読み込むと便利

includeとはGitLab 11.4にcoreに入った機能で、外部のCI設定を .gitlab-ci.yml に取り込む機能です。

https://docs.gitlab.com/ee/ci/yaml/#include

他のCIサービスでいうとCircleCIのorbsが一番近いです。

include導入前

includeを使う前は各リポジトリに下記のような .gitlab-ci.yml が点在していました。

.terraform: &terraform
  image:
    name: hashicorp/terraform:0.12.5
    entrypoint: [""]

  before_script:
    - terraform init -input=false

  tags:
    - terraform

stages:
  - test
  - deploy

cache:
  key: "${CI_JOB_NAME}"
  untracked: true
  paths:
    - .terraform/

tflint:
  <<: *terraform

  stage: test

  variables:
    TFLINT_VERSION: "0.9.2"

  script:
    # tflintのDockerイメージを使うとterraform initできなくてmoduleが読めないためバイナリをDLする
    - cd /tmp
    - wget https://github.com/wata727/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip
    - unzip tflint_linux_amd64.zip

    - ./tflint

  tags:
    - docker

terraform-plan:
  <<: *terraform

  stage: test

  script:
    - terraform plan -input=false

  except:
    - master

terraform-apply:
  <<: *terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  only:
    - master

terraform-apply-manual:
  <<: *terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  except:
    - master

  when: manual

2箇所くらいならまだ許せたのですが4つ超えたあたりで厳しい感じになりました

include導入後

先日会社のGitLabをバージョンアップしたので includeを使ってリファクタリングしました。

まずは下記のようにテンプレートファイルのみを用意したリポジトリを用意

variables:
  TERRAFORM_VERSION: ""
  TFLINT_VERSION:    ""

.terraform:
  image:
    name: hashicorp/terraform:${TERRAFORM_VERSION}
    entrypoint: [""]

  before_script:
    - terraform init -input=false

  tags:
    - terraform

stages:
  - test
  - deploy

cache:
  key: "${CI_JOB_NAME}"
  untracked: true
  paths:
    - .terraform/

tflint:
  extends: .terraform

  stage: test

  script:
    # tflintのDockerイメージを使うとterraform initできなくてmoduleが読めないためバイナリをDLする
    - cd /tmp
    - wget https://github.com/wata727/tflint/releases/download/v${TFLINT_VERSION}/tflint_linux_amd64.zip
    - unzip tflint_linux_amd64.zip

    - ./tflint

  except:
    - master

  tags:
    - docker

terraform-plan:
  extends: .terraform

  stage: test

  script:
    - terraform plan -input=false

  except:
    - master

terraform-apply:
  extends: .terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  only:
    - master

terraform-apply-manual:
  extends: .terraform

  stage: deploy

  script:
    - terraform apply -input=false -auto-approve

  except:
    - master

  when: manual

Terraformリポジトリでは下記のように別リポジトリのtemplateを呼び出すだけで使えるようになります

include:
  - project: "group/gitlabci-terraform-template"
    file:    "/template.yml"

variables:
  TERRAFORM_VERSION: "0.12.5"
  TFLINT_VERSION:    "0.9.2"

メリット

  • CIの設定を一箇所に集約したことにより、利用側ではTerraformとtflintのバージョンだけ渡すだけで使えるようになる

上記templateでやってること

masterブランチ以外では terraform plantflint を自動実行しつつ、必要なら terraform apply を手動実行

f:id:sue445:20190920235810p:plain

masterブランチでは terraform apply を自動実行

f:id:sue445:20190920235819p:plain

Terraform実行専用のRunnerを作る

前述の設定で tagsterraform を書いてたやつです。

このTerraform専用のRunnerですが、下記のようにlimitを1にしたdocker executorを作っています。

[[runners]]
  name = "terraform"
  executor = "docker"
  limit = 1

https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-runners-section

limit = 1 にする理由

AWSでTerraformを使う場合にはbackendをs3にしつつ、DynamoDBで排他ロックをかけるのがデファクトになってます。

www.terraform.io

しかしこれだと同時に2つのブランチをpushした場合に後から実行された方が排他ロックエラーになってジョブが失敗します。

Terraformを触る人数が多いとこの手のエラーはしょっちゅう起こるし、1人しか触ってない場合でもトピックブランチをpush後にミスに気づいてforce pushしたような場合にもエラーになります。

それを防ぐためにRunnerを limit = 1 にする(つまり複数のジョブの同時実行をさせない)ことでジョブが確実に1つずつ実行されるようになり、さっきのような排他ロックエラーを回避できます。(planの時だけロックかけないってのも頑張ればできそうだけど怖いのでやったことはない)

ちなみにジョブの並列実行数を意図的に制限するというのは僕が知る限りGitLab CIでしか実現できない機能です。(少なくともTravis CI, CircleCI, Wercker, GitHub Actionsには存在しないです)

このTerraform専用Runnerはここ1年くらいずっと使っていますが、CircleCIでTerraformを動かしてた時によく起こってた排他ロックエラーが一切発生しなくなったのでおすすめです。

CIマニアから見たGitHub Actions(Beta)の使い所

1ヶ月くらい使って勘所が見えてきたのでメモ

メリット

1リポジトリ辺り20並列までジョブを並列実行できる

これに尽きる。

CircleCIにしろTravis CIにしろorganization(user) *1単位で並列数が縛られているため、例えば同じuserの他のリポジトリでジョブが詰まっていると別リポジトリではqueueが詰まってジョブが実行されません。

しかしGitHub Actionsの場合リポジトリ単位で並列数が縛られているので、あるリポジトリで20並列ジョブ実行中だからといって他のリポジトリでジョブが詰まることがありません。

後の方でデメリットについて色々書いてるけど、このメリット1つだけで他のデメリットを帳消しにしてお釣りがくるくらいの破壊力はあります。(偏見)

ジョブ実行時はアクセストークンが勝手に設定されている

CIのジョブの中でGitHubAPIを使いたい場合、パーソナルアクセストークンを発行してsecretsとかにセットすると思うのですが、GitHub Actionsだと secrets.GITHUB_TOKEN に勝手にセットされているので発行する必要がないです。

特に会社系のorganizationの場合、誰かがジョブ実行用のパーソナルアクセストークンを発行してCIを設定してその後その人が退職してorganizationから外された後にCIが動かなくなるってことが割とよくあると思うのですが(僕は過去2~3回見てきました)、その心配がなくなるのは地味に嬉しいところ。

マトリクステストがやりやすい

Travis CIライクな記法が使えるのでマトリクスの軸が増えてもyamlが長大になりづらいというメリットがあります。

jobs:
  build:
    strategy:
      matrix:
        ruby:
          - v2.3.x
          - v2.4.x
          - v2.5.x
          - v2.6.x
        gemfile:
          - gemfiles/rails_4_0.gemfile
          - gemfiles/rails_4_1.gemfile
          - gemfiles/rails_4_2.gemfile
          - gemfiles/rails_5_0.gemfile
          - gemfiles/rails_5_1.gemfile
          - gemfiles/rails_5_2.gemfile
          - gemfiles/rails_6_0.gemfile

最初に書いた1リポジトリ辺り20並列の仕様もあるのでライブラリ作者的に嬉しい。

実際にGitHub Actionsに移行したプロダクト

時系列順に紹介

zatsu_monitor

github.com

元々はTravis CIを使ってたんですが、僕の他リポジトリで採用してる「gitのtagをpushしたらCI上でバイナリをビルドしてReleasesに自動アップロードするお手軽リリース」がなくて割と面倒でした。

そのうちTravis CIで実装しようかと思ってた矢先にGitHub Actionsが使えるようになったので移行してお手軽リリースを実装しました

activerecord-compatible_legacy_migration

github.com

僕がメンテしてるgemの中で2番目にマトリクス数がヤバかったやつ。(Travis CIで40個)

並列数は多いものの後述のindex_shotgunと違ってMySQLPostgreSQLのようなDBを使ってないのでGitHub Actionsで初マトリクステストやるには丁度よさそうな規模感と難易度と踏んで移行

index_shotgun

github.com

僕がメンテしてるgemの中で1番マトリクス数がヤバいやつ。

マトリクス数はTravis CIだけで50個くらいあって、歴史的経緯でCircleCIも併用してるのでそれも合わせると55個くらいあります。

なんでマトリクス数がこんなにヤバいのかというと「各Rubyのバージョン x 各activerecordのバージョン x 各DB(sqlite3, MySQL, PostgreSQL, Oracle)」でテストしてるため。

複数のCIを併用してる弊害として .circleci/config.yml だけ修正したのにTravis CI側でもジョブが実行されて30分くらい待たされるという事象が度々あったので、CIを1つにまとめたいとは思ってました。

実際にGitHub Actions対応を行ったのが下記PRですが、Railsで使えるdatabase.ymlをテストコードで使っているので普通のRailsアプリをGitHub ActionsでCIする時の設定は参考になると思います。

github.com

ちなみにGitHub Actions移行後はマトリクス数60個くらいですが、全部実行し終わるための所要時間は18分前後です。

デメリット

Betaクオリティなので大目に見てますが、大目に見た上でもここはつらいというところだけ書いてます

yamlのanchorが使えない

Railsのdatabase.ymlでよく見る

default: &default

development:
  <<: *default

的なやつ。

yamlファイルが一定規模以上になるとanchorとmergeを使って上記のようなリファクタリングをしてDRYにしたくなるのですが、GitHub Actionsだと「Anchors are not currently supported」というエラーになって使えなくてつらい。

f:id:sue445:20190909225037p:plain

anchorとmergeでリファクタリングできるのがYAMLの特徴だと思うんだけど、それが(まだ?)使えないのが残念。

マトリクステストだとSlack通知がつらい

GitHub ActionsにSlack通知機能が搭載されていなくてstepの中で自分でSlack APIを叩くかmarketplaceにあるSlack通知用のCheckを使うことになります。

しかしマトリクステストのstepsの中で直接Slack通知を行うと通知が大量にきてノイズになるので、全てのマトリクステストが終わった後に1回だけ成功か失敗かの通知を投げてほしいと思うのが人情ですが、それを実装するのが今のGitHub Actionsの仕様だと非常に面倒です。

上記要件を真面目に満たそうとするとこんな感じになります。

jobs:
  build:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        # 略

    steps:
      # 略

      - name: Slack Notification (not success)
        uses: homoluctus/slatify@v1.5
        if: "! success()"
        with:
          job_name: '*build*'
          type: ${{ job.status }}
          icon_emoji: ":octocat:"
          url: ${{ secrets.SLACK_WEBHOOK }}

  notify:
    needs: build
    runs-on: ubuntu-latest

    steps:
      - name: Slack Notification (success)
        uses: homoluctus/slatify@v1.5
        if: always()
        with:
          job_name: '*build*'
          type: ${{ job.status }}
          icon_emoji: ":octocat:"
          url: ${{ secrets.SLACK_WEBHOOK }}

needs を使えば前のジョブが全部終わった時に次のジョブを実行するというworkflowが組めるのでこれを使えばマトリクステストが全て終わった時にnotify ジョブで1回だけ通知を飛ばすということが簡単にできそうに見えます。

しかしneedsに書いたジョブが全て成功した時にしか実行されないので notify ジョブだけだとジョブ成功時の通知しか飛ばせません。

そのためマトリクステストの中で失敗時の通知を飛ばすような実装が必要になります。

GitHub ActionsのマトリクステストはTravis CIと違ってfail-fast(マトリクステストがどれか1つでも失敗したら他はスキップする)という仕様なのでジョブ失敗時の通知は1回しかこないように見えますが、同時に20並列もテストを実行してるとだいたい同時にテストが失敗するため失敗時の通知は複数飛んできます。

ジョブ失敗時はだいたい10個くらい同時に通知が飛るのですが、他にやりようがないので今は我慢してます、、、

f:id:sue445:20190909231009p:plain

合わせて読みたい

sue445.hatenablog.com

*1:ここで言ってるuserというのはジョブの実行者ではなくリポジトリの所有者の方です

zatsu_monitor v1.0.0をリリースした

リリースノート

https://github.com/sue445/zatsu_monitor/releases/tag/v1.0.0

Breaking changes

zatsu_monitorは今まで 0.3.3 のようなtagをつけていたのですが、Go modulesがprefix付きのtagを推奨していたので *1 、それに準拠して v1.0.0 のようなtag名に変更しました

アプリ的な機能変更は特にない(強いて言うならGo 1.13へのアップデートとライブラリのアップデートくらい)のですが、プロビジョニングツールとかからReleasesのバイナリをDLしてるケースもあるので *2 念の為Breaking changes扱いにしています。

*1:https://github.com/golang/go/wiki/Modules#modules

*2:自分のItamaeレシピでやってる