GitHub Actions 上で MySQL のシステム変数をカスタマイズする #GitHub #Actions #MySQL

概要

GitHub Actions の Workflow では、 Services として MySQL が利用できる。

character_set_server などのシステム変数を変更する方法についてまとめる。

採用した方法: SQL ファイルでグローバル変数を書き換える

Workflow で使う MySQL は基本使い捨てになるため、グローバル変数を書き換える方式にした(ボツ案は後述)。

グローバル変数の書き換えは再起動時に失われるが、そもそも使い捨てであることを考えると、この方式が一番シンプルになると思われたため。

下記はチェックアウトして、直下の SQL を流すだけのサンプル。

jobs:  
  php:  
    runs-on: ubuntu-latest
    services:  
      mysql:  
        image: mysql
        ports:  
          - 3306:3306  
        env:  
          MYSQL_ALLOW_EMPTY_PASSWORD: yes

steps:  
  - uses: actions/checkout@v3  

  - run: |
      mysql -u root --protocol tcp < setup.sql

SQL は下記のような感じになる。

SET @@GLOBAL.character_set_server = 'utf8mb4';  
SET @@GLOBAL.collation_server = 'utf8mb4_general_ci';

ちなみにプロトコルを指定しているのは、 ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2) とか言われて怒られるため。

詳細は公式ドキュメント参照。

MySQL :: MySQL 8.0 Reference Manual :: 4.2.4 Connecting to the MySQL Server Using Command Options

この方式で最低限 SQL ファイルは構成管理されるが、あくまでユニットテストなどを流すために利用する使い方を想定しており、本番用の設定は別で管理するのを前提にしている。

採用しなかった方法①: 設定ファイルで書き換え

my.cnf のような設定ファイルを別途用意する方法も当初は考えたが、下記の理由で採用を見送った。

  • Workflow のジョブは MySQL サービスが立ち上がった後に動く
  • my.cnf の反映は大抵再起動が必要

再起動して、かつサービスがレディになるのを改めて待つのは複雑になったり Workflow の実行時間が伸びてしまうのに抵抗があった。

nao-y.hatenablog.com

採用しなかった方法②: options

MySQL の公式イメージを純粋に使用する場合、 CMD を上書きすることで設定ファイルなしのパラメータ変更方法( Configuration without a cnf file )が紹介されている。

https://hub.docker.com/_/mysql

ただし、GitHub Actions の services では CMD の変更方法は提供されていないようだ。

options (jobs.<job_id>.services.<service_id>.options) で docker create の option は設定できるため、ENTRYPOINT の上書きでどうにかならないか試行錯誤してみたが、どうも行き詰まったので諦めた。

docs.github.com

その他参考

DataGrip で sql_mode に STRICT_TRANS_TABLES が適用される #mysql

概要

DataGrip で MySQL に接続すると、 sql_mode を別途設定しているにも関わらず、STRICT_TRANS_TABLES が勝手に適用されていた。

グローバルの設定を確認すると、意図したものが設定されているが、セッションの設定は STRICT_TRANS_TABLES になってしまう。

DataGrip に限らず、 IntelliJ IDEA とか JetBrains 系 IDE の DB ツールで同じことが起きると思う。

-- グローバルは問題ないけど
SELECT @@GLOBAL.sql_mode;
-- SESSION だと何故か STRICT_TRANS_TABLES
SELECT @@SESSION.sql_mode;

原因

DataGrip が MySQL の接続に JDBC ドライバを使用していたため。

JDBC は STRICT_TRANS_TABLES を有効にするようになっている。

以下はチケットに対して、バグじゃないよ、という話がされているのが下記リンク。

bugs.mysql.com

STRICT_TRANS_TABLES とは何か

sql_mod に設定することで、 SQL を Strict (厳密) にチェックしてくれるようになる。

逆にオフにしていると、たとえば文字列長超えを切り捨ててデータ登録できたり、デフォルト値を持つカラムに Null でクエリを発行した場合にデフォルト値で更新してくれたりする (警告は出る)。

MySQL 5.7.5 からはデフォルトで有効になっている。

dev.mysql.com

JDBC は何故勝手に有効にするのか

jdbcCompliantTruncation を有効にするためには、 STRICT_TRANS_TABLES を有効にしておく必要があるため、jdbcCompliantTruncation を有効にすると自動的に sql_mode に設定されるようだ。

jdbcCompliantTruncation は JDBC の設定値で、前述のような値切り捨てが発生した場合に例外を投げるかどうかを設定できる。

