error codes

ソフトウェアエンジニアリングなど、学んだこと、思ったことの記録です。

なんで GraphQL クエリ書いているんだっけ?

雑記。

graphql-codegen

GraphQL は便利だ。

うまく言えない部分も多いけど、 GraphQL を使ったプロジェクトと、普通の HTTP の API を使ったプロジェクトだと、GraphQL を使ったプロジェクトの方が開発体験が良い。あるいは、開発体験を上げるためのステップが少なく感じる。 GraphQL 周辺のエコシステムが充実しているのもあるだろう。

その中でよく使われるのは graphql-codegen だ。自分の周りだと、ほとんどの場合、 graphql-codegen を使ってフロントエンドのコード生成をしている。

一方で、少しイマイチだな、と思う場面が増えてきて、それでこれを書いている。

SQL と O/R Mapper

GraphQL はクエリ言語だが、昔からあるクエリ言語が SQL だ。

そのままの SQL を使うことも多いけど、アプリケーションからは何らかの O/R Mapper やクエリビルダーと呼ぶようなものを使うことが自分は多い。 O/R Mapper の功罪はあると思うけど(自分は深くはわからない)、長くアプリケーション開発で SQLRDBMS が使われるに至った貢献の一つをしているような気もする。

クエリをプログラマブルに扱えるし、特に現代であれば、言語の強力な型システムの恩恵を受けることができる。

Prisma を初めて触ったときは結構感動した(と同時に TypeScript の型システムの柔軟さにも感動した)。

クエリにも、その結果にも、厳密な型が効く。

そうであれば、同じクエリ言語である GraphQL を扱う場面でも同じような体験が得たいように思う。

GraphQL を TypeScript で書く

graphql-codegen で GraphQL クエリを書くときは、下記のような感じでテンプレートリテラルでクエリを書いたり、クエリを別ファイルにして書いたりしている。

const GetArticleQuery = gql`
  query getArticle($id: Int!) {
    article(id: $id) {
      id
      title
      body
    }
  }
`

VS Code でもエクステンションを入れて、スキーマがちゃんと読まれていれば、ちゃんと補完も効いている気がするけど、そんなに体験は良くはない(あと、正直、自分は GraphQL のシンタックスをあんまり覚えていなかったりする)。

クエリの表現(仕様?)上の問題も少し感じていて、 Fragment と variables の噛み合わせもあまり良くなかったりする。(もっともこれをクエリ言語に求めるのも求めすぎのような気もしていて、それこそ別の言語やツールでプリプロセスするべき範囲なのかもしれない)

あと、コード生成のタイミングの問題もある。クエリを書いてから、型を含むコード生成をするわけだけど、新しいクエリを書いたりちょっとクエリを修正したりするたびに、コード生成のコマンドを実行するのは面倒だと感じる。

一方、 Prisma の体験を振り返ってみると、スキーマを変えた後に一度だけコード生成をすれば良いだけだし、 TypeScript のコード上でガンガン補完が効く。

フロントエンドでの GraphQL を使った開発も、同じように TypeScript の世界にいたままクエリをかけるのがいいのではないかと思うようになった。

そんな感じの体験を実現するライブラリ

TypeScript のコード上で、型の恩恵を受けつつ GraphQL のクエリを組み立ててくれるライブラリは、いくつかある。

例えば、 genql のリンク先の補完の様子とかを見てほしいけど、こういう体験ができたらいいよねと思う。

ちなみに最後の GQty はかなり野心的な気がしていて、自分は、 RailsActiveRecord をフロントの世界に持ってきてしまったものように見えた。

ここまで書いてきて、結局、自分はちゃんとしたプロダクト開発で、これらのライブラリを使ったことはないので、実際の開発体験はどうなのかみたいのは自信がない。

その他

こんな感じのことをここ最近考えていて、自分でも GraphQL のクエリビルダを書いてみていた。

GitHub - daisaru11/graqq: Write GraphQL queries in TypeScript

一方で、既存のライブラリの劣化再生産みたいにしかならなくて、ちょっと書き続けるモチベが落ちている。

ちょっと面白かったのは、TypeScript の型生成の周辺で、 TypeScript の Compiler API を触ったところ。TypeScript の AST を組み上げて、バッとコードを生成する。 下記の記事がわかりやすい。 zenn.dev

他の場面でも、いろいろ便利に使えそうなものに感じたので、機会があれば使ってみたい。

React を学び直す

最近は React と戯れる時間が増えた。

