くりにっき

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

go-gitlabが GitLab.com に移管していた

前置き

個人OSSに入れてるdependabotでmoduleをアップデートしてたらgolangci-lintでこういうwarningが出ていたので気づいた。 *1

  Running [/home/runner/golangci-lint-1.60.3-linux-amd64/golangci-lint run] in [/home/runner/work/gitpanda/gitpanda] ...
  Error: gitlab/blob_fetcher.go:19:54: SA1019: gitlab.Client is deprecated: use gitlab.com/gitlab-org/api/client-go instead. See https://gitlab.com/gitlab-org/api/client-go (staticcheck)
  func (f *blobFetcher) fetchPath(path string, client *gitlab.Client, isDebugLogging bool) (*Page, error) {
                                                       ^
  Error: gitlab/commit_fetcher.go:15:56: SA1019: gitlab.Client is deprecated: use gitlab.com/gitlab-org/api/client-go instead. See https://gitlab.com/gitlab-org/api/client-go (staticcheck)
  func (f *commitFetcher) fetchPath(path string, client *gitlab.Client, isDebugLogging bool) (*Page, error) {
                                                         ^
  Error: gitlab/fetcher.go:12:33: SA1019: gitlab.Client is deprecated: use gitlab.com/gitlab-org/api/client-go instead. See https://gitlab.com/gitlab-org/api/client-go (staticcheck)
    fetchPath(path string, client *gitlab.Client, isDebugLogging bool) (*Page, error)
                                   ^
  Error: gitlab/url_parser.go:41:17: SA1019: gitlab.NewClient is deprecated: This module has been migrated to gitlab.com/gitlab-org/api/client-go. See https://gitlab.com/gitlab-org/api/client-go (staticcheck)
    client, err := gitlab.NewClient(params.PrivateToken, options...)
                   ^

GoでGitLabのAPIを使う時には https://github.com/xanzy/go-gitlab を使うんだけど、https://gitlab.com/gitlab-org/api/client-go に移管したらしい。

github.com

golangci-lintを入れていなかったら絶対気づかなかったので入れててよかった〜

雑に置き換えるコマンド

基本的には移管前後でパッケージの構成は変わらないので文字列置換でいいんだけど複数のリポジトリで対応する必要があったのでコマンドで全部できるようにしました。

手元のリポジトリだとこれで全部書き換えました。

find . -name '*.go' -type f -exec sed -i '' 's|github.com/xanzy/go-gitlab|gitlab.com/gitlab-org/api/client-go|g' {} +
sed -i '' 's|github.com/xanzy/go-gitlab v[^ ]*|gitlab.com/gitlab-org/api/client-go latest|' go.mod
go mod tidy
gofmt -w .

Macにデフォルトで入ってるBSD版のsedなのでGNU版はちょっとコマンドが変わるかも。

自作gemでモンキーパッチrbsを利用する

コンテキスト

僕が直近2〜3年以内に新しく作ったgemでは全てrbsとsteepを導入してガッツリ型を書いています。

しかし自分のgemのrbsは書けても、自分のgemが依存しているrubyの標準ライブラリのメソッドや依存している別のgemの方で型定義がなかったり足りていなかったりして steep check が通らなくて困ることがよくあります。

こういう時には https://github.com/ruby/gem_rbs_collection や https://github.com/ruby/rbs にパッチを送ればいいんですが、とはいえ本家に取り込まれるまで自分のgemの開発が止まるのはつらいのでfork版を使うことが多いでしょう。

しかし足りない型定義が多かった時にfork版をメンテするのもつらい気がしているので*1僕はモンキーパッチとしてrbsを入れるようにしています。

こういうことは多分自分しかやっていないだろうなあと思いつつ、他の人がどうやってるか知りたいので自分が普段やってることを紹介してみます。

構成

下記のように sig/non-gemify/ のようなディレクトリを作って、ここにモンキーパッチrbsを入れています。

$ tree sig/
sig/
├── non-gemify
│   ├── io.rbs
│   └── kernel.rbs
├── ruby_header_parser
│   ├── argument_definition.rbs
│   ├── config.rbs
│   ├── enum_definition.rbs
│   ├── function_definition.rbs
│   ├── parser.rbs
│   ├── struct_definition.rbs
│   ├── type_definition.rbs
│   ├── typeref_definition.rbs
│   └── util.rbs
└── ruby_header_parser.rbs

3 directories, 12 files

sig-non-gemify/ のようにしてもよさそうですが好みの問題かと思います。

エディタやIDEによっては sig/ 以外のディレクトリを認識しなくて困ることをなんとなくエスパーして sig/ の中にサブディレクトリを作る方法にしました。(杞憂かも)

手順

モンキーパッチrbsをgemファイルに含めて https://rubygems.org/ で配布を行うと、そのgemをインストールしたユーザの開発環境に悪影響が起きるかもしれません。

そのため、gemspecで下記のように除外設定を入れた方がいいです。

   spec.files = IO.popen(%w[git ls-files -z], chdir: __dir__, err: IO::NULL) do |ls|
     ls.readlines("\x0", chomp: true).reject do |f|
       (f == gemspec) ||
-        f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
+        f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile sig/non-gemify])
     end
   end