リファレンスにも STRICT_TRANS_TABLES を有効にしないと効果がないことが書かれている。

dev.mysql.com

DataGrip 等での設定のカスタマイズ方法

データソースの設定の「Advanced」でカスタマイズできる。

jdbcCompliantTruncation は true / false で指定すればよい。

sql_mode を変更したいときは sessionVariables に sql_mode='aaa,bbb' のような形式で設定すれば OK。(まるっと無効にしたいときは sql_mode=0

stackoverflow.com

終わりに

  • JDBC を使っている DB クライアントやアプリケーションでは同じことが起きそう
  • Strict でも問題なく動くように常日頃から実装していきたい

参考

Goland で Docker を使った開発環境を作成する #golang #goland

概要

ネットに十分な情報はあるのだが、なんやかんや毎回詰まったりちょこちょこ調べたりしているので自分用にまとめておく。

  • Goland を使う
  • Docker で動かす Web アプリケーション
  • air でホットリロード
  • delve でリモートデバッグ
  • テストも Docker 経由でデバッグ

今回のサンプルは GitHub に置いてある。

github.com

雛形の作成

mkdir go-dev-starter
cd go-dev-starter
# 初期化
go mod init

Hello World を表示するだけの Web アプリケーションをサンプルとして作成する。

ポートだけ環境変数で変えられるようにしてある。

package main

import (
    "fmt"
    "net/http"
    "os"
)

func main() {
    err := http.ListenAndServe(
        fmt.Sprintf(":%s", getEnv("PORT", "18080")),
        http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            fmt.Fprintln(w, "Hello, world!")
        }))

    if err != nil {
        fmt.Printf("failed to terminate server: %v", err)
        os.Exit(1)
    }
}

// getEnv は指定した key の環境変数を取得します
// 環境変数が未設定の場合 fallback の値を返します
func getEnv(key, fallback string) string {
    if value, ok := os.LookupEnv(key); ok {
        return value
    }
    return fallback
}

Dockerfile の作成

delve と air を入れた Dockerfile を作成する。

今回は分かりやすくするために、開発用のみ記載している。

FROM golang:1.19  
  
WORKDIR /app  
RUN go install github.com/cosmtrek/air@latest  
RUN go install github.com/go-delve/delve/cmd/dlv@latest  
  
CMD ["air"]

docker-compose.yml の作成

docker-compose.yml を用意する。

security_opt と cap_add は delve のリモートデバッグ用に設定してある。

40000 番のポートはリモートデバッグで Listen する用。

version: "3.9"  
services:  
  app:  
    container_name: dev-server  
    build:  
      context: .  
    environment:  
      PORT: 8080  
    volumes:  
      - .:/app  
    ports:  
      - "18000:8080"  
      - "40000:40000"  
    security_opt:  
      - "apparmor=unconfined"  
    cap_add:  
      - SYS_PTRACE

air の設定ファイルの作成

コンテナ内で air init すると設定ファイルの雛形が作成される。

docker compose up -d
docker compose exec app air init

ビルドコマンドと full_bin を delve を使うように書き換える。

--- a/.air.toml
+++ b/.air.toml
@@ -5,14 +5,14 @@ tmp_dir = "tmp"
 [build]
   args_bin = []
   bin = "./tmp/main"
-  cmd = "go build -o ./tmp/main ."
+  cmd = "go build -gcflags=\"all=-N -l\" -v -o ./tmp/main ."
   delay = 1000
   exclude_dir = ["assets", "tmp", "vendor", "testdata"]
   exclude_file = []
   exclude_regex = ["_test.go"]
   exclude_unchanged = false
   follow_symlink = false
-  full_bin = ""
+  full_bin = "dlv --listen=:40000 --headless=true --api-version=2 --accept-multiclient exec ./tmp/main"
   include_dir = []
   include_ext = ["go", "tpl", "tmpl", "html"]
   kill_delay = "0s"

Docker の実行構成を作成

Goland 側から実行できるようにしておく。

リモートデバッグの実行構成を作成

ここでリモートデバッグ用のポートを設定しておく。

今回の場合だと 40000 番。

デバッグ実行を試す

作成した Docker のリモートデバッグの構成を実行し、ブレイクポイントを貼る。

エンドポイントにアクセスするとブレイクポイントで止まってくれる。

curl localhost:18000

ホットリロードを試す

適当に書き換えると、air が自動でリビルドしてアプリケーションを起動し直してくれる。

ただリモートデバッグが一度切れてしまうので再実行する必要がある。

