gormのアップデートをした話

はじめに

この記事は、仕事で使っているgormのバージョンをv1からv2にアップデートした時の備忘録です。 またgorm v1を使っている人でかつv2にアップデートしたい人向けの記事なので細かいところは省いています。

経緯

仕事で使っているgormのバージョンがv1で特に問題もなく開発できていたんですが、今後の開発で「v2に上げたほうがいいんじゃないか」という話になりアップデートを進めることになりました。 で、ORMのアップデートとかいい経験になりそうだったので手を挙げてみた感じです。

やったこと

v1とv2の差分確認

まずは、ざっと公式のリリースノートを読みながら変更箇所の把握を進めました。

gorm.io

次に、実際にv2へとアップデートした先人の知見などを確認しておおよその変更箇所を確認していきました。

tech.techtouch.jp

qiita.com

future-architect.github.io

zenn.dev

tchssk.hatenablog.com

大体上記の記事を読めば、おおよその変更差分は確認できますね。

使用するパッケージの置き換え

一番最初に進めたのは gorm v1 を使っている箇所をv2に置き換えることでした。

import (
-     "gorm.io/gorm"
+    "github.com/jinzhu/gorm"
)

パッケージを置き換えることでビルドエラーになっているところがはっきりし、そこを随時直していけば手戻りも少なく済みそうだったので。

DBのドライバー置き換え

次にDBのドライバーを置き換えました。

gorm v2ではv1の時とDBへの接続方法が変わっています。

gorm v1では以下のようなコードで接続できます。

import (
  "github.com/jinzhu/gorm"
  _ "github.com/jinzhu/gorm/dialects/mysql"
)

func main() {
  db, err := gorm.Open("mysql", "user:password@/dbname?charset=utf8&parseTime=True&loc=Local")
  defer db.Close()
}

ただ、v2ではこの辺りの仕様が変わっているので以下のように置き換える必要があります。

import (
  "gorm.io/driver/mysql"
  "gorm.io/gorm"
)

func main() {
  // refer https://github.com/go-sql-driver/mysql#dsn-data-source-name for details
  dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
  db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
}

RecordNotFoundの置き換え

次に、RecordNotFoundエラーの置き換えを進めました。

gorm v1では以下のようにレコードがない場合のエラー処理を行います。

if err := db.Where("name = ?", "jinzhu").First(&user).Error; gorm.IsRecordNotFoundError(err) {
  // record not found
}

この辺りの処理はv2ではerrorsを使ってチェックする方式に変わっています。

errors.Is(err, gorm.ErrRecordNotFound)

サブクエリの置き換え

gorm v1ではサブクエリは以下のようにSubQueryを使っていました。

db.Where("amount > ?", db.Table("orders").Select("AVG(amount)").Where("state = ?", "paid").SubQuery()).Find(&orders)

これがgorm v2ではSubQueryメソッドが廃止され、以下のようにクエリを引数として渡すように変更されています。

subQuery := db.Select("AVG(age)").Where("name LIKE ?", "name%").Table("users")
db.Select("AVG(age) as avgage").Group("name").Having("AVG(age) > (?)", subQuery).Find(&results)

型エラーの修正

gorm v2ではLimit()Offset()が受け取る引数の型がuint64からintに変更されています。

その為、既存の実装を以下のようにキャストして対応しました。

db.Limit(int(limit)).Offset(int(offset))

またCount()が受け取る引数の型もint64に変更されているので以下のようにキャストしてたりします。

var total int64
db.Where("user_id = ?", userID).Count(total)
return uint64(total)

本来であれば元々の引数の型を修正するのが筋がいいんですが、今回は比較的急ぐ必要もあったのでキャストすることで対応しています。 この辺りは今後も改修が必要そうです。

おわりに

基本的にこんな感じの置き換えなどをガガガっと進めたのでアップデートに着手してから大体1か月くらいでリリースまで行けたかな? 変なクエリとか書いて無ければ大体上記に書かれている内容に置き換えていけばすんなりアップデートはできそう。

