なにもわからない

気分で技術系の雑記を書きます

Docker for Mac でも快適な Symfony 開発環境を作りたい

f:id:tamakiii:20191124095441j:plain

Symfony Advent Calendar 2019 3日目の記事です。

昨日は @polidog さんの 「JsonSchemaBundleを作った話」 でした。


Docker が開発環境のお供として定着したおかげでチーム内の環境差はだいぶ小さくなりました。 しかし、Docker for Mac は APFS との相性が悪く、特に Symfony プロジェクトでの DX の悪さは悩みの種でした。

この問題の解決方法はいくつかありますが、個人的に NFS を使った方法が手間と効果のバランス上よいと思っています。 Docker 標準機能の consistency delegatedcached もそれなりには効きますが、開発中の DX 的にはまだ不満が残ります。 docker-sync も試してみましたが動作が安定せず解決策とはなり得ませんでした。

(他にも実は VMWare 上の Ubuntu で Docker を動かした方が consistent よりは早かったりします)

既に世に情報はだいぶ出回ってはいますが、今回は特に macOS Catalina 上で快適に動く Symfony 開発環境を docker-compose と NFS で作る方法について、私的なおすすめも交えて書きます。

長いので結論を先に書くと、ウェブのレスポンスが 2833 ms → 370ms に、bin/console の実行が最大300% ほど高速になりました。

サンプルプロジェクト

github.com

続きを読む

macOSX 向け dotfiles を Github Actions でテストする

macOS 環境が無料で CI が回せる時代になりましたね。16インチ MacBook Pro を買ったこともあって GitHub Actions で macOS 向け dotfiles をテスト&見直ししてみました。 案の定動かない処理もあったりして、やはりテストを定常的に回すのは効くなと思い知らされましたが、それにもいくつかハマりどころがあったので書いておこうと思います。

環境構築スクリプトを育てていると、どうしても今のPCの環境には適用できるけれど実は新規PCには適用できなくなっているということは起きてしまいます。まっさらな状態からセットアップすることはめったにありませんからね。具体的にはディレクトリのないところにsymlinkを貼るとか、セットアップの前の方で入れているツールを後の方で使っているのだけどPATHが通っていないとか、そういうケースです。

そこでCIを回しましょうという話です。最近、GitHub Actionsのmacos-latestでCIを回すようになりました。実際にCIを回したら、必要なディレクトリを作るのを忘れていたり、諸々通らないことが発覚しました。

itchyny.hatenablog.com

現状の設定ファイルを先に貼っておくと、こうなりました。

リポジトリと作業の様子はこちらです。

1. GitHub 上で YAML ファイルを initialize する

f:id:tamakiii:20191121211312p:plain

画面上から YAML ファイルが作れて PR も作れます。とりあえず PR を作ればテンションが上がるのでとりあえず作るといいと思います。 自分は Set up a workflow yourself から始めました。

help.github.com

2. デフォルトで HomeBrew やいくつかのパッケージがインストールされている