React 自体は、だいぶ昔から触っているし、プロダクトのコードも時々修正したり、修正を眺めたりしていたんだけど、あまりしっかりとコードを書く機会はしばらくなかった。

大きな差分としては、 Hooks だった。 Hooks が出た当初、どうしてこのコードが動くんだろうというのが第一印象で、その後なんとなく理解したもののなんか変なものを入れたなあという印象があった。 時間を経て、今回しっかり使ってみると、ああこれは手に馴染む仕組みだなと、感じた。

カスタムフックを使うと、コンポーネントから、状態管理や、副作用を伴うような非同期処理を引き剥がすことができる。このカスタムフックを使う感触がよくて、凝りすぎると見通しが悪くなるだろうが、なかなか書き味がよい。

また、このあたりは、 redux やその周辺ライブラリが一部担っていた気がするけど、Context API もできたいま、 redux を使う必要はあるのだろうか、と思ったりもする。

Portal ってやつも便利だった。Modal とか DOMツリーを外れるコンポーネントをさっと作れる。

ライブラリとしては、 styled-component / styled-system というのを少し覚えた。これは人によってちょっと好き嫌いはあるかもだけど、個人的には便利に思えた。

React / TypeScript / VS Code / ESLint / Prettier というスタックは、定番となったし、本当に生産性が高い。

一方で、 Hooks の概念をちゃんと理解する、TypeScript を自在に扱う、というのがより重要になってきていて、ちゃんとしたフロントエンドのコードを書く、依然として難しい時代だなと感じる。

BigQuery Data Transfer Service / S3 転送の設定を Terraform で書く

BigQuery Data Transfer Service というのがあって、この S3 転送 というのを使うと、ログなどのデータ転送をだいぶ楽に行えることを知った。

これまでは、embulk とかを使って転送していたわけだけど、どこで動かすかとかを考えると地味に面倒で、一方 Data Transfer Service を使うと転送設定を書いておくだけで良いので、とても楽。

今回転送したかったものは、ログデータで、 S3 に日付ごとのパスに配置されている。これを日次で BigQuery に転送したい。

また転送したいログはいくつかあって、手動で設定していると管理が難しいので Terraform で転送設定を書くことにする。

resource "google_bigquery_dataset" "logs" {
  dataset_id = "test_logs"
  location   = "asia-northeast1"
}

resource "google_bigquery_table" "logs_app_log1" {
  dataset_id = google_bigquery_dataset.logs.dataset_id
  table_id   = "app_log1"

  time_partitioning {
    type = "DAY"
  }

  schema = file("...")
}

resource "google_bigquery_data_transfer_config" "logs_app_log1" {
  display_name   = "app_log1"
  data_source_id = "amazon_s3"

  params = {
    data_path                       = "s3://logs-bucket/app_log1/{run_time-24h|\"%Y\"}/{run_time-24h|\"%m\"}/{run_time-24h|\"%d\"}/*"
    destination_table_name_template = "app_log1$${run_time-24h|\"%Y%m%d\"}"
    field_delimiter                 = ","
    file_format                     = "JSON"
    max_bad_records                 = "0"
    access_key_id                   = aws_iam_access_key.transfer_user.id
    secret_access_key               = aws_iam_access_key.transfer_user.secret
    skip_leading_rows               = "0"
  }

  schedule                 = "every day 06:35"
  data_refresh_window_days = 0

  destination_dataset_id = google_bigquery_dataset.logs.dataset_id
  location               = "asia-northeast1"

  lifecycle {
    ignore_changes = [params["secret_access_key"]]
  }
}

ランタイムパラメータ というのを使って、ジョブが実行される日付を元に S3 のパスを指定できるので、日付ごとにパスが切られているようなログの溜め方をしていると、転送が行いやすい。

過去分のデータの転送は、バックフィル実行というのを行えば良いようだ。 Cloud Console からも行えるし、APIもありそうなので、すでにS3にデータが溜まっている場合も過去分データを問題なく転送できそう。

もう少しちゃんと使ってみると、はまりどころなども出てくるかもしれないが、いったんうまく動いたように見える。

コードレビュー

この記事を読んで。

Pull Request レビューの限界

とても同意できる部分が多かったし、考えさせられる部分も多かった。 チーム開発において、コードレビューが当たり前のように正しいもの、と認識されるようになって、ただ当たり前になりすぎて、どういう意味があるのか、少し考える機会が少ないのかもしれない。