GitHubで自動リリース&リリースノート自動作成ができるようにしてみた

はじめに

仕事の関係でGitHub上に管理しているコンテンツのリリースとリリースノート作成にだいぶ困っていたので、GitHub Actionsでよしなにできないかやってみた記事です。 対象としてはGitHub Releasesを使っていて、自動でリリースタグが切られたり、自動でリリースノートが作成されるとうれしい人向け。

元々の問題

仕事の関係でかなりの数のコンテンツを管理して修正してたりしてたんですが、リリースタグとか作っていなかったので「どこで・誰が・何を」修正したかが良くわからないということが起きてました。 で、毎回タスクのチケットとか確認して、そこに貼られているPRとかを見て「ああ、この時に修正されたか」とかやってました。

はっきり言ってかなりコストかかってたので何とかできないか考えてました。

で、そんなときにrelease-drafterの存在を知り、こういうのでリリースノートを良い感じに整形すればよさそうだということに思い至りました。

github.com

ただ、別件でデフォルトブランチにPRがマージされたタイミングで自動的にコンテンツがリリースされていたので微妙に用途に合いませんでした。

なので、今回欲しかったものとしては

  • デフォルトブランチにPRがマージされたら自動でリリースタグを切れる
  • デフォルトブランチにPRがマージされたら自動でリリースノートが作成される

というものになります。

あれこれ調べてみたんですが、リリースノートの自動作成とかリリースタグのバージョンのインクリメントはあるけど、ちょうどいいものはなかった感じでした。

というわけで

既存の便利そうなGitHub Actionsを組み合わせてよしなにできないかやってみました。

作ったもの

github.com

使ったGitHub Actions

github.com

github.com

github.com

やったこと

バージョンのインクリメント

mathieudutour/github-tag-actionを使い、前のリリースタグのバージョンを取得し、インクリメントしたものを生成しています。

      - uses: actions/checkout@v2
      - name: Bump version and push tag
        id: tag_version
        uses: mathieudutour/github-tag-action@v5.6
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

ここで生成したバージョンはリリースタグを切るタイミングで使用しています。

変更内容の自動生成

要するにリリースノートを自動生成します。ここではmetcalfc/changelog-generatorを使って以前のリリースから現在の変更までの差分をよしなに生成しています。

      - name: Generate changelog
        id: changelog
        uses: metcalfc/changelog-generator@v1.0.0
        with:
          myToken: ${{ secrets.GITHUB_TOKEN }}

リリースタグを切る

最後にsoftprops/action-gh-releaseを使ってリリースタグを切ります。

      - name: Publish release
        uses: softprops/action-gh-release@v1
        with:
          name: ${{ steps.tag_version.outputs.new_version }}
          tag_name: ${{ steps.tag_version.outputs.new_tag }}
          body: ${{ steps.changelog.outputs.changelog }}

リリースの名前やタグにはバージョンを自動生成した時の値を渡し、リリースの内容には自動生成したリリースノートを渡しています。

あとはこれを自動でリリースタグ作成&リリースノート作成をしたいリポジトリに追加するだけです。

やってみて

意外と既存のものを組み合わせて良い感じに自動化できたので満足してます。 あとGitHub Actionsでの自動化とか深堀しても面白そうかなと思いました。

参考

github.com

github.com

github.com

Mastodonのサーバー情報をGrafana/Prometheus/Statsdで一覧できるようにしてみた

はじめに

先日、管理しているMastodonのサーバーを別の仮想サーバーに移行したので、ついでにいい感じのダッシュボードで諸々の情報を確認できるようにしてみた。

サーバーの移行の経緯はこちら

gamelinks007.hatenablog.com

作ったもの

こんな感じの画面を作りました。

f:id:gamelinks007:20211013232342p:plain

構成としては、収集した情報の表示はGrafanaを使い、情報収集はStatsdを経由してPrometheusで処理するようにしています。

環境