少し面倒だが、 Ctrl + D のショートカットを使えるので許容範囲かな。。(うまい方法あったら教えてほしい)

テストの追加

とりあえず試すだけなので、環境変数設定のテストを書く。

package main  
  
import (  
   "os"  
   "testing")  
  
func Test_getEnv(t *testing.T) {  
   want := "9999"  
   key := "FOO"  
   err := os.Setenv(key, want)  
   if err != nil {  
      t.Fatalf("failed to set env: %v", err)  
   }  
  
   if got := getEnv(key, "8080"); got != want {  
      t.Errorf("getEnv() = %v, want %v", got, want)  
   }  
}

Run Target に Docker を追加

docker compose 経由で実行できるターゲットを追加しておく。

デフォルトターゲットも変更しておく。

テストのデバッグ

テストのデバッグ実行も Docker 経由で実行できることを確認する。

IDE からテストを実行してみる。

テスト実行後のログを見ると、Docker 経由で実行されていることが分かる(go や delve のパスで分かる)。

ブレイクポイントを認識してくれないことがあるが、一度止めずに実行すると認識してくれる。

参考

「プロフェッショナルWebプログラミング Laravel」を読んだ #Laravel

現職だと Web アプリのフレームワークCakePHP を利用してるんだけど、Laravel もう少し知っておきたいな〜、と思って読んでみた。

感想

本は実際に手を動かしながら基本的な機能を触っていく感じで、 Laravel の雰囲気を掴むのにいい感じ。

カバー範囲は広く、 テストや CI 、 Heroku を使ったデプロイまで言及されている。

Laravel はフルスタックフレームワークという感じで、一般的な Web アプリで必要になりそうな機能がかなり簡単に使えるようになってる。

メール送信や Queue を使った非同期処理だったり、スケジューラーだったり。

Docker での開発も sail ですぐ始められる。

テストもユニットテストやフィーチャーテスト、Laravel Dusk でのブラウザテストだったり。

その一方で使い手にとって簡単がゆえに、裏の仕組みは意図的に知ろうとする必要があるのと、そこに踏み込んだときの学習コストはそれなりにかかるかもしれない。

深堀りするというよりは、サクッと全体像を把握したり、取っ掛かりを作っておくのに良い本だなと思いました。

ハマったところ

前提として、手を動かす時は本のバージョン無視してとりあえず最新入れるようにしているので、場合によっては動かなかったりする。

これは自分が悪いんだけど、その上でつまずいたところを備忘録としてメモ。

Laravel Dusk でブラウザテスト

これは調べていた感じ、バージョンと言うよりM1 のせいかな?という雰囲気だったけど、ほどほどで諦めたので定かではない。

フロントエンドの開発

書籍では Laravel Mix を使う前提になっているが、 Laravel Breeze は最新だと Vite になっている。

いくつかエラーを踏んだり動かなくなったりしたけど、原因さえ分かっていると、エラー内容を愚直に対応していけばそれほど苦労はしなかったと思う。

書籍に合わせたかったので、 Mix を使うように修正していくことで解決した。

CakePHP の BelongsToMany でdependent は default が true になっている #CakePHP

アソシエーションの dependant

CakePHP のモデルのアソシエーションには dependant というキーが用意されており、 true に設定することで削除のときに関連付けたモデルのレコードもまとめて削除することができる。

book.cakephp.org

belongsToMany だけデフォルト true

ところが hasOne など他のアソシエーションはデフォルトが false になっているが、 belongsToMany はデフォルトが true になっている。

なので、明示的に設定しなくても関連したモデルのレコードが消されてしまう。

意図しない削除を避けたり他のアソシエーションと一貫性をもたせるたりするためにも、デフォルトは false の方がいいと思って違和感がある。

互換性が理由だった

そこで Cake の実装を見てみると、互換性のために true にしているとコメントがしてあった。

cakephp/BelongsToMany.php at 4.4.6 · cakephp/cakephp · GitHub

過去の PR や Issue を追っかけてみたところ、どうやら関連レコードをどうやっても消してしまうという問題があったようだ。

消さないようにする修正を入れたものの、消すほうが現行の挙動だから、互換性のために default を true にしているというのが経緯らしい。

修正自体はかなり前なので、 CakePHP を長く触っている人なら常識なのかもしれない。

github.com

FakerPHP/Faker で \Faker\Generator::image が使えない #php #laravel

概要

