浜田市への意見などを投稿できる目安箱サービスを試しに作ってみた

はじめに

元々は浜田市若者会議という会で、「浜田市に住んでいる若者がどんな意見を持っているかを知りたい」という話が出たのがきっかけです。 とりあえず、作ってはみたんですがまだ実際にリリースしたりするように話が詰まっていないので、本当にリリースするかは未定です。

ちなみに、浜田市若者会議についてはこちらの記事を参照していただければと思います。

gamelinks007.hatenablog.com

で、色々とアイディアとか仕様とかまとめたところを「目安箱のようなサービスでいいんじゃないか?」と思い、この土日を使って実際に作ってみました。

作ったもの

github.com

f:id:gamelinks007:20211115222921p:plain

Firebase/React/TypeScriptを使い、開発しました。

投稿されたご意見はFirebaseのRealtime DBに保存され、保存されるタイミングでCloud Functionsに追加した処理が呼び出され、Slackに自動で転送されるようになっています。

やったこと

基本的にはコミットログを見ていただければわかりますが、Firebase周りの設定を整えたりBlumaを使っていい感じにコンポーネントを作ったりしています。 あと、フォーム周りは react-bulma-componentsbulma-toastを使ってシンプルに実装しています。

なので、全体的にあまり時間はかかってない感じですね。

あとは、Slackへの転送する処理とかを以下のような感じで実装してます。

github.com

ちなみに、問い合わせなども同じようにSlackに流すようにしています。

おわりに

とりあえず、サクッとFirebaseを使ってシンプルな目安箱サービスを作れた! あとは実際にサービスとして提供できるように諸々の話を詰めていきたいですね

GoでWebsocketを使ったリアルタイムチャットを作ってみた

はじめに

勉強がてらGoでWebsocketを使ったチャットアプリを作ってみた時の備忘録です。

作ったもの

github.com

フレームワークとしてはGinを使い、ORMにはgorm v2を使っています。 またWebsocket周りはmelodyを使っています。

github.com

やったこと

細かいところは基本的にはコミットログをにまとまっているのでそちらを見ていただければと思います。

github.com

実装の肝としては、routes/setup.goに以下のようにWebsocket用のルーティングを追加して、

   // チャットルーム用のWebsocket接続先
    router.GET("/rooms/:id/messages/ws", messageController.Websocket)

    m.HandleMessage(func(s *melody.Session, msg []byte) {
        m.Broadcast(msg)
    })

    m.HandleConnect(func(s *melody.Session) {
        log.Printf("websocket connection open. [session: %#v]\n", s)
    })

    m.HandleDisconnect(func(s *melody.Session) {
        log.Printf("websocket connection close. [session: %#v]\n", s)
    })

    // メッセージがブロードキャストされたときの処理
    m.HandleMessage(messageController.Broadcast)

フロント側から接続するときのルーティングとWebsocket周りの接続を設定しています。

あとは、メッセージを作成する処理をコントローラーのアクションを以下のように作成しています。

func (messageController *messageController) Broadcast(s *melody.Session, msg []byte) {

    roomPath := strings.Trim(s.Request.URL.Path, "/roomsmessagews")
    roomID, _ := strconv.ParseUint(roomPath, 10, 64)
    var message model.Message
    message.Content = fmt.Sprintf("%s", msg)
    message.RoomID = roomID
    messageController.messageRepository.Store(messageController.db, &message)

    messageController.m.BroadcastFilter(msg, func(q *melody.Session) bool {
        return q.Request.URL.Path == s.Request.URL.Path
    })
}

あとはフロント側でWebsocketのエンドポイントに接続させておけばいい感じです。

やってみて

意外と簡単に実装できたので今後仕事のコードとかで導入してみてもいいかなと思いました。

gormでPreload周りのメモリ使用量とかを比較してみた

はじめに

gamelinks007.hatenablog.com

この記事は、上記の記事でgormのバージョンを上げる際にPreload周りの挙動を調べてた時のあれこれをまとめたものです。

ことの発端

gorm v2へのアップデート時に、一部処理でメモリリークらしき挙動が確認されたのがことの発端。 で、色々調べてた感じでgorm v2でPrelaod関連でmap[string]interface{}がそこそこ使われてたのでこの辺でメモリリークしてるのかもと思いPreload周りのメモリ使用量を調査してみました。

ちなみに、メモリリークしてたのは変なクエリをコード内で作成してて、それが原因でテーブル内のレコードを全件取得してたのが原因でした……。

前提条件

gorm v1とv2との間でPreloadを多用した際にメモリの使用量に差が出るのかを調査します。

メモリ使用量などはPrometheusやGrafanaを使いグラフで監視することにしました。

また最低限度の実装でも再現するのかを確認したいので使用するライブラリは以下のものだけにしています。

  • gin
  • gorm(v1またはv2)
  • sqlite3

またベンチマークにはApache Benchを使用し、ベンチマーク用のプログラムとしては以下を使用しました。

またデータの数も影響する可能性を考えてデータは以下のようなものを使用した。

  • users
    • 件数:10000
    • テーブル構成:create table users(id integer primary_key, name text not null);
  • tweets
    • 件数:10000
    • テーブル構成:create table tweets(id integer primary_key, content text not null, user_id integer);

なお、tweetsのuser_idはランダムにしているためv1とv2とで取得する件数は若干違います(ただしその差は2件のみ)

調査結果

f:id:gamelinks007:20211027215459p:plain

以下のリクエストをそれぞれのエンドポイントに投げたグラフが上のものです。

ab -n 10000 -c 50 <エンドポイント>

なお、v1とv2それぞれのリクエスト成功数は以下の通りです

v1: Total of 3305 requests completed(17:49に開始)
v2: Total of 5588 requests completed(17:50に開始)

傾向としては - v1 - 全体的にヒープ上のメモリを使う - ヒープのオブジェクト数はv2より少ない - GCの呼び出し回数はv2より多い - v2 - 全体的にヒープ上のメモリはv1より使用しない - ヒープ上のオブジェクト数はv1に比べてかなり多い - GCの呼び出し回数はv1より少ない

総合するとv2のほうが総合的なパフォーマンスは上っぽい。リクエストをさばいた量も多く、且つヒープ上のメモリもv1に比べると少ないのも評価できるポイント。

ただv2はGC自体の呼び出し回数が少ないので他のクエリも同時に捌いている状態だとパフォーマンスが悪くなる可能性があるかもくらい……?

おわりに

トータルで見るとgorm v2のほうがパフォーマンスが上なので、これからGoでWebアプリ開発とかするのであればgorm v2使うほうが良さそう。

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時間くらいかかったので細かいところでよりあれこれやってます。 なので本当に必要に迫られない限りはサーバーの移管は大変なので興味本位ではしないようにしましょう。

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