OSはUbuntu 20.04、サーバーとしてはさくらのクラウド上にメモリ3G・コア1の仮想サーバーを使用しています。 またGrafanaとPrometheusはそのサーバーにインストールしています(外形監視はMackerel入れてるので)

やったこと

Mastodon側の設定

Mastodonはデフォルトでstatsdに対応しています。そこで.env.productionSTATSD_ADDR=localhost:9125を追加します。

STATSD_ADDR=localhost:9125

statsd-exporterを追加

次に、Prometheusでデータを収集できるようにstatsd-exporterをインストールします。 以下のリンクからwgetcurlで落としてきて解凍します。

prometheus.io

解凍後、以下のコマンドで/bin配下に移動させます。

sudo mv ./statsd_exporter /bin/statsd_exporter

最後にsystemdで起動できるようにします。

以下の設定を/etc/systemd/system/statsd-exporter.serviceに追加します。

[Unit]
Description=statsd-exporter
After=network.target

[Service]
Type=simple
User=ubuntu
ExecStart=/bin/statsd_exporter

[Install]

WantedBy=multi-user.target

あとは以下のコマンドでstatsd_exporterを有効化し、起動します。

sudo systemctl enable statsd-exporter.service
sudo systemctl daemon-reload
sudo systemctl start statsd-exporter

これでMastodonとstatsd-exporterとの間でデータのやり取りができるようになりました。

Prometheusの導入

基本的にインストール自体は公式のバイナリを落としてきて、それをaptに追加してインストールすればOKです。

prometheus.io

インストール後、/etc/prometheus/promtheus.ymlscrape_configs:に以下の設定を追加します。

  - job_name: mastodon
    static_configs:
      - targets: ['localhost:9102']

これでMastodonの情報をPrometheusに渡すことができるようになりました。

あとは以下のコマンドでPrometheusを起動します。

sudo systemctl start prometheus

Grafanaの導入

最後にGrafanaを導入します。

これも公式が出しているインストール手順通りにすればOKです。

grafana.com

僕のケースの場合、同じサーバー内でGrafana/Prometheus/Statsdを使用するためportの番号を変更しました。

/etc/grafana/grafana.ini/usr/share/grafana/conf/defaults.ini/usr/share/grafana/conf/sample.iniで3000が使用されているので5000とかに変更しています。

またサブドメインを使ってダッシュボードにアクセスできるようにLetsencryptで証明書の発行とDNS周りの設定を変更しています。

それとNginxの設定として以下のようなconfを追加しました。

map $http_upgrade $connection_upgrade {
  default upgrade;
  '' close;
}


server {
    listen 80;
    server_name grafana.gamelinks007.net;
    root /usr/share/nginx/html;
    location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name <設定したサブドメイン>;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;
  ssl_session_tickets off;

  # Uncomment these lines once you acquire a certificate:
  ssl_certificate     /etc/letsencrypt/live/<設定したサブドメイン>/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/<設定したサブドメイン>/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 80m;

  root /usr/share/nginx/html;

  location / {
    proxy_pass http://localhost:5000/;
  }

  location /grafana/api/live {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $http_host;
    proxy_pass http://localhost:5000/;
  }
}

あとはGrafanaの起動とNginxの設定再読み込みするだけです。

sudo systemctl start grafana-server
sudo systemctl reload nginx

これで指定したサブドメインにアクセスするとGrafanaのログイン画面が表示されます。

f:id:gamelinks007:20211014000601p:plain

おわりに

良い感じに管理画面ができたので一時間当たりのつぶやき数とか確認できるようにしてみたいですねー。

参考

stackoverflow.com

grafana.com

zenn.dev

github.com

qiita.com

qiita.com

github.com

gist.github.com

blog.yukimochi.jp

handon.hatenablog.jp

Creatodonのサーバー移管とv3.4.1へのアップデートをした

はじめに

これは僕が管理しているCreatodonというMastodonのサーバーの移管とアップデートを行ったときの備忘録になります。 一部正確な情報ではないかもしれませんのでご了承ください。