あとはsig-non-gemify/ のように sig/ ディレクトリの外に置くようにした場合には下記のように Steepfile に設定追加も必要だと思います。

 target :lib do
   signature "sig"
+  signature "sig-non-gemify"

モンキーパッチrbsが完全になくなった時に Steepfile に存在しないディレクトリを書いた時にエラーにならないかも気になったんですが、今のsteepの最新版(1.8.3)だと問題なかったです。

モンキーパッチrbsを使うメリット

  • 本家のgem_rbs_collectionを併用できるので常に最新を使い続けることができる
  • 本家に投げたパッチが取り込まれた時に自分のgemではそのモンキーパッチrbsだけを消すだけでいい
  • 一気に取り込まれるとは限らないのでマージされたものから段階的にモンキーパッチrbsを消すことができる

最後に

みんなのgem開発時のこの手の困りごとの解決方法を知りたいので教えてください!!!

2024/12/05 20:27追記

ruby-jp slackで聞いたら

sig/gems/ ってディレクトリにいろいろ放り込んでますね。取り込まれた後にお掃除してます、

とのことだった。そうなりますよね...

*1:実際にそういう運用はしたことないのであくまで想像

AWSのTerraformでDynamoDB state lockingからS3-native state lockingに移行する

前置き

先日Terraform 1.10がリリースされました。

www.hashicorp.com

github.com

Experimentalではありますが個人的には Introduce S3-native state locking (Terraform実行時のロックでDynamoDBが不要になる)がアツイ機能で、いち早く使いたいと思っていました。

github.com

しかし、軽くググった感じ移行ガイド的なものが見つからなかったので手順をまとめました。(英語版は末尾にあります)

手順

Terraformのbackendを書き換える

backendで dynamodb_table を指定してる箇所を use_lockfile *1 に書き換えます。

 terraform {
   backend "s3" {
-    dynamodb_table = "terraform-lock"
+    use_lockfile = true
   }
 }

Terraformで使ってるIAMユーザやIAMロールがterraform.tfstate.tflockにアクセスできるようにする

S3-native state lockingはBackendで利用しているS3バケットのtflockファイルへの s3:PutObject が必要になります。

例えば下記のようなkeyの場合、末尾に .tflock を付与した terraform.tfstate.tflock にも権限が必要です。*2

terraform {
  backend "s3" {
    key = "terraform.tfstate"
  }
}

そのため、下記のように変更が必要です。(TerraformがAmazonS3FullAccessを持っている場合には不要)

Before

{
  "Action": [
    "s3:GetObject",
    "s3:PutObject",
    "s3:DeleteObject"
  ],
  "Resource": "arn:aws:s3:::YOUR-BACKEND-BUCKET-NAME/terraform.tfstate",
  "Effect": "Allow"
},

After