コードレビューはちゃんとやろうとするとかなりの時間をとられる。まとまったコードのPull Requestに対して、背景を理解して、差分を隅々まで見て、差分以外の関連する部分も確認する。背後にあるデータや様々な条件に想像を膨らませる。こういうことを真面目にやると、2、3時間かかる事は普通にある。

僕は、コードレビューには、大きく二つの目的があると思っていて、コードの質に対するレビューと、不具合を見つけるためのレビューがあると考えている。 それらがどれくらい必要とされるかは、チームの状態だったり、プロダクトの状況・重要性によって、結構幅があるんじゃないかと思う。人は、いろんなプロジェクトに関わって、その中でいろんなチームの文化に触れて、コードレビューに対する考え方を持っていくんじゃないかと思うし、コードレビューといったときに、哲学とまではいわないけどなんらかの思いを持っているんじゃないかと思う。

自分は、どちらかというとコードレビューというものに価値がある、と思っている方だと思う。

ただ、コードの質を上げる、という意味においては、コードレビューによって直接的・本質的な効果は出にくいなと思っていて、そういったものは普段の会話の中で設計について議論したり、チーム全体のエンジニアリングの力を上げるための施策をしたほうが効果的だと思っている。

振り返ると、自分がコードレビューで重視してきたのは、不具合や危険なコードを防ぐ、というところが大きい。 これはおそらく経験によるところがあって、自分が経験がだいぶ浅い時代、あるチームに参加したときに、すごく厳しいレビューをする人がいた。自分の人生の中でも尊敬するエンジニアの一人だ。そのチームのプロダクトは、コードも歴史を重ねていて複雑になっていたし、決して綺麗とは言えるコードではなかった。普通に考えると、不具合も起きやすい状態だった。でも、その人は、すべてのコードに対して丁寧に目を通していて、そんな条件のバグ見つけるか?っていう不具合を、コードを読んだだけで見つけていた。もちろん、その人が書くコードにほとんど不具合はなかった。人はここまでコードに対して注意深くなれるのかと衝撃をうけた。 おそらく、これは人によって感じ方は違うと思うけど、自分はこの人のことをかっこいいと思ったし、ここまでしてちゃんと自分たちがリリースするコードに責任をもつ、というのがプロのエンジニアなんだと思った。

だから自分は、おそらく、人より注意深くコードレビューをする方だと思うし、時間をかけることが多い。ただ、これはだいぶ偏った考え方の自覚もあるから、全員がやるべきだとも思わない。

でも、そうやってコードを読む、ということを習慣づけていくと、コードを読むということが自分の力になるというのを実感するようになる。コードを読んでそのプロダクトの詳細を理解する。そうするとちゃんとそのプロダクトでエンジニアとして価値を出せる。そう思うようになった。

一方で、いろんなエンジニアを見ていると、人それぞれに強い領域があって、チームでの価値の出し方も違うし、成長の仕方も全然違うんだなとも思うようになった。だから、コードレビュー怠けるけど、コードをたくさん書くみたいな人がいてもいいし、そういうチームの方が強いと思う。

チームの文化としてコードレビューはこうあるべき、というのはあまり考えなくなったけど、自分自身としては、これからもなるべく人のコードをたくさん読んでいきたいとは思っている。

振り返り、SRE、自動化

去年の後半あたり、SRE 本を読んだ。

買ったのは、翻訳本の発売すぐだったので、だいぶ前なのだけど、その当時はあまり面白いと思えずに読むのをやめてしまったのだと思う。そのあと、所謂、インフラエンジニアというロールになるのを意識し始めて、クラウドAWS)を使って、システムの構築や運用をする経験を積んでいった。

AWS のたくさんあるサービスを調べて、小さなスタートアップのシステムを効率よく構築・運用するにはどうしたらいいかを考えた。構成管理を行い、 CI/CD の仕組みを整えた。途中から、社内でマイクロサービスの波が来たので、基盤のようなものが必要だと考えて、ロギングやモニタリングの横串の仕組みを整えたりした。

そういったの経て、 SRE 本を読んだのもあると思う。また、直近で Kubernetes に触れる機会があったのも大きいと思う。SRE 本を読んで、面白い、わかる、と少し感じられるようになった。

自律的なシステム