経緯

2021年9月30日以降でLet's Encryptのルート証明書が切り替わる件で一週間ほどCreatodonからの配送がつまっていたのが元々の発端。 この件に関しては、証明書の再取得とCentOSのca-certificatesを再インストールすることで解消できました。

元々はさくらインターネットさんが提供されているスタートアップスクリプトで構築したものなんですが、だんだんcld3などの一部ライブラリがインストールしにくかったり、使用しているPostgreSQLのバージョンが古く辛かったりとメンテナンスがかなり難しくなってきていました。

manual.sakura.ad.jp

そこで、今回以下のようにサーバーの移管と使用しているバージョンのアップデートを行うことで根本的な解決に乗り出しました。

  • CentOS7 -> Ubuntu 20.04へとサーバーを移行
  • v3.0.1 -> v3.4.1(実際には今朝7時時点のmasterブランチ)をリリース

ちなみに、元々の想定ではUbuntu 20.04で環境構築だけするつもりだったんですが、mimemagicがGPL違反している件で古いバージョンがすべてRubyGemsから削除されていたのでv3.0.1での環境構築が出来ず、泣く泣くアップデートも行いました。

qiita.com

やったこと

現状の環境の把握

何はともあれ現状の環境を確認することからはじめました。

環境確認時点では以下のような構成になっていました。

  • DB: PostgreSQL 9.6
  • メディア: AWS S3にて管理
  • Dokcer: 使用せず

新しいサーバーにデータを移行する際に一番つらそうなメディアは既にS3に出していたので肝になりそうなのはDBだけだとわかりました。 またDockerを使っていないので、普通にpg_dumpとかでデータをエクスポートすればOKそうでした。

DBを以下に移行できるかが今回の移管作業の成否を決めるといっても過言じゃない感じでしたね。

ちなみに、この記事を読んでさくらのスタートアップスクリプトを使ってMastodonの構築した人向けに捕捉しとくと、UbuntuとかのほかのOSに移行するときには事前にある程度環境構築とかしたうえで作業すると楽ですよ(メディアだけS3に出すとか、移行先に必要なライブラリとか入れとくとか)

移行先のサーバーの環境構築

基本的にMastodon公式が出している手順を参考にPostgreSQLのDBを作成するところまで行いました。

docs.joinmastodon.org

ソースコードのCloneに関してはCreatodonでは一部翻訳の差し替えなどを行っているので一旦後回しにしました。

それとアップデートの関係でRuby 2.7.4をインストールしたんですが、opensslがインストールできないなどのエラーが出るので以下の記事を参考にRubyGems自体をアップデートしておきましょう。

www.task-notes.com

DBの移行

次にDBの移行を行いました。

とりあえず、全部のサービスを止めます。

sudo systemctl stop mastodon-*

次にpg_dumpでDBからバックアップを取得します。

sudo su - postgres
pg_dump mastodon > dump.sql

mastodonの部分にバックアップを取りたいDB名を入れて実行しました。

次に、移行先のサーバーで移行元のサーバーのIPなどでDBにアクセスできるように設定を修正しました。

実際の作業に関しては以下の記事を参考にさせて頂きました。

zenn.dev

ymotongpoo.hatenablog.com

で、取得したバックアップを移行先のDBに放り投げます

psql --host=<移行先のサーバーのIP> --username=<移行先のDBのユーザー> -d <移行先のDB名> < dump.sql

今回は移行元と同じユーザー名とDB名でDBを作成してました。 特に大きな問題はなくすんなりと以降は完了しました。

ただ、移行元のDBが20GB近くあったので時間がかかりましたね……。 その辺を作業前に見積もっておくとよりスムーズに済むかもしれません。

ソースコードの修正

Creatodonでは一部コードを弄っているので、本家のソースコードをそのまま取り込むとエラーやコンフリクトが起きます。 そこで、一旦手元の環境でソースコードの追従を行い、エラーになっている箇所などの洗い出しを行いました。