{
  "Action": [
    "s3:GetObject",
    "s3:PutObject",
    "s3:DeleteObject"
  ],
  "Resource": [
    "arn:aws:s3:::YOUR-BACKEND-BUCKET-NAME/terraform.tfstate",
    "arn:aws:s3:::YOUR-BACKEND-BUCKET-NAME/terraform.tfstate.tflock"
  ],
  "Effect": "Allow"
},

一応

"Resource": "arn:aws:s3:::YOUR-BACKEND-BUCKET-NAME/terraform.tfstate*"

みたいなこともできるとは思いますが、AWSでは 最小特権アクセス許可を適用する のをベストプラクティスに挙げているので .tflock だけに権限を追加するのがいいと思っています。

https://github.com/sue445/terraform-aws-template/blob/main/cloud_formation/setup-terraform.yml のようにTerraform実行に必要なリソースをCloudFormationでセットアップしてるのであればその時に使ったテンプレートファイルを編集して再実行するのが一番楽だと思います。

この時点でTerraform実行時にDynamoDBが不要になっているはずなのでTerraformを実行する

問題なければTerraform実行用のDynamoDBのテーブルと、そのテーブルへの権限を消す

小規模利用のAWSアカウントであればTerraform実行用のDynamoDBがアカウント全体の費用のそれなりの割合を占めているはずなので、この時点で消していいでしょう。

中規模以降のAWSアカウントであればTerraform実行用のDynamoDBの費用の割合なんて微々たるものなので消しても消さなくてもどっちでもいいと思います。

あとがき1

僕がメンテしてる https://github.com/sue445/terraform-aws-template でもS3-native state lockingに移行したのですが*3、terraform-aws-templateの利用者のための移行手順は下記に書いています。

github.com

あとがき2

自分のリポジトリでは https://github.com/hashicorp/terraform/pull/35661 だけを見て移行したので気づかなかったんですが、 https://developer.hashicorp.com/terraform/language/backend/s3 にはS3-native state lockingに利用するtflockオブジェクトに対する権限が含まれていなかったので追記するPRを出してます。(マージ済)

github.com

Go 1.23でgo.modにtoolchainを書かなくてよくなった

tl;dr;

タイトルが全て

~Go 1.20

go.modに

go 1.20

のようにマイナーバージョンまで書いていてもDependabotでエラーが起きませんでした。

Go 1.21~1.22

go 1.21

のようにマイナーバージョンまで書いていた場合、Dependabotでエラーになります。

この問題を回避するには下記のようにtoolchainでパッチバージョンまで書く必要がありました。

go 1.21
toolchain go1.21.0

詳しくは下記を参照。

github.com

Go 1.23~

下記のようにgoでパッチバージョンだけ書く&toolchainが無い状態でも Dependabotがエラーにならなくなりました!(Go 1.20までと同じ挙動)

go 1.23

詳しくは下記を参照。

github.com

Go 1.23といえばrange-over func*1 が目玉機能だと思いますがtoolchainが不要になったのも地味に嬉しいやつです

create-merge-requestを作った

これ何?

GitLab CI上で行った変更をMerge Requestとして投げるためのツールです。(開発期間は1週間くらい)

gitlab.com

GitHub Actionsだと peter-evans/create-pull-request が便利でよく使っているのですが、同じようなことをGitLab CIでもやりたくて作りました。

サンプル

一番簡単なサンプルはこれ。

stages:
  - build