これは dotfiles をテストする目的ではあまり嬉しくないことですが、デフォルトで HomeBrew や git, node などがインストールされていました。 ちょっと困ったのが、Brewfile で brew node をインストールさせたい場合、brew node@12brew @node13 が衝突してコケてしまいました。 これはメインのスクリプトを動かす前にデフォルトでインストールされている node をアンインストールすることで回避しました。(brew uninstall node@12

==> Downloading https://homebrew.bintray.com/bottles/node-13.1.0.catalina.bottle.tar.gz
==> Downloading from https://akamai.bintray.com/58/5863d08b039c44d35a8343179cf93d495e1288efe2c669ccb77704a236e0e17b?__gda__=exp=1574272109~hmac=9967f775a1b5093bc37183b749e04921790fe0894fdec9d213efacd13b9c6ac8&response-content-disposition=attachment%3Bfilename%3D%22node-13.1.0.catalina.bottle.tar.gz%22&response-content-type=application%2Fgzip&requestInfo=U2FsdGVkX1-HgyW49y3C7roMUH2e6Ay__vYCfMAjNkaXbnNPwYSBlYuywDo6KSDP77-xzhgl2poWSbWyOqlhQXdPRm-Heaa85s3o5DAWzDspCV12Yu3lCCUO08p1ocfNZMtoImL1AsiYZ7xjt1rbgQ&response-X-Checksum-Sha1=7352019f912696f10f9cf7feaaad9c0e2666894f&response-X-Checksum-Sha2=5863d08b039c44d35a8343179cf93d495e1288efe2c669ccb77704a236e0e17b
==> Pouring node-13.1.0.catalina.bottle.tar.gz
Error: The `brew link` step did not complete successfully
The formula built, but is not symlinked into /usr/local
Could not symlink bin/node
Target /usr/local/bin/node
is a symlink belonging to node@12. You can unlink it:
  brew unlink node@12

To force the link and overwrite all conflicting files:
  brew link --overwrite node

To list all files that would be deleted:
  brew link --overwrite --dry-run node

Possible conflicting files are:
/usr/local/bin/node -> /usr/local/Cellar/node@12/12.13.0/bin/node

3. キャッシュの有効サイズは 400MB

ライフサイクルはこのようになっているので、Workflow が成功するまではキャッシュが有効になりません。

  • ロックファイルからキャッシュキーを生成する(今回は ${{ hashFiles('Brewfile') }}
  • キャッシュキーからキャッシュの有無を探す
  • Workflow が成功したらポストプロセスが動いてキャッシュを圧縮して保存する

またキャッシュされる最大サイズは 400MB で、これを超えるとポストプロセスでキャッシュが保存されなくなります。

/usr/bin/tar -cz -f /Users/runner/runners/2.160.2/work/_temp/430ca3b6-c547-4cda-ae16-a5e6e998894c/cache.tgz -C /usr/local/Cellar .
##[warning]Cache size of ~722 MB (757155145 B) is over the 400MB limit, not saving cache.

HomeBrew の場合、キャッシュ対象はざっと見た感じ2つあり、ひとつが $(brew --cache)/downloads = ~/Library/Caches/Homebrew/downloads、もうひとつが /usr/local/Cellar です。 前者は install プロセスはスキップできませんが、download プロセスがスキップできます。ただし、40行ほどの内容でも 900MB 近くなってしまったため、post プロセスで保存される前にファイルサイズの大きいものから10件ほど削除することでキャッシュを効かせました。

gist.github.com

後者の /usr/local/Cellar なら 400MB を下回ってくれるのでは、と思いましたがこれも 700MB 超えでそのままではキャッシュに乗りませんでした。 キャッシュを効かせても Workflow ごとに5分ほどの実行時間を要しているので、あまりキャッシュは有効に使えなそうな雰囲気があります。

4. brew install が Host key verification failed. で死ぬ

main.yml から make -f brew.mk を叩き、brew.mk 内で brew install している場合は問題なく動くのですが、main.ymlmake -f brew.mk の間にひとつシェルスクリプトMakefile を挟むだけで Host key verification failed. fatal: Could not read from remote repository. で死ぬようになりました。 具体的なドキュメントは出てきませんでしたが、おそらくセキュリティ上の都合だと思われます。 解決策はなかったので諦めて間に何も挟まない形にしました。ci.mk と実機で叩くセットアップスクリプトを近い形にしておきたかったんですが、まぁ致し方ないです。

github.com

Selenium IDE は for each したりできるよ

ウェブブラウザのオートメーションツール Selenium には、Google Chrome / Mozilla Firefoxプラグインとして動く、最近開発が活発な Selenium IDE というものがあります。

その Selenium IDE には conditional logic and looping を実現する Control Flow と呼ばれる機能があります。

Selenium IDE comes with commands that enable you to add conditional logic and looping to your tests. This enables you to execute commands (or a set of commands) only when certain conditions in your application are met, or execute command(s) repeatedly based on pre-defined criteria.

docs.seleniumhq.org

何かと言うと if とか for each とか書けるよって話なんですが、まぁ当然 JavaScript の実行結果をイテレートできます。

JavaScript Expressions Conditions in your application are checked by using JavaScript expressions. You can use the execute script or execute async script commands to run a snippet of JavaScript at any point during your test and store the result in a variable. These variables can be used in a control flow command. You can also use JavaScript expressions directly in the control flow commands.

コツは execute script の、 Target には return 式を、Value には格納先変数を書き、変数を ${} で展開すること、くらいでしょうか。

f:id:tamakiii:20191121002629p:plain
Selenium IDE なるほどですね

もっとリッチなエディタがあれば RPA できそうですが(=Microsoft Power Automate に期待してます)、これだけでも人間が学習すればある程度使えそうです。実行環境を用意するのだけ面倒くさそう。

jq で特定条件にマッチする要素を置換する

こういった JSON.name == "Michel".attributes だけを置換して全体を出力したい。

/tmp
$ cat test.json
{
  "elements": [
    {
      "name": "John",
      "attributes": {
        "private": true,
        "last_login": "2019-11-01 00:00:00"
      }
    },
    {
      "name": "Michel",
      "attributes": {
        "private": false,
        "last_login": "2019-11-02 00:00:00"
      }
    },
    {
      "name": "Paul",
      "attributes": {
        "private": false,
        "last_login": "2019-11-31 00:00:00"
      }
    }
  ]
}

map(), select(), |=, //, . を駆使して実現できる。すごく雑に書くと

  • map() => .elements[] のような配列を map() する
  • select() => 条件に一致するものだけフィルタする
  • // => a // ba ? a : b と評価される演算子
  • . => イテレートしている要素

キモは //

A filter of the form a // b produces the same results as a, if a produces results other than false and null. Otherwise, a // b produces the same results as b.

This is useful for providing defaults: .foo // 1 will evaluate to 1 if there’s no .foo element in the input. It’s similar to how or is sometimes used in Python (jq’s or operator is reserved for strictly Boolean operations).

stedolan.github.io


これらは以下の様に組み合わせて使う。

# 条件にマッチしない場合 `.` (=`.attributes`) に置換する(≒何もしない)
/tmp
$ cat test.json | jq '.elements |= map((select(0)) // .)'
{
  "elements": [
    {
      "name": "John",
      "attributes": {
        "private": true,
        "last_login": "2019-11-01 00:00:00"
      }
    },
    {
      "name": "Michel",
      "attributes": {
        "private": false,
        "last_login": "2019-11-02 00:00:00"
      }
    },
    {
      "name": "Paul",
      "attributes": {
        "private": false,
        "last_login": "2019-11-31 00:00:00"
      }
    }
  ]
}

# 条件にマッチする場合 `elements[].attributes` を `[]` に置換する
/tmp
$ cat test.json | jq '.elements |= map((select(1).attributes |= []) // .)'
{
  "elements": [
    {
      "name": "John",
      "attributes": []
    },
    {
      "name": "Michel",
      "attributes": []
    },
    {
      "name": "Paul",
      "attributes": []
    }
  ]
}

そして特定条件時のみ .attributes を置換する

/tmp
$ cat test.json | jq '.elements |= map((select(.name == "Michel").attributes |= { private: true, last_login: "2222-22-22 22:22:22"}) // .)'
{
  "elements": [
    {
      "name": "John",
      "attributes": {
        "private": true,
        "last_login": "2019-11-01 00:00:00"
      }
    },
    {
      "name": "Michel",
      "attributes": {
        "private": true,
        "last_login": "2222-22-22 22:22:22"
      }
    },
    {
      "name": "Paul",
      "attributes": {
        "private": false,
        "last_login": "2019-11-31 00:00:00"
      }
    }
  ]
}

202X年もシェル芸は身を助けてくれそうです

man したい alpine ワンライナー

macOSLinux を跨って使うような Shell Script や Makefile を書く際に、オプションなど挙動の差をどう解決するか考えることがよくある。 そんなときにサクっと alpine を Docker で立ち上げて man を見るのに使えるワンライナーのメモです。

docker run --rm -it alpine ash -c 'apk add --update --no-cache man man-pages coreutils-doc && ash'

PAGER=less の方が慣れてるのであれば

docker run --rm -it -e PAGER=less alpine ash -c 'apk add --update --no-cache man man-pages coreutils-doc && ash'

alpine も他 Linuix ディストリビューションとちょいちょい動き違ったりするんですが、最近 alpine びいきなので(まぁ)。

linuxcommand.sourceforge.net

wiki.alpinelinux.org

linuxjm.osdn.jp

pkgs.alpinelinux.org

MySQL on Docker で MySQL init process failed.

$ docker-compose up
...
mysql_1  | MySQL init process in progress...
mysql_1  | MySQL init process in progress...
mysql_1  | MySQL init process in progress...
mysql_1  | MySQL init process in progress...
mysql_1  | MySQL init process in progress...
mysql_1  | MySQL init process failed.

以前、環境変数 MYSQL_HOST が悪さをするバグがあったらしいけど既に解消済みらしい。

開発時にいちいちホストOS からパスワードを入力してログインするのが面倒で /etc/mysql/conf.d/mylogin.cnf にこんな設定を書いていた

[client]
password="pass"

MySQL コンテナの初期化時に [client] が書かれていると init process filad. で落ちるのは既知の問題らしい(まぁそりゃそうだ)

パスワードを簡単にするのでもいいけど、~/.my.cnf は初期化プロセスに関係しないらしいので置き場所を変えてやればいい。root なのはご愛嬌

$ git diff docker-compose.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index 1d974da..b09d973 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -32,7 +32,7 @@ services:
       TZ: Asia/Tokyo
     volumes:
       - .:/project:delegated
-      - ./docker/mysql/conf.d/mylogin.cnf:/etc/mysql/conf.d/mylogin.cnf
+      - ./docker/mysql/my.cnf:/root/.my.cnf
       - ./docker/mysql/initdb.d:/docker-entrypoint-initdb.d
       - mysql:/var/lib/mysql
     security_opt:

データボリュームの初期化はこう

docker-compose down --volumes mysql

init process するスクリプトの本体はこの辺 github.com

他にもタイムアウト伸ばしたら?とか、メモリ足りないんじゃない?とかいう話もある

Amazon Linux 2 on Amazon Workspaces

手順は classmethod の記事を見つつポチポチと。 メールが届くまで20分ほどかかるのと、パスワードポリシーに合っていないと気づきにくい点だけアレ dev.classmethod.jp

動いたの図 f:id:tamakiii:20180627224015p:plain

Google Chrome のインストールは yumintoli.com

まだ全然手に馴染んでないけどキーボードの設定 f:id:tamakiii:20180627224004p:plain f:id:tamakiii:20180627224023p:plain f:id:tamakiii:20180627224201p:plain