ここである程度は解消できたんですが、実際にデプロイした際にはcld3周りの改修とかでだいぶ時間を取られました。

github.com

あと、独自テーマを追加している場合はSCSSの変数がいくつか足りなかったり、そもそもなくなったファイルをインポートしてたりするのでアセットがビルドできなくなるので注意してください。

github.com

github.com

github.com

github.com

ソースコードなどの移行

ソースコードを修正出来たら、Mastodon公式のようにsu - mastodonでユーザーを切り替えてliveというディレクトリ名でCloneします。

su - mastodon
git clone https://github.com/S-H-GAMELINKS/mastodon.git

ちなみに最近GitHubソースコードをCloneするときにAPIトークンが必要になってたりします(HTTPSの場合)。 その辺も注意したほうがいいかもしれません。

ちなみに、僕の場合はsshで取ってきました。

ソースコードを移行できたら次は.env.productionをどうにかこうにかして移行しましょう。 僕の場合は原始的にsshでサーバーにそれぞれ接続して内容をコピーして対応しました。

マイグレーション

普通の移行であれば以上のところで大体OKなんですが、僕の場合はアップデートも行う必要があったのでマイグレーションも追加で行いました。

RAILS_ENV=production bundle exec rails db:migrate

で、あとはマイグレーションが終わるのを待つだけと思ってたんですがここでDBのindex周りでエラーが出ました。

そこで公式の出しているコマンドでインデックスの修正を行いました(本来ならDBのバックアップが必要ですが、今回は移行元にバックアップがあるのでしませんでした)

RAILS_ENV=production ./bin/tootctl maintenance fix-duplicates

docs.joinmastodon.org

これでインデックス周りの不具合は解消されたので再度マイグレーションを実行します。

デプロイ作業

あとはアセットをビルドしたり、SSL証明書の発行やNginxの設定をよしなにしておけばOKです。 ただし、ドメインDNSSSL証明書の発行だけは諸々済んだ後でやりましょう。 またドメインDNSを修正してからSSL証明書を発行しないとエラーになるので注意が必要です

おわりに

とりあえずざっと書いていますが、対応を始めてからここまでで7~8時間くらいかかったので細かいところでよりあれこれやってます。 なので本当に必要に迫られない限りはサーバーの移管は大変なので興味本位ではしないようにしましょう。

とりあえず、ガガっと諸々できたので良かった……。

雑にRefinementsが使えるようにしてみた

経緯

最近、「Rubyにこういうのあったら便利かなぁ」と思ったメソッドをrstdというgemに追加して個人プロジェクトとかで使えるようにするのがマイブームになっています。

github.com

で、色々実装している時に、サクッとRefinementsが使えないだろうかと思い、実装してみた記事になります。

やりたかったこと

以下のようなコードで特定のクラス内などでサクッとRefinementsが使いたかった。

class A
    using refine_method(Integer, :inc, ->() {self + 1})

    def inc
      puts 41.inc
    end
end

A.new.inc
# => 42  

実装

で、実際の実装は以下のような感じです。

module RefineKernel
  refine Kernel do
    def refine_method(klass, method_id, expr)
      Module.new{
        refine klass do
          define_method method_id.to_sym, expr
        end
      }
    end
  end
end

using RefineKernel

class A
    using refine_method(Integer, :inc, ->() {self + 1})

    def inc
      puts 41.inc
    end
end

A.new.inc
# => 42  

やってること

module RefineKernel
  # Kernel で refine_method が使えるようにする
  refine Kernel do
    def refine_method(klass, method_id, expr)
      Module.new{
        refine klass do
          define_method method_id.to_sym, expr
        end
      }
    end
  end
end

Kernelrefine_method というメソッドが使えるようにしています。
またrefine_method は引数として klassmethod_idexprを受け取ります。

klassはRefinementsでメソッドを追加したいクラスを引数として渡しており、method_id が追加するメソッド名、expr がメソッドの実装となっています。

あとはusing RefineKernelすれば、以下のようにRefinementsを使ってメソッドを追加し、利用することができます。

