2022年になった

2021年の振り返り

仕事

転職した。 バックエンドエンジニアという枠で採用されたものの1on1で「インフラも実は興味があって、チャンスがあればやらせて欲しい」という旨を上司に伝えたところいつの間にかSREになっていた。まだまだスキルは乏しいため、自らSREと名乗るのは若干抵抗がある。 リモートワーク中心のスタイルで新しい会社のカルチャーに慣れながら、 新しい職種としてキャッチアップしていった1年だった。 今年もチームの一員として頑張りたい。

生活

入籍した。引っ越した。車買った。 ほぼ入籍と同時期に同棲を開始したため、生活環境を整えたり、それに慣れていたりした1年だった。 新型コロナウイルスの影響でバカンス的な何かはほとんどできてないのが残念。 あとはお金の勉強の一環で積立NISAとふるさと納税を始めてみた。

2022年やりたい

2021年はプライベートが忙しくて、あんまりオープンな活動ができなかった。 2022年は一つぐらい何かアウトプットしたい。

nginxのシグナルについて

Controlling nginx

nginxのドキュメントを読んでいて気づいたのですが、 nginxのmaster processおよびworker processはTERMを受信するとgraceful shutdownしてくれないみたいです。 なんでnginxのDockerコンテナでエラーが起きないんだろうと思っていたところ、 Dockerイメージ側でgraceful shutdownの対応がされているようでした。

Nginx の Docker コンテナがデフォルトで graceful shutdown になってちょっと幸せ - Qiita

Use TERM as STOPSIGNAL. · nginxinc/docker-nginx@16ec71e · GitHub

2021/12/25:訂正 コミットのURLを間違えて掲載していました。 graceful shutdown対応のコミットは下記のURLになります。 @shogo82148さんに指摘をいただきました。ありがとうございます!

Use SIGQUIT instead of SIGTERM for graceful shutdown of nginx · nginxinc/docker-nginx@3fb70dd · GitHub

日頃あまり読まないドキュメントに目を通すと新たな発見があって、面白いですね。

actions-setup-tfcmtを作ってみた

suzuki-shunsuke/tfcmtをsetupするアクションを書いてみました。

github.com

blog.chaspy.me

tfnotify はメンテされていないので、tfcmt と比較する。tfcmt は Terraform での通知に特化しているので多くの優位点がある。その内容は tfcmt の README にある通りだが、 * 結果がより見やすい * 削除時に警告される * PR にラベルが付与される * terraform 以外での変更が行われた部分をわけて表示する * github-comment と連携し、過去の comment を hide する

tfcmtは上記のようにPRがコメントで荒れにくい!差分が見やすい!といった本当に顧客が求めていた機能が備わっています。 今回は初めてのActionの公開だったため他のAction*1の実装を参考にしながら、 とりあえず動くという状態まで頑張ってもっていってみました。

トランザクション中にMySQLに再接続するということについて

問題

Ruby on Railsではコネクションプーリングが基本的に有効になっていて、コネクションが使い回されるという仕組みになっています。 Auroraにフェイルオーバーが発生し、マスターがレプリカに降格した場合は コネクションがレプリカに向き続けてしまってWriteを発行したらエラーが発生してしまうという問題があります。 この問題に対処するためにアプリケーション側でよしなにフェイルオーバーに気づき、再接続する仕組みを入れないといけないのです。

一般的に ActiveRecord::StatementInvalidMysql2::Error をキャッチして、--read-only なエラーだったら コネクションを再接続するというgemを使ったアプローチが取られることが多いようですが、これは本当に適切なのでしょうか? :thinking_face: トランザクション内で再接続された場合はどうなるんだろうと思い、調べてみました。

ActiveRecord::Base.transaction do
   foo = User.new(name: "foo")
   foo.save!
   ActiveRecord::Base.connection.reconnect! // 状況を再現しやすいようにわざと再接続してみる
   bar = User.new(name: "bar")
   bar.save!
end

puts User.count # 2か0を期待したいが、1が返ってくる

上記の場合ですと、コネクションが切断された段階でデータベースはロールバックされ、ロックが解除されたりします。 しかし、ActiveRecord::Base.transaction は例外を検知しているわけではないので、後続の処理が実行されます。 後続の処理はauto-commit modeで実施されるため、不整合が発生する可能性があります。 これはおそらくdatabase.yml に書く reconnect のオプションと実質同じなんじゃないかと思っています。

解決策

が正しいと思っています。

