くりにっき

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

GitHub Actionsで特定の条件の時だけenvironmentを設定したい

tl;dr;

environment三項演算子でいい感じにする

モチベーション

TerraformのワークフローをGitHub Actionsで動かしてるんだけど、terraform apply した時(具体的にはmainブランチか手動でのbuild実行時 *1 )のみ environment をセットしてDeployments *2に通知したかった。( terraform plan (Terraformのdry run)ではDeploymentsには通知したくない)

実装例

こんな感じ。

environment: ${{ ((github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch') && 'terraform-apply') || '' }}

github.com

分かりづらいんですが terraform apply 時のみDeploymentsに通知されています。

解説

environment に空文字を渡してもエラーにならず、非空文字が渡された時のみそれが environment としてDeploymentsに通知される」という仕様を利用しています。*3

実はGitHub Actionsに三項演算子は無いのですがそれっぽいものは実装することができます。

ググれば色々ありますが自分は下記を参考にしました。

qiita.com

*1:後者はトピックブランチでの手動applyを想定

*2:https://docs.github.com/ja/rest/deployments/deployments?apiVersion=2022-11-28

*3:将来的に挙動が変わる可能性はあります

rspec-parameterized v1.0.0をリリースした

リリースノート

https://github.com/tomykaira/rspec-parameterized/blob/master/CHANGELOG.md#v100-2022-12-31

主な変更点

rspec-parameterizedの実装を

の2つに分割しました。

普通にバージョンアップするだけで良ければ bundle update するだけでよくて、 RSpec::Parameterized::TableSyntax を利用していなければ Gemfile の記述を rspec-parameterized-core に差し替えることでnative extensionへの依存をなくすことができます。

詳しいことは https://github.com/tomykaira/rspec-parameterized/blob/master/UPGRADING.md を見てください

あとついでにRuby 2.6未満のサポートも完全に切りました。

bundle gem した時に spec.required_ruby_version = ">= 2.6.0" がついてきたのを消すかどうかちょっと迷ったんですが、メジャーバージョンアップする時くらいしか古いバージョンのサポートやめることができない *1 のでこの機会にRuby 2.6以上必須にしました。(Ruby 2.5のEOLは1年以上前なのでもうさすがにいいよね...)

コンテキスト

rspec-parameterizedのRuby 3.2対応を行った時に id:joker1007 から「TableSyntaxはオプショナルな機能なのでなるべく利用者にnative extensionをコンパイルさせたくない」というコメントがありました。*2

その後railsrspecみたいにgemを分割するのはどうか?という方針に関してPR上で合意を得られました。

Ruby 3.2 リリースパーティー presented by アンドパッド - connpass でjoker1007さんに会った時に直接話して、自分が作業することに関する合意を得られたのでgemを分割するようにしました。

頑張ったこと:可能な限り全てのコミットを移行した

素朴にやるならgemの中で使ってるclassやmoduleをそのままコピペするのが一番ラクなのですが、それだと新しいリポジトリで歴史も失われてしまいます。

そのためコミットもなるべく移行しました。

リポジトリからcherry-pickする方法は特に凝ったことはしておらず、 bundle gem で新しいgemのスケルトンを作った後に

git remote add v0-origin git@github.com:tomykaira/rspec-parameterized.git

で一時的にremoteを追加して必要なコミットを1つずつcherry-pickしています。

以前 id:koic がrubocop-railsやrubocop-performanceで似たようなことをやってた気がするのでその時の移行スクリプトやコマンドを見つけることができればよかったのですが、見つからなかったので必要なコミットを全て手でcherry-pickしてまわりました。

幸いなことにrspec-parameterizedは200コミットくらいしかなかったので気合でなんとかしました。(もう少しスマートなやり方があればよかったんですが別リポジトリにcherry-pickした時のコンフリクトの解消が結構多くて完全に自動化するのは難しそうだった...)

頑張ってcherry-pickしたおかげで新しく作ったリポジトリでもContributorsが引き継がれています。

それでは良いお年を!

*1:この辺はメンテナの好みによるんですが、僕は「古いバージョンのサポートをやめる = BREAKING CHANGE」と考えてる派なので古いバージョンのサポートをやめる時はだいたいメジャーバージョンを上げてます

*2:https://github.com/tomykaira/rspec-parameterized/pull/81#discussion_r1050584335

gem_rbs_collectionを使ってるgemをいい感じにCIする

前置き

gemを作る時にはだいたい最初に bundle gem コマンドでgemの雛形を作りますが、最近のbundlerだと sig/rbsファイルを格納するディレクトリ)とrbsファイルの雛形も作られるようになっています。

最近作った https://github.com/sue445/doorkeeper_jprbsをいい感じにCIするところまで持っていけたのでメモ。

やったこと

gem_rbs_collectionをsubmoduleとして追加する

https://github.com/ruby/gem_rbs_collection のREADMEに書かれているようにsubmoduleとして追加。

gemにsubmoduleを追加すると rake build した時に作られるgemファイルにも含まれないか心配だったのですが、調べたところsubmoduleの中身は含まれなかったのでgem_rbs_collectionを追加することで配布するgemファイルが肥大化することはなさそうです。

手元でrbs validateを叩けるようにする

Rakefile に下記のようなtaskを追加。 *1

desc "validate rbs"
task :rbs_validate do
  command = %w(
    rbs
    --repo vendor/rbs/gem_rbs_collection/gems/
    -r date
    -r forwardable
    -r uri
    -r faraday:2.5
    -r hashie:5.0
    -I sig/
    validate
    --silent
  ).join(" ")

  sh command
end

-r でrequireするものは作るgemによって変わりますが他の部分はだいたい使いまわしでよさそう。 -r を個別指定するのが面倒なんだけど今の所他にいい方法は見つからなかった...

CIでrbs validateを実行する

さっき追加した rake rbs_validateGitHub Actionsのworkflowで実行 *2

こんな感じ。

- run: bundle exec rake rbs_validate

submoduleとしてgem_rbs_collectionをdependabotで定期更新する

.github/dependabot.yml に下記のようなファイルを追加 *3 することでDependabotでsubmoduleも定期的に更新してくれるようになります。

version: 2
updates:
  - package-ecosystem: gitsubmodule
    directory: "/"
    schedule:
      interval: monthly

interval(daily or weekly or monthly)は適当なので適宜変えてください。

実際にDependanotでgem_rbs_collectionを更新した時のPRはこちら

github.com

DependabotがマージしたPRのジョブに対してsecretsを渡したい

前提

事象

Dependabotが作成したPRに対して @dependabot merge のようにコメントをつけるとPRのジョブが全て正常終了した時にDependabotが自動でマージしてくれるんですが、マージコミットに対するデプロイ用のジョブが実行される時にデプロイ時に必要なリポジトリのsecretsが取得できずにデプロイできなくて困りました。

GitHub Actionsの設定

      - name: Create Sentry release
        uses: getsentry/action-release@v1
        env:
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
          SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
          SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_FRONTEND }}
        with:
          environment: production

ジョブの実行結果

確かにsecretsが渡されていない模様。(なにか値が渡されていれば *** のようにマスクされた状態でログに出るはず)

原因

GitHubのRepository secrets(リポジトリ全体で使えるsecrets)やEnvironment secrets(productionやdevelopmentなどの環境毎に使えるsecrets)*1リポジトリに対してWrite権限を持ってるユーザが実行したジョブにしか渡されないのですが、 @dependabot merge でマージを行った場合はリポジトリにWrite権限を持っていないDependabotがジョブを実行したことになってるためsecretsが渡されていませんでした。

解決策

Dependabot secrets に設定すればDependabotが実行したジョブに対してsecretsが渡されるようになります。

ただ、このDependabot secretsは本来Dependabotがプライベートなパッケージマネージャにアクセスするために利用してるものだと思うので、今回のようなケースはGitHubがあまり想定してない解決方法のような気がしています。(なので今後GitHubの仕様変更で使えなくなる可能性も十分ありうる)

2022/10/5 19:50追記

別の場所で

このユースケースならAuto mergeが良いかなと思いましたが、botにマージさせたい理由が何かあった感じでしょうか?

って質問があったので追記。

メンテしてるリポジトリが70〜80個あるんですが*2、起きている時間帯にdepandabotのPRがくるとqueueが詰まって業務や趣味開発に支障が出るので、自分が寝ている時間帯になるべくPRをマージまでさせたいというのが一番の理由です。 (このエントリだと説明端折っていたけど実際には https://github.com/marketplace/actions/dependabot-auto-merge@dependabot merge のコメントをつけています)

*1:https://docs.github.com/ja/actions/security-guides/encrypted-secrets

*2: Depebdabotを入れてるリポジトリだけでも20個以上はあると思う

個人gemのドキュメントを全部GitHub Pagesに移行した

自分が作ってるgemでは https://github.com/lsegal/yard でドキュメントを書くことが多くて、そのドキュメントをホスティングする場所として https://rubydoc.info/ を使っていました。

しかし https://rubydoc.info/ がここ最近ずっと重くて表示するのに十数秒待たされてイラッとすることが多かったので、一念発起して全部GitHub Pagesに移行しました。

3日間で多分30〜40リポジトリくらい作業したと思います。

作業内容はこんな感じ。

github.com

設定ファイルはほぼ全部コピペでいいんだけど、リポジトリ毎にGitHub Pageを有効化したり手元でyardを実行してwarningが出てないかの確認をしてたらそこそこ時間かかりました。

最近だと静的ファイルをgh-pagesブランチとかにコミットにしなくてもGitHub Actionsから直接GitHub Pagesにデプロイできるようになって便利。

github.blog

小ネタですがgemspecに下記のように書いておくと https://rubygems.org/ からリンクが貼られるようになります。

spec.metadata["documentation_uri"] = "https://sue445.github.io/rubicure/"

doorkeeper_jp gemを作った

doorkeeper_jpとは

DoorkeeperAPIクライアントgemです。

github.com

自分のアプリで Doorkeeper API を使おうと思ったんですがAPIクライアントがなくて作りました。

基本的にはいつもの感じのAPIクライアントなんですが、gemでdoorkeeperといえば超有名な https://github.com/doorkeeper-gem/doorkeeper がある関係でgemの名前やネームスペースをかぶらないようにするのが大変でした。(名前重要)

Herokuにあった個人アプリを軒並み対応した

背景

Herokuの無料プラン終了のため10個以上あった個人アプリを1ヶ月くらいかけて色々対応してました

blog.heroku.com

対応したと言ってもHerokuから移行しないという判断を下した対応もあるのでその辺含めて書いていきます。

やったこと1. 廃止

これが一番簡単

それなりに利用されてそうだけど諸般の事情*1で移行しないと決めたアプリに関しては下記のように「NO LONGER MAINTAINED !」ってお知らせをだして、GitHubにもお知らせを書いてリポジトリアーカイブしました。

https://github.com/sue445/app-stat-api#no-longer-maintained

具体的には下記が該当

github.com

github.com

github.com

github.com

github.com

実は一番最後のやつに関しては途中までGoogle App Engine(以降、AppEngineと呼称)に移行してたんですが *2、移行作業中にこれを動かしてるTwitterボットのアカウントが凍結してることを知って一気にやる気が消えて移行断念しました...

ボットが凍結されていたらツイート投稿時にエラーになってるはずなんだけどRollbar入れてたのに検知できていなかったっぽくて本当に謎。

やったこと2. GCPに移行

完全に自分の慣れの問題だけど手持ちのHerokuアプリはほぼ全部GCPに移行しました。

僕自身はAWSGCPを両方とも業務で使い倒しているからAWSにも全然移行できたと思います。

この手のプラットフォーム選定は誰しも一家言持っているはずなのでみんなも自分が好きなやつに移行すればいいと思います。

GCPといっても色々ありますが、僕は下記のような選定基準で使い分けました。

ユースケース

URLベースで見たユースケース

  • URLベースだとこんな感じ
    • 今まで作ってきたツールが割とウェブアプリとかwebhookとかが多かったので自分の中ではURLを起点に考えるのが自然でした
  • SlackやGitHubに設定するwebhookであればランダムな文字列が入っていても問題ないと思うのでCloud FunctionsやCloud Runでよい
    • ちなみにGoogle App Engineだと一度起動したインスタンスはリクエストの処理時間が一瞬だったとしても最低15分間の費用が発生するため *3、webhook用途だとあまり向かないというのもあります
    • Cloud Functions *4 やCloud Run *5だといずれも100ミリ秒単位の課金
  • 普通にブラウザからアクセスさせたい場合はそういうランダムな文字列が入っていると見栄えがよろしくないのでGoogle App Engineを使うかカスタムドメインを使うしかなさそう
  • 安いTLDを使えばドメインはそんなに費用かからないしGCPのマネージドSSL証明書も無料なのでカスタムドメインでもええやん説はあるんですが、SSL証明書を紐づけるためのロードバランサーが個人アプリだと高くつくんですよね...

実行環境で見たユースケース

  • 図中の「使いたいDockerイメージがGCPにある」とは、DockerイメージがContainer Registry(gcr.io)かArtifact Registry(pkg.dev)にホスティングされていること
    • Cloud RunやGoogle App Engine Flexible EditionはこれらのDockerイメージしか対応していないので実は重要ポイント
    • Cloud Runで動かしたいためだけにいくつかの個人ツールはArtifact RegistryにDockerイメージをホスティングしました
  • DockerHubやGitHubのContainer Registry (ghcr.io)のイメージを使いたい場合はGKE一択
    • GCEインスタンスdocker-compose up もできなくはないけどマジでサーバの管理やりたくなさすぎる...(2回目)

実際にGCPに移行したアプリ達

Cloud Run

HerokuからCloud Runに移行したのは下記

github.com

github.com

github.com

採用理由としては

  • アプリケーションをDeploy to Herokuで配布してたので、脱Heroku後はDockeriseしてDockerイメージとして配布したかった
  • リリース用のバージョンtagとは別に、mainブランチの内容を常にデプロイして動作確認として使うための環境を考えたらCloud RunかAppEngine(Flexible Edition)が適切だった
  • emoy_webhookは会社アカウントのHerokuでも動かしているのでそっちのHeroku対応必要だったのだが、OSSとして配布してるDockerイメージを会社のGCPプロジェクトのCloud Runで動かすのが一番楽

って感じだったと思います。

個人アプリを配布したい場合はアプリケーションをDockeriseしてArtifact RegistryでDockerイメージを配布し、それをCloud Runとかで動かせるようにするのが便利ですね

ちなみにCloud Run移行後に Cloud Run Button の存在を思い出したんですが、DockeriseとしとけばCloud Run以外のDocker環境でも動かせるのでこれはこれで対応としてはありだったと思ってます。

Cloud Functions

HerokuからCloud Functionsに移行したのは下記

github.com

Cloud Run移行したアプリと違ってこれはDockerイメージとして配布する必要がなかったのでCloud Functiinsに移行しました。

ちなみにこのアプリは素朴なSinatraアプリなのですが、https://github.com/GoogleCloudPlatform/functions-framework-ruby を使うと下記のような感じでSinatraアプリを手軽にCloud Functions対応できてむっちゃ便利でした。

FunctionsFramework.http("regional-rb-calendar") do |request|
  App.call(request.env)
end

アプリケーションコードだとfunctions-framewor依存はこの3行だけなので、 Sinatra::Base を継承した App に関してはいつも通りにSinatraアプリとしてテストを書けるのが嬉しい。

AppEngine

OSSじゃなくて恐縮ですがサザエ実況用の個人アプリをHerokuからAppEngineに移行してました。

デプロイ時にdb:migrateするために https://github.com/GoogleCloudPlatform/appengine-ruby を使っているのですが、Ruby 3.0だと動かなかったのでパッチを投げています。もし困っている人がいたらこのパッチを使ってください。 *6

github.com

GCP移行した全てに共通してること

下記のような理由で1つのアプリにつきGCPプロジェクト(とそれに対応するTerraformリポジトリ)を1つずつ作ってます

  • 予算アラートを仕込む時はGCPプロジェクト単位でやるのが慣れている
  • 各アプリは完全に独立してるのでGCPプロジェクトも独立してるのが自然
  • AppEngineのアプリやFirestoreのスキーマみたいに1つのGCPプロジェクトで1つしか作れないものがたまにある
    • Firestoreに関してはkeyのprefixを工夫すれば複数アプリ共存させることもできるが、keyのパターンやcollection単位でIAMの権限を制御できないのでGCPプロジェクトを分けるのが確実

正直Terraformリポジトリを量産するのは大変のように思えるかもしれないですが、 https://github.com/sue445/terraform-gcp-template のおかげで1つ辺り10分前後で初期セットアップ(リポジトリ作ってから、git pushしたらGitHub Actionsでapplyされるところまで)できているのでそんなに大変ではなかったです。(自分のいつもやる構成をテンプレートリポジトリにしといて本当に良かった...)

terraform-gcp-templateに関しては下記を参照

sue445.hatenablog.com

やったこと3. CircleCIに移行

下記はCircleCIに移行しました

github.com

  • 雑なボットだとCloud Functionsでもいいんだけど、GCPプロジェクトの管理が手間なのも一理ある
  • ボット実行時の状態管理(通知したかどうかのフラグなど)が不要な場合はCIで動かすのが一番お手軽

というモチベーションでCircleCIに移行しました。

こいつは元々Herokuでボットを動かすついでにちょっとした画面も作っていたんですが、画面はなくてもいいのでこの機会に画面をなくしてCircleCIで動かせるようにしました。

ちなみにみんな大好きGitHub Actionsは https://docs.github.com/ja/site-policy/github-terms/github-terms-for-additional-products-and-features#actions

GitHub でホストされるランナーを使用している場合、GitHub Actions が使用されているリポジトリに関連付けられているソフトウェア プロジェクトの運用、テスト、デプロイ、または公開 とは無関係のその他のアクティビティ。

に該当しそうなので、ボット実行の環境として使うのは危ない予感がして避けました。

  • アプリの定期CIのためにスケジューラを使うのはいいけど、スケジューラ実行が主になるとダメだという認識
  • GitHub Actions が使用されているリポジトリに関連付けられているソフトウェア プロジェクト( = ボットスクリプト)の運用」で解釈すればセーフなんだが、サポートに問い合わせるとやぶ蛇になりそうなので聞きづらい...

CircleCIはこの手の記述はなかったので問題ないはず

付録A. 道のり

付録B. 調査メモ(移行時に参考にしたドキュメントやサービスなど)

無料プラットフォームがまとまってるドキュメント

移行作業中何回も読んだ。

blog.unasuke.com

ElephantSQL (PostgreSQL)

www.elephantsql.com

20MBまでなら無料

www.elephantsql.com

  • 前述のサザエ実況アプリではこれを採用しました
    • 毎週日曜のサザエ実況ツイート数をDBに記録してるんだけど、約10年分のデータで8.6MBしか使ってなかったのであと10年は無料プランで戦えるw
  • AWS, GCP, Azureが選択可能
  • 無料だと東京リージョンが使えるのはAWSだけですが、東京リージョンがあるだけでもかなり嬉しい

PlanetScale (MySQL)

planetscale.com

無料プランだと1 organization辺り1データベースが無料で使える

planetscale.com

  • 1データベースとはいえストレージ5GB使えるので小さいアプリなら1つのデータベースに複数相乗りは可能そう
  • Railsだとschema_migrationsが有るので複数相乗りは厳しい気がする

Redis Enterprise Cloud

redis.com

30MBまで無料

Redis Enterprise Cloud Pricing | Redis

  • クラウド(AWS, Azure, GCP)のリージョンが使える
  • 今回の移行よりも前に別アプリで使ってた

付録C. Redisを雑にFirestoreに置き換えたらクラウド破産しかけた

  • OSSにしてない個人アプリでHerokuとRedisを使っていて、GCP移行時にHerokuのRedisをFirestoreに差し替えた
  • RedisのノリでFirestoreでもgetしまくったらRead Opsがヤバいことになった
    • FirestoreはReadやWriteやDeleteの回数に対して課金が発生する
  • 一番多いときでRead Opsが1日1,600万回(1日9ドル)
  • 色々チューニングして今は1日辺りの無料枠に収まる範囲で運用できるようになっている
  • クラウド破産」は正直釣りタイトルだったと反省してる

2022/09/22 20:45ブコメレス

id:tengo1985

ドメインSSL証明書はcloudflare被せちゃダメなの?とは思った。アプリの用途や利用具合なんかはわかってないからなんとも言えないんだけど。

なるほどー!普段そんなに使ってないから最初から選択肢になかったです。(仕事ではたまに使うけど個人では契約していない)

*1:主に自分のモチベーションの問題

*2:余談ですが「以降」と「移行」をかけたダジャレではありません

*3:https://cloud.google.com/appengine/pricing?hl=ja

*4:https://cloud.google.com/functions/pricing

*5:https://cloud.google.com/run/pricing

*6:appengine:execの処理の実装はserverless-exec-rubyにあるのでこっちをなおす必要がある