using RefineKernel

using refine_method(Integer, :inc, ->() {self + 1})

p 41.inc
# => 42

ちなみに以下のような感じでも動作するので意外と便利かも……?

using refine_method(Array, :pow, ->(num = 2){ self.map{|v| v ** num }})

ary = [1, 2, 3]

puts ary.pow
# => [1, 4, 9]

puts ary.pow(3)
# => 1, 8, 27

using refine_method(Object, :xbox, ->(title = "HALO", *args){
  puts title if args.size == 0
  args.each do |arg|
    puts "#{title} #{arg}"
  end
})

obj = Object.new

obj.xbox
# => HALO

obj.xbox("HALO", "CE", 2, 3)
# => HALO CE
# => HALO 2
# => HALO 3

obj.xbox("Devil May Cry", "", "2", "4", "5")
# => Devil May Cry 
# => Devil May Cry 2
# => Devil May Cry 4
# => Devil May Cry 5

ただ、一度に一つのメソッドしか定義できないのが不満かな。 そこらへんはいい感じにHashオブジェクト内にSymbolとProcを格納してmapとかで回せばいいのかもしれないけど。

実際のPRとか

github.com

github.com

Cognitoで複数のコールバック先を指定して別々のページに遷移できるようにしてみた

注意

仕事の関係でCognitoのに複数のコールバック先を指定できないか試したときの覚書です。

背景

とあるWebサービスでCognitoを使ってソーシャルログインで自動的にアカウントを作成できる画面を作っていました。 で、仕様変更でアカウント登録画面が複数になり、アカウント登録の画面それぞれにうまいことコールバックさせる必要が出てきた。

やったこと

Cognito側に複数のコールバック先を先に指定し、それぞれの登録画面用のソーシャルログインの処理を以下のように切り出した。

    Auth.configure({
      Auth: constAuth,
      oauth: {
        ...constOauth,
        redirectSignIn: process.env.REACT_APP_CALLLBACK_URL,
      },
    });

const socialLogin = async (provider: CognitoHostedUIIdentityProvider) => {
    await Auth.federatedSignIn({
      provider,
    });
  };

const anotherSocialLogin = async (provider: CognitoHostedUIIdentityProvider) => {
    Auth.configure({
      Auth: constAuth,
      oauth: {
        ...constOauth,
        redirectSignIn: process.env.REACT_APP_ANOTHER_CALL_BACK_URL,
      },
    });
    await Auth.federatedSignIn({
      provider,
    });
  };

上記のコードでは、それぞれソーシャルログインでコールバック先を切り替えている。

で、あとはコールバック先でソーシャルログイン時に作成されたアカウント情報とかを受け取って、それぞれの画面に遷移させればOK。 その辺はreact-touterhistoryとか使って前のURLをよしなに比較すればOKな感じです。

参考

ref: Pass "redirectUri" as a parameter to Auth.federatedSignIn #4326

浜田市の防災情報をLINEで通知してくれるBotを作った。

はじめに

浜田市の若者会議のメンバーの方から「浜田市の防災情報を通知してくれるアプリとかあるとうれしい」という話をいただいたので、実際に作ってみた話です。

 

作ったもの

github.com

 

実際にLINEで防災情報を通知してくれるBotはこちらのQRコードを読み込むか「@689nsohm」で友達追加していただければOKです。

f:id:gamelinks007:20210723154237p:plain

やったこと

基本的な構成としてはAWS Lambda/Event Bridge/Ruby 2.7/LINE Messaging APIを使用して作りました。

 

肝としては10分に一回RSSを取得しに行くようにしたことと、その取得時間とRSSの作成時間を比較して直近のものだけをLINEに通知するようにしたところですね(防災情報などはできるだけ最新の情報がラグなくユーザーに届ける必要があるので)

 

今後

とりあえず、防災情報に関しては通知できるようになっているので実用性は高そう。

あとは実際に利用してくれるユーザーをいかに増やすかが課題になりそう