Laravel の Factory で画像のテストデータを作ろうとして $this->faker->image(storage_path('app/public/images')) とかやっても、画像ファイルがうまく生成されず、画像ファイル名も 0 とかで生成されてしまう。

v1.20.0 で非推奨になっており、 Faker2 では削除されるようだ。

カスタムプロバイダーを使うように変更して対応する必要がありそう。

false が返ってきている

tinker を使って試してみると、 false が返ってきている。

ついでに試してみた imageUrl はうまくいっているように見える。

$ sail tinker
Psy Shell v0.11.5 (PHP 8.1.9 — cli) by Justin Hileman
>>> $faker = \Illuminate\Container\Container::getInstance()->make(\Faker\Generator::class);
=> Faker\Generator {#3653}

>>> $faker->image(storage_path('app/public/images'));
=> false

>>> $faker->imageUrl();
=> "https://via.placeholder.com/640x480.png/00aa44?text=quo"

placeholder.com によるブロック?

もう少し追いかけてみると、テスト画像の生成は \Faker\Provider\Imageplaceholder.com を経由して行われているようなのだが、レスポンスステータスが 403 になっていた。

Stackoverflow で placeholder.com が何か変更したか、 IP ブロックをしているみたいな話を見つけた。

"this is popular problem" って言ってるけど、他に類似の情報は見つけられなかった。(単なる自分の調査力不足かもしれない)

stackoverflow.com

Faker\Provider\Image が非推奨になっていた

FakerPHP/Faker は v1.19.0 を利用していたのだが、v1.20.0 で非推奨になっていたようだ。

Marked the Faker\Provider\Image as deprecated

github.com

どうも外部サービスに依存するのってどうなのよ?不安定になっちゃわない?っていう理由らしい。(それっぽい議論をしている Issue や PR をいくつか見つけた)

どう対応するか

Provider is deprecated and will no longer be available in Faker 2. Please use a custom provider instead

非推奨のメッセージによれば、やりたいならカスタムプロバイダーを用意する必要がありそうだ。

生成元を placehold.jp に差し替えるっていうネタも見つけたんだが、さすがにちょっとこれはやんちゃ過ぎるかなと。(一応それっぽくは動いた)

stackoverflow.com

前述の Stackoverflow で紹介されていた smknstd/fakerphp-picsum-images も試してみたんだが、こちらも 403 になってしまうようだった。(imageUrl はこちらも大丈夫)

github.com

人間をリソースと呼ばない方がいいと思う

社内に投下したポエムを一部修正してリポスト。

「リソース」っていう表現が嫌い

表題の通りなんですが、僕は人間をリソースと呼ばないほうがいいと思っていて、何ならとても嫌いな表現なんです。

文脈上使わざるを得ないときもありますが。

よく聞くような使われ方だと、「開発リソースが足りない」とかですね。

言葉狩りか?と言われるとそうかもしれないですが、日常的に使う言葉というのは結構大事だと思っていて、連想される思考になっていく(あるいはもうなっている)と考えているからです。

「代替可能」を連想させる

じゃあどういうことを連想するかというと、リソースという言葉は「代替可能な資源」をイメージしやすいと思うんです。

そうすると、表面上のスキルとかスペックで人間を捉えて、例えば組織編成とか要員計画だとか、なんだかパズルみたいに組み合わせるとうまくいくような気がしちゃうんですよね。

エモい話ってわけじゃない

あぁ、別に人間に寄り添うとかそういうエモい話をしているつもりはなくて。 人間はもっと めんどくさい 複雑で、目に見えない変数がたくさんあると思うよって話です。

たぶん採用のオファー面談で、「リソースが足りないんです!」ていう話をしたら最悪の口説き文句だと思います。
(これに共感できない人は多分価値観が根本から違うと思うんで、そういう人もいるんだな、くらいに受け取ってください)

人が違うと別の組織になる

自分はよくサッカーをメタファーにするんですが、たとえばある選手が移籍したチームはどうなるでしょうか。

同じポジションで、似たようなプレースタイルの選手は獲得するかもしれません。あるいは、若手を抜擢したりするかも。

でも絶対に同じようにはできない。

戦術、フォーメーション、他の選手とのコンビネーションを見直し、更には試合ごとに手直しを加えながら「新しいチーム」を作っていくと思います。

おしまい

だから僕としては、人間に「リソース」っていう表現は使わないで、できるだけ具体的な名前を使って、顔を思い浮かべながら話したほうがいいんじゃないかなって思ってます。

おまけ

似たような観点の記事でおすすめを教えてもらったので置いておきます。

www.benlinders.com