create_mr_for_changes:
  stage: build
  
  image: debian:stable-slim

  before_script:
    # Download and install latest create-merge-request
    - apt-get update
    - apt-get install -y curl
    - VERSION=$(curl -s --fail https://gitlab.com/sue445/create-merge-request/-/raw/main/VERSION?ref_type=heads)
    - pushd /tmp
    - curl --retry 3 -L -o create-merge-request.tar.gz https://gitlab.com/sue445/create-merge-request/-/releases/${VERSION}/downloads/create-merge-request_Linux_x86_64.tar.gz
    - tar xzvf create-merge-request.tar.gz
    - mv create-merge-request /usr/local/bin
    - popd

  script:
    # TODO: Add your changes
    - date > now.txt

    # Create Merge Request if repo is changed
    - create-merge-request

  rules:
    # Run only scheduled pipeline
    # ref. https://docs.gitlab.com/ee/ci/pipelines/schedules.html
    - if: $CI_PIPELINE_SOURCE == "schedule"

このサンプルでは実行時にnow.txtを更新して、その差分をMerge Requestとして投げています。

頑張りポイント

gitコマンドを使わずにgitリポジトリにアクセスした

create-merge-requestはGoで作りスタンドアローンバイナリとして配布しています。

しかしスタンドアローンバイナリとして配布しているツールが実行環境にインストールされているgitコマンドに依存していると真のスタンドアローンとはいえないため、 https://github.com/go-git/go-git というGo実装のgitを利用して実行環境にgitがインストールされていなくても使えるようにしました。(システムにインストールされているgitコマンドを使うとgitのバージョンが変わった時に振る舞いが変わると面倒なので内部に組み込んだ方が依存を管理しやすいという嬉しさもある)

可能な限りパラメータを省略できるようにした

下記のhelpを見てもらえれば分かりますが、GitLabのリポジトリに新しいcommitをpushしてMerge Requestを投げるだけでも下記のパラメータが登場します。

REQUIRED PARAMETERS:
   --gitlab-api-endpoint value  GitLab API Endpoint (e.g. https://gitlab.com/api/v4) [$GITLAB_API_ENDPOINT, $CI_API_V4_URL]
   --gitlab-access-token value  GitLab access token [$GITLAB_ACCESS_TOKEN]
   --gitlab-project value       GitLab Project Path (e.g. gitlab-org/gitlab) [$GITLAB_PROJECT, $CI_PROJECT_PATH]

OPTIONAL PARAMETERS:
   --author-email value                     author email for commit (default: [email protected])
   --author-name value                      author name for commit (default: create-merge-request)
   --commit-message value, -m value         commit message (default: [create-merge-request] automated change)
   --source-branch value                    Merge Request branch name (default: create-merge-request/patch)
   --source-branch-suffix value             Merge Request branch name suffix (none,random,timestamp,short-commit-hash) (default: none)
   --target-branch value                    Send Merge Request to this branch (e.g. main, master) (default: main) [$CI_DEFAULT_BRANCH]
   --title value, -t value                  Merge Request title (default: Changes by create-merge-request)
   --description value                      Merge Request description (default: Automated changes by [create-merge-request](https://gitlab.com/sue445/create-merge-request))
   --labels value [ --labels value ]        Merge Request labels
   --assignees value [ --assignees value ]  Merge Request assignees (e.g. user1,user2)
   --reviewers value [ --reviewers value ]  Merge Request reviewers (e.g. user1,user2)

しかし初手で大量にパラメータを設定させるのは大変です。

GitLab CIでは $CI_API_V4_URL や $CI_PROJECT_PATH などの環境変数が自動で用意されているため、可能な限りそれらから設定を読み込むようにしました。

それによりGitLab CIで実行する時には GITLAB_ACCESS_TOKEN でGitLabのアクセストークンだけ設定されていればとりあえずMerge Requestが作成できるようにしました。

おまけ

GoのバイナリをGitLabのリポジトリのreleasesに添付する時に https://github.com/goreleaser/goreleaser を利用したのですが、GitLab CI用の設定が現在のGitLabではDeprecatedなものだったのでパッチを投げました。

github.com

terraform-version-updaterを作った

これ何?

https://github.com/tfutils/tfenv で使われる .terraform-version を自動でバージョンアップするためのツールです。(制作期間2日)

github.com

最初の構想

最初は https://github.com/minamijoyo/tfupdate にパッチを送ろうと思っていました。

しかしtfupdateはHCL形式のファイルを扱うことに特化しているため、.terraform-versionのようなプレーンテキストを対応させるのは難しそうな気がしたので別物として作ることにしました。 (できないことはないのだが、結構広範囲に改修する必要がありそうだったり、自分の書いたコードがツールの既存コードとは明らかに異質なものになりそうなので尻込みした)

GitHub Actionsで動かす

DependabotだとTerraformのproviderを自動バージョンアップできますが、Terraformの本体のバージョンは自動バージョンアップできません。

そのためGitHub Actionsでterraform-version-updaterを定期実行することにより、Dependabotのような感じで自動バージョンアップができるようになります

実際に使ったworkflowファイルはリポジトリのREADMEに貼っています。

https://github.com/sue445/terraform-version-updater?tab=readme-ov-file#github-actions-example

feed_squeezerを作った

これなに?

RSSフィードを任意のキーワードで絞り込んだ結果をさらに別のフィードとして返すためのproxy的なウェブアプリです。

github.com

モチベーション

僕はSlackで色々なRSSフィードを購読しています。

YouTubeのチャンネルにもRSSフィードが存在しているのでSlackで購読しています。

例えば 東映アニメーション公式YouTubeチャンネル - YouTube にはプリキュアをはじめとして様々な動画が公開されています。その中でプリキュアに関係する動画だけをSlackで購読したいってことがよくあるため作りました。

2〜3年前くらいに作って個人Slack内でほそぼそと運用していたのですが、OSS化する機運が高まったのでOSS化しました。

2024/06/21 19:59 追記

個人slackだとこういうクエリでfeedをsubscribeしています

使った技術

  • Go
  • Bootstrap
  • Vue.js

工夫ポイント

配布形式をどうするか

Webサービスとして公開するのも考えたのですが、色んなところで使われだすとGoogle Cloudの個人アカウントのCloud RunやCloud Functionsの無料枠が一瞬で吹き飛びそうな気がしたのでDockerイメージやバイナリでの配布にしています。

ちなみにこの画面はローカルで動かしているのを撮影したのでlocalhostになってますが、実際はSlackbotからアクセスできる場所に置く必要があるので自分の好きな場所にDockerイメージやバイナリを動かして利用してください。

僕のおすすめはCloud Runで、Terraformのサンプルコードも置いています。 https://github.com/sue445/feed_squeezer/tree/main/_examples/gcp_cloud_run_terraform

GoでGoogleライクな検索クエリで文字列マッチするような関数を作った

  • AAA BBB : AND検索
  • AAA | BBB : OR検索
  • (AAA BBB) | CCC, (AAA | BBB) CCC : 括弧をつけて検索条件の優先順位付け

具体的には https://github.com/alecthomas/participle というパーサーライブラリを用いて実装しました。

github.com

participleは汎用的なパーサーライブラリなので使うのが難しかったのですが、ChatGPTに助けてもらいながら実装できました。

取得したフィードを一瞬だけキャッシュする

Slackでフィードを購読する時は /feed subscribe *1 を使います。

登録されたフィードは10〜15分おきくらいにSlackbotによる購読チェックが実行されます。

この時にSlackbotは同一のURLに対してほぼ同時にHEADとGETを実行します。

YouTubeチャンネルのフィードには短時間ながらRateLimitが設定されているため、瞬間的にたくさん呼び出すと429エラー(Too Many Requests)になります。

この429エラーを回避するためにキャッシュを導入しました。

バイナリにviewを同梱する

feed_squeezerに渡すurlやqueryなどのパラメータはURLエンコーディングする必要があります。

手でURLエンコーディングするのは大変なのでいい感じに変換するためのフォームをトップページに用意しています。

Goの https://pkg.go.dev/embed を用いてHTMLファイルをバイナリに含めています。

ただしjsやcssなどもバイナリに含めるとバイナリサイズが大きくなりそうだったのでHTMLファイルで利用しているBootstrapやVue.jsなどはCDN経由で参照するようにしました。

ちなみにこれがOSS化する前のfeed_squeezerの画面です。Bootstrapを入れるだけで全然違うなw

最近のエコシステムに乗っかる

今までGo製のツールを作る時はバイナリリリースとかCIでのlint実行とかはオレオレ実装を流用してたのですが、この機会に https://github.com/goreleaser/goreleaser や https://github.com/golangci/golangci-lint にも初挑戦しました。

2024/06/14 13:28 追記:ブコメレス

id:igrep

絞るのに特化したYahoo! Pipesみたいな

なるほど。Yahoo! Pipesは全然知らなかったです