SRE 本では、自動化にはレベルがあるといっていて、最もハイレベルなものは自律的なシステムだといっている。 これまで自分は、自動化というと、Terraform や Ansible なもので構成管理をしたり、特定の目的のスクリプトを組んで流したりする、そういうものが一番に頭に浮かんでいた。 自律的なシステムは、外部ではなく内部に仕組みを持ち、自己修復性を持ち、正しい状態に収束するように動く。Kubernetes の Operator は、まさしくそういうものだと思うし、クラウドネイティブ全体がそういうパラダイムと言えるかもしれない。

この話は、冪等性、中央集権 vs 分割統治、また、 push型 vs pull型などいろんな話題を含んでいるように思える。いずれにせよ、ソフトウェアエンジニアリングというのは、すこし見方を変えると、硬直的だったものが柔軟でスケーラブルなものに変わったりするのが面白い。 また、壊れることを前提に考えるというのも重要だ。そうすると壊れにくいものができる。壊れるものをコントロールするには、ソフトウェアを書く、ということが必要になる。

SRE は、自動化のためにエンジニアリングに注力する。これは、元々アプリケーションを書いていた自分にとっては、ポジティブなことに感じる。コードを書くのは楽しい。コードを書くことで、システムをコントロールして、スケーラブルな仕組みを作る。 実際には、すごく高度な自律性のあるシステムを組むということは、今の自分の状況だと稀だろう。でも、ちょっとした glue コードを書く、何かのイベントに反応してシステムの状態を柔軟に変える、とか明日からやっていけることは多くある。

話はすこし逸れて、Go という言語。最近は、だいぶ手に馴染んできたなと感じる。 小さなバイナリになり、デプロイしやすい。安定的に動く。リソース消費も小さい。標準ライブラリも充実している。小さなスクリプトを書くこともできるし、サーバを書くこともできる。システムを扱うようなエンジニアにとっては、かなり理想に近い言語なのではないかと思う。 クラウドネイティブ関連の OSS を見ていると、多くは Go で書かれているので、そういったときに躊躇なくコードが読める、というのも非常に大きい。

共通基盤をつくる

基盤というものについて。今の自分の仕事の一つが、アプリケーションを効率的に、開発・運用するための基盤を作ること、だと思っている。 本番運用できるシステムというのは、それなりに考慮しておかないといけないことがある。それはスタートアップの小さなシステムであっても。モニタリング、ロギング、セキュリティ、守っておかなければならないものがある。 歴史を重ねてくると、運用するサービスは増えていく。新しいプロダクトも立ち上がる。そういった中で、あるサービスを効率的にプロダクションレディに持っていくにはどうしたらいいか、あるいはプロダクションレディだと自信を持つにはどうしたらいいか、と考える。一つの方法が、共通基盤をつくってそこに乗せるというやり方だと思う。

ある程度はうまくいっていると思う。一方で、迷いも多くて、自分が考えた基盤は正しい設計だったのか、正しい技術選択だったか、不安になることもある。また、その基盤が硬直性を生んでいるのではないか、使う側の自由を奪っていないか、そうのもある。 クラウドは、プリミティブなものから、どんどん抽象化されてアプリケーションの課題を解決するために洗練されてきている。特にサーバレスの文脈でそう思う。 基盤を作るというは、そういう状況の中で、足りないものを埋める、あるいはあえて制約を課す、ということだと思う。時代と要求に合わせて、拡張する、あるいは消える、ということをしていかなければならない。アプリケーションエンジニアが、必要としているコンポーネントを使える状況を作っておく。その上で信頼性の高いシステムになるようにする。

アプリケーションのパラダイムも知っておくこと、クラウド技術のパラダイムも知っておくこと、両方が重要だと思うが、それは楽しいことだとも思う。

これから

大きな組織の SRE の話を聞くとやっていることのレベルが違いすぎて圧倒されることはある。まあ、自分で SRE を名乗ったことはないのだが、先人たちの偉大なプラクティスを、自分なりに噛み砕いて実践していくことには価値があると思っている。

最近自分は、「コントロール」という言葉を意識的に使うようになった。サービスが増えていくとだんだん詳細は把握できなくなっていくし、昔のことは忘れてしまったりする。権限移譲も行っていく必要がある。そういう中で、システム全体に自信を持つにはどうしたら良いか、コントロールできている、という意識を持つにはどうしたら良いか。正しく動かすにはどうしたら良いか。 小さなチームで、大きなシステムを扱えるようにするというのは、エンジニアリング、なのだと思う。

tfupdate を Github Actions で定期実行する

一ヶ月ぐらい前から、 tfupdateGithub Actions から定期的に実行する生活をしている。