前者の仕組みとして、active_record_mysql_xverify というgemがあるようです。 active_record_mysql_xverify - so what GitHub - winebarrel/active_record_mysql_xverify このgemはコネクションをプールからcheckoutするときに 前回エラーが発生したというフラグを持っているかつinnodb_read_only が有効だったら、再接続するというアプローチを取っています。 むやみに毎回チェックするわけではないのが良いですね。 ですが、unicornみたいなprefork型のサーバーだと1つのプロセスに対して、1つのコネクションが使われ、そもそもプールに返却されることがないっぽい(?)ので 上記のgemは効果的な解決策にならないみたいです。

また、Delayed JobのバックエンドとしてActive Recordを使っている場合はどこかで再接続する仕組みを用意しなければならず面倒ですね。

Delayed Jobにはlifecycleというフックポイントがいくつか用意されており、これを活用するといいんじゃないかという結論に至りました。 lifecycle.before(:error) と書くことでジョブが再試行される前にコネクションが再接続されると思います。

class Delayed::Plugins::ClearConnectionsOnError < Delayed::Plugin
  callbacks do |lifecycle|
    lifecycle.before(:error) do |_worker, _job|
      is_read_only = ActiveRecord::Base.connection.execute('SHOW GLOBAL VARIABLES LIKE "innodb_read_only"').first.fetch(1) == "ON"
      if is_read_only
        ActiveRecord::Base.clear_all_connections!
      end
    end
  end
end

Delayed::Worker.plugins << Delayed::Plugins::ClearConnectionsOnError

終わりに

Railsノウハウ難しい・・・ 間違っていることを書いていたら、教えていただけると助かります。

合わせて読みたい

MySQL :: MySQL 5.6 リファレンスマニュアル :: 23.7.16 自動再接続動作の制御

Amazon Aurora MySQL を使用する際のベストプラクティス - Amazon Aurora

GitHub ActionsでECSのタスク定義のみを更新する

GitHub ActionsでECSサービスをデプロイするときによく使われる aws-actions/amazon-ecs-deploy-task-definition ですが、 タスク定義のみを更新することもできます。READMEのサンプルコードにはその例がないので、気づきにくいです。

inputs:
  task-definition:
    description: 'The path to the ECS task definition file to register'
    required: true
  service:
    description: 'The name of the ECS service to deploy to. The action will only register the task definition if no service is given.'
    required: false

amazon-ecs-deploy-task-definition/action.yml at 6d98f28115f7e2c01d1aff47de6e7bd5b7c22540 · aws-actions/amazon-ecs-deploy-task-definition · GitHub

inputsにtask-definitionだけ入力してあげるだけでOKです。

# 中略
    - name: register new task definition family
      uses: aws-actions/amazon-ecs-deploy-task-definition@v1
      with:
        task-definition: ${{ steps.render-container.outputs.task-definition }}

ECS Scheduled Taskでバッチ処理するタスク定義の更新などで活用できると思います。

参考

GitHub Actionsがdependabotで発火した場合のみ挙動を変えたい

DependabotのPRを元に発火するGitHub Actionsはread-onlyなGITHUB_TOKENのみを扱うことができるため、step内で Secretsを参照している場合はfailしてしまいます。

【Github actions】DependabotのPull RequestでSecretsが参照できずワークフローがFailになった場合の対処 - Simple minds think alike

failしていると煩わしいので、dependabotで発火した場合はそもそもジョブを実行しないようにするか pull_request_targetworkflow_run を使うようにすると良さそうです。 今回はdependabotで発火した場合は、ジョブを実行しないようにするワークフローを例として残しておきます。

name: example

on:
  push:

jobs:
  verify-actor:
    runs-on: ubuntu-latest
    outputs:
      actor: ${{ steps.echo.outputs.actor }}
    steps:
      - name: echo actor
        id: echo
        run: |
          echo "::set-output name=actor::${{ github.actor }}"
  

  trigger_deploy:
    runs-on: ubuntu-latest
    needs: [verify-actor]
    if: needs.verify-actor.outputs.actor != 'dependabot[bot]'
    steps:
      - uses: actions/checkout@v2

      - name: something
        run: |
          echo "hey"

一つのジョブ内で条件分岐をしようとすると、全てのstepにifを入れないといけません。 そのため、最初に条件分岐をするためだけのジョブを作り、後続のジョブの ifneedsoutputs を参照することで 条件文の記述を1回にしています。

stepの出力を後続stepから参照する場合は、echo "::set-output name=key::value 形式で参照できるようにしておく必要がありますが、 jobの出力を後続jobから参照する場合は、jobのoutputsneeds を使う必要があります。(ややこしい)

参考

【Github actions】DependabotのPull RequestでSecretsが参照できずワークフローがFailになった場合の対処 - Simple minds think alike

GitHub Actions でモノレポ上の変更があったプロジェクトだけテストを走らせる

GitHub ActionsでJobのOutputの値を後続Jobで参照する - notebook