tfupdate は、個人的にはとても嬉しいツールで、確かにこれは欲しかったものだということを感じさせてくれるものだった。

仕事で管理している terraform の state はどんどん増えていて、それぞれに terraform や provider のバージョンを指定している。これまでは、たまに思い立ったようにアップデートしたり、新しい AWS のサービスが使えなくて都度アップデートしたりしていたが、state 数が多いと結構面倒な作業だった。

tfupdate を CI で定期的に実行して、自動で PR 作成、あとはマージするだけ、としておくと常に最新の terraform と provider で日々仕事が行える。

CircleCI で実行する場合は、作者の方の記事 や、Orbもあるようなので、それを使うと良いと思う。

自分は、最近は CircleCI から Github Actions に徐々に移行していて、tfupdate も Github Actions から実行するようにしている。

Github Actions から tfupdate を実行する

自分の場合は、自作の Actions を作っていて、これを使っている。

daisaru11/tfupdate-github-actions

これを使う場合は、次のような yamlリポジトリに置くだけで動くと思う。

on:
  schedule:
    - cron:  '0 0 * * *'

jobs:
  update_terraform:
    runs-on: ubuntu-latest
    name: Update terraform versions
    steps:
    - name: "Checkout"
      uses: actions/checkout@v1
    - name: tfupdate
      uses: daisaru11/tfupdate-github-actions@v1
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        tfupdate_subcommand: terraform
        tfupdate_path: './my_workspace'

  update_provider:
    runs-on: ubuntu-latest
    name: Update provider versions
    steps:
    - name: "Checkout"
      uses: actions/checkout@v1
    - name: tfupdate
      uses: daisaru11/tfupdate-github-actions@v1
      with:
        github_token: ${{ secrets.GITHUB_TOKEN }}
        tfupdate_subcommand: provider
        tfupdate_path: './my_workspace'
        tfupdate_provider_name: aws

tfenv に対応する

tfenv を使っていて、 .terraform-version ファイルを置いている場合は、これもアップデートしないと、HCL 側の required_version と齟齬が出てしまう。

若干、アドホックな対応で、これでよかったのかと思うところはあるけれど、 update_tfenv_version_files というフラグを action に渡せるようになっていて、これを有効にすると .terraform-version も更新してくれるようになっている。

        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          tfupdate_subcommand: terraform
          tfupdate_path: "./my_workspace"
          update_tfenv_version_files: true

Terraform の Github Actions と一緒に使う

Terraform の plan と apply も 公式の action を使って行っている場合。

こちらの action には、 tf_actions_version という使用する Terraform の バージョンを指定する引数があるので、これも更新されるようにしないといけない。

いくつか方法をある気がするけど、自分は前述の tfenv を使っていて .terraform-version が置いてある状況だったので、これを参照するようにした。

    - name: "Detect terraform version"
      id: get_tf_version
      working-directory: "./my_workspace"
      run: echo "::set-output name=tf_version::$(cat .terraform-version)"
    - name: "Terraform Plan"
      id: tfplan
      uses: hashicorp/terraform-github-actions@master
      with:
        tf_actions_version: ${{ steps.get_tf_version.outputs.tf_version }}
        tf_actions_subcommand: "plan"
        tf_actions_working_dir: "./my_workspace"
        tf_actions_comment: true

::set-output というコマンドを標準出力に出しておくと、それを後続のステップで使用することができる。

まとめ

上記のような感じで、terraform と provider のバージョンを最新に保つようにしている。

今は毎週月曜の朝に定期実行されるようにセットしている。自動でつくられた PR をみて、plan に変な差分が出てないことだけを確認してマージする。良い気分で一週間が始まるようになった。

こまごまとしたツールをまとめてインストールための Github Actions を書いた

kubernetes にデプロイする時に、 kustomize とか skaffold とかを使っていて、Github Actions から行っていた。

最近のツールはバイナリ で配布されていて便利なのだけども、それでもいくつものツールを curl でダウンロードしてパスを通すというスクリプトをベタ書きするのは、ちょっとあれだったので、一つの Action にした。

github.com

Docker の Action は書いたことあるけど、nodejs の Action は書いたことなかったので、練習も兼ねている。

使い方はこんな感じで、必要なツールをバージョンと共に書けばインストールされるようにしている。

      - name: Set up tools
        uses: daisaru11/setup-cd-tools@v1
        with:
          kubectl: '1.6.0'
          kustomize: '3.5.4'