patorashのブログ

方向性はまだない

CircleCIで使う処理をShellScriptにしていっている

CircleCIで時々思わぬところでライブラリ系のキャッシュがされずにエラーになってしまうことがあった。 処理を速くするために複雑なキャッシュをするようになっていたので、それが原因である…。久々に自分で見てみても複雑だなぁ〜と思う。

patorash.hatenablog.com

もっとシンプルにしたいなぁと思っていたら、たまたま以下の記事を見かけた。

qiita.com

これには感銘を受けた。makeはイマイチよくわかっていないので今のところは採用しないが、.circleci/config.ymlで使っているcommand類はShellScriptにすることが可能だ。ShellScriptにすることで、ローカル環境で試しやすくなる。config.ymlに直書きだと、試しづらい。当たり前だけれどなんだか目から鱗だった。

遅くても必ず完走させるようにしたい

キャッシュの不備が原因でテストがコケるのは本当にダサい…。しかも何が原因でライブラリの更新漏れやキャッシュし忘れが発生するのかを探すのが大変になってくると本末転倒…。

それならばもうシンプルに毎度毎度、ちゃんとライブラリの更新処理とキャッシュ処理をするようにした。これでもう予期しないエラーにはならないはず!😀

ただし、このせいで6分くらい遅くなっている…😫

workspaceを渡すのが遅い

CircleCIでは、workspaceを保存して次のジョブに渡すことができる。これをすると、次のジョブで環境構築する手間が省けるので速…そうなのだが、この受け渡し処理が遅い…。dockerイメージを含めているせいで容量も大きいというのはあるのだが、3分以上かかる。受け取るのも1分近くかかる。

1コンテナで環境構築を行い、テストを流す際は4コンテナにそれを渡して処理させているので、料金的には多少安いはずなのだが、時間的には直列になるので頂けない。

コマンドのShellScript化

これはローカルで実験がしやすくなったのでよかった。

エラーを見逃してしまう

ただ、CircleCIのconfig.ymlではステップ毎に定義していたものを1つのShellScriptにまとめたので、エラーの検知がうまくいっていなかった。エラーが起きているのに次の処理に行ってしまい、最後の処理が成功しているので終了コードが0になり、CircleCI上で成功したことになってしまっていた。

エラー対策

ShellScriptでもtry-catchみたいなことができれば…と思い、調べてみたら、trap関数を使えばできることがわかった。

zuqqhi2.com

手元にあるシェルスクリプトコマンドブックにも書いてあった。とにかく、これを使えばtry-catch-finallyができる。 エラーが起きようが起きまいが、docker-compose downはさせたかったので、finally的にやるようにした。

docker_compose_down() {
  docker-compose down
}

catch_error() {
  printf "エラーが発生しました"
  exit 1
}

trap docker_compose_down EXIT
trap catch_error ERR

上記の定義してあるファイル(common.sh)を、全てのShellScriptで読み込むように修正した。

#!/bin/bash

SCRIPT_DIR=$(cd $(dirname $0); pwd)
. $SCRIPT_DIR/common.sh

一旦これで様子見する。

高速化するには?

スタディストさんの開発ブログにCircleCIの高速化に効きそうな記事があったので、これらについて今後試していきたい。

medium.com

medium.com

それにしてもやはりdocker-composeを使ってテストをするのは速くなりにくいようだ…。 machineだと、resource_class: smallが選択不可能なので、smallにして並列数を2倍に増やす作戦も失敗に終わったし、打ち手に限りがある。

1台でキャッシュ用ファイルを作る戦略だったが、そろそろ見直したほうがいいかもなぁ…と考えている。 まぁdocker-composeを使っているせいなので(キャッシュしたいファイルがdocker volumeにあるから引っ張り出すのが遅い)もうちょい、いい方法がないか考える。まぁ速度よりも安定性をまずは重視する。

tmuxに慣れようとしている最中

便利なツールにも慣れていきたいなと思って、最近まで放置していたものにも挑戦していこうと思い、tmuxに再チャレンジしている。再チャレンジというのは、昔に使おうとして面倒になって断念していたからだ。

tmuxはターミナルの多重接続ソフトウェアである。詳しくはWikipediaを。

tmux - Wikipedia

いいところは、接続を終了せずにデタッチという形でセッションを残しておけるところ。普通のターミナルだと、終了したらセッションも終了するので、続きから作業ができないが、tmuxなら会社のPCでセッションをデタッチしておいて、家に帰ってから家のPCでセッションをアタッチして続きを作業する、というような使い方できる(そんな使い方はまだしたことはないが)

あとは、複数ウィンドウを立ち上げたり、ペインを分けて1画面で複数使えるようにしたりできるのが便利。。。なのだが、それはiTermやWindows Terminalでもできたから、そのために慣れきれなくて断念してきていた。

しかし、SSHした先で複数ウィンドウを使ったりペインを分けたりできるのは便利だろうし、なによりデタッチ・アタッチを使いたい機会がいつかくるだろうという想いもあった。

私は習うより慣れろ派ではなくてひとまず知識をザックリ入れてから慣れたい派なので、tmuxの記事を探そうとした。そうしたら、Kindle Unlimitedで読めるtmuxの本があったので、それを読んだ。

ターミナルマルチプレクサ tmux 入門

ターミナルマルチプレクサ tmux 入門

知りたいことも一通り書いてあったし、ザックリと知識が入ってきたことで若干苦手意識が薄れた。まずウィンドウに慣れようと思って、ここ数日はiTermでタブを使わずにtmuxでウィンドウを作って作業していた。 本を読んで、ペインの使い方もなんとなくわかったので、とりあえず素直に分割して使うところまではやれている。幅の調整とかはまだ。 慣れないのが、コピーモード。まだ全然慣れないのだけれど、慣れたら便利そうなので、少しずつやっていく。

本はざっくりと知れるのに便利なのだが、なんだかんだでチートシートも欲しかったのでqiitaにあるものを参照させてもらった。

qiita.com

こちらにあるものから、.tmux.confを作って、マウス操作である程度できるようにした。

そして今はWindowsでブログを書いているのだが、tmuxの操作の検証のためにWindows Terminalを立ち上げているが、やはりCtrlキーはCapsLockキーの位置にあってほしいなと強く感じている…。あまりキーバインドは弄っていないのだが、いじったほうがいいかなぁと感じ始めている。でもどのキーがどれになるかわからなくなるので、キーボードのキーの上に上書きできるシールが欲しい…。

【追記】

Microsoft製のCtrl2CapというCtrlキーとCapsLockキーを入れ替えるソフトの存在を知ったのでインストールしといた。便利。

docs.microsoft.com

でもCtrlキーにCapsLockキーが割り当てられてないような…😅

ansibleでSSH接続時にToo many authentication failuresが出た場合の対処法

小ネタです。表題の通りですけど、エラーが出ました。

ちょうど他のサーバにSSHするときにも同じエラーが出ていました。

kou-i.hatenadiary.org

通常のSSHでは、.ssh/configの中でIdentitiedOnly yesを設定すれば、問題なく接続できるようになりました。

そして、ansible-playbookを実行しようとしたら、同じようなエラーが…。

TASK [Gathering Facts] ***********************************************************************************************************************************************************************************************************************
fatal: [vagrant]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: Received disconnect from 127.0.0.1 port 2222:2: Too many authentication failures\r\nDisconnected from 127.0.0.1 port 2222", "
unreachable": true}

PLAY RECAP ***********************************************************************************************************************************************************************************************************************************
vagrant       : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0

インベントリではどうやってIdentitiedOnlyを指定するんだ?エラーメッセージでググったら、即出てきました。

Ansible SSH too many authentication failures · GitHub

ansible_ssh_extra_args="-o IdentitiesOnly=yes"を追加します。

[app]
vagrant ansible_host=127.0.0.1 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=/Users/*******/sources/github.com/********/ansible-project/.vagrant/machines/default/virtualbox/private_key ansible_ssh_extra_args="-o IdentitiesOnly=yes"

これで問題なくansible-playbookを実行できました👏

ansibleでheroku cli経由でherokuに自動ログインする方法

herokuへのメンテナンスを行うための中継サーバの構成をansibleで作っている最中です。

やりたいことはほぼできたのですが、1つだけ課題が残っていました。それは、ansibleでherokuに自動ログインできないことです。今回、それをなんとか自動ログインさせることに成功したので、メモとして残しておきます。

expectモジュールだとNG

herokuへのログインはheroku cliを使えばCUIでできるので、ansibleのexpectモジュールを使ってログインしようとしたのですが、なぜかうまくいきませんでした。 どうも、herokuのパスワードを入力後の改行コードが何故かパスワードの一部として解釈されてしまい、タイムアウトしてしまいました。

NGだったやつを残しておきます。

---
- name: heroku-cliのインストール
  command: sudo sh -c "export PATH=/usr/local/bin:$PATH && curl https://cli-assets.heroku.com/install.sh | sh"
  args:
    creates: /usr/local/bin/heroku

- name: pexpectのインストール
  pip:
    name: pexpect
    executable: pip3.8

- name: Heroku Loginする
  become_user: "{{ user }}"
  expect:
    command: heroku auth:login --interactive
    responses:
      "Email: ": "{{ heroku_email }}"
      "Password: ": "{{ heroku_token }}"
    echo: yes
  environment:
    PATH: "/usr/local/bin:{{ ansible_env.PATH }}"
  no_log: yes

scriptモジュール経由でexpectを使えばOK

入力の自動化はexpectでできるはずなので、もっと具体的に書き込めばいいかなと思い、expectコマンドをインストールして直接使うようにしてみました。

roleのファイルはこれ。 変数でheroku_emailheroku_tokenを渡しています。(2要素認証にしているためパスワードの代わりにトークンを使用) ローカルにscriptがあればいいので、scriptモジュールで指定。argsのexecutableで実行される命令としてexpectを指定しています(デフォルトだとshになる)

---
- name: heroku-cliのインストール
  command: sudo sh -c "export PATH=/usr/local/bin:$PATH && curl https://cli-assets.heroku.com/install.sh | sh"
  args:
    creates: /usr/local/bin/heroku

- name: yumでexpectのインストール
  yum:
    name: expect
    state: present

- name: ローカルから、expectスクリプトを実行してheroku loginする
  become_user: "{{ user }}"
  script: heroku_login.exp
  args:
    executable: /usr/bin/expect
  environment:
    PATH: "/usr/local/bin:{{ ansible_env.PATH }}"
    HEROKU_EMAIL: "{{ heroku_email }}"
    HEROKU_TOKEN: "{{ heroku_token }}"
  changed_when: false

expectスクリプトである、heroku_login.expは、これ。肝は、sendでパスワード文字列を送った後に入れているsleep 1です。

spawn heroku auth:login --interactive
sleep 1
expect "Email: "
send "$env(HEROKU_EMAIL)"
send "\r"
sleep 1
expect "Password: "
send "$env(HEROKU_TOKEN)"
sleep 1
send "\r"
sleep 2
interact

heroku cliは、入力されたパスワード文字列を****という感じに隠してくれるのですが、表示が****になる前に改行コード\rを送ってしまうと、パスワードの1部と解釈されるみたいでした。

そのため、パスワード入力後に1秒のsleepを差し込んで、****に変換されるのを確認した後、改行コードを送るようにしたところ、ログインできました👍

isucon10の予選に参加しました。

いい感じにスピードアップコンテスト、略してisuconの第十回目の予選に参加してました。 実は初参加でした。メンバーは私と、上司の@kazuhisa1976の2名です。

isucon.net

もう全然惜しくもないし、点数も800点くらいだったので全然ダメでした。あぁ~、技術力のないエンジニアであることが露呈したんじゃ~😭

とりあえずやっていたことと、反省点を上げていこうと思います。(敢えて、まだ他のブログとか見てません!自分が思いつく反省点を上げるため)

やったこと

いきなり修正するのはまず止めよう、問題点を一旦洗い出そうと話していたので、気になる点をtrelloに書いていきました。

  1. まずはRubyの実装に変更(デフォルトがgolangのため)
  2. サーバの構成の調査(cat /proc/cpuinfoと/proc/meminfo)。全部1コア2GB。
  3. インデックスの状態を調査(primary keyのみindexあり)
  4. コードを読んでインデックスが貼れそうなところに目星を付ける
  5. nginxにリバースプロキシを仕込んでラウンドロビンにする
  6. 3台目のDBに接続させるようにして、1,2のDBを落とす
  7. 3台目のDBのスキーマファイルを修正して検索条件に使われるものにインデックスを貼るよう設定
  8. csvのインサートが1件ずつなのをバルクインサートにする
  9. MySQLのバージョンが5.7だったので、8.0にする。その際に、2台目で作業。
  10. MySQL8.0のサーバに接続するように書き換え。
  11. mysqld.cnfを雑に書き換えたら、めちゃくちゃスコアが下がったのでデフォルトに戻す。
  12. なぞっての処理でN+1が発生しているので、位置に入っているか直接見られるようにするためにestateテーブルにgeometry型のカラムを追加
  13. カラムを追加したせいでレスポンスが変わってしまい、select *できなくなり(大失態)クエリのselectを書き換えていく
  14. 2台目をDB専用にして、mysqld.cnfを修正して調整…。
  15. unicornのワーカー数を調整

これくらいだろうか…。600点台からなかなか抜け出せなくて、途中で700点台でまた止まって、ぎりぎり800点台に到達して終わった感じだったけれど、本戦参加者は2500とかいってたから全然あかんかったなぁと思う。

反省点

1. botを弾いてなかった

終わった後で気づいた。Bot弾いてない。Discordの会話を見る限りでは、nginxで弾けそうだった。

2. 直接サーバで作業しすぎた

各サーバのコードの同期をとるのが大変で、毎回そこで時間食ってたと思う。同期をとる仕組みを先に作るべきだった。

3. MySQLに詳しくなさ過ぎた

普段PostgreSQLを使っているので全然わからんかった…。めっちゃググった。

4. MySQL8.0にしたけれどよかったのか?

過去のやつでミドルウェアのバージョンアップで性能が向上するという話を見かけていたのでやったのだが、効果が今一つわからなかった(ベンチマークが不安定過ぎて5.7の状態で取れなかったため)。ベンチマークが取れるようになるまで待つという選択肢もあったが、時間がもったいないからエイヤでやってしまった。8.0からカラムにgeometry型が使えるとあったのでやったのだが、カラムを追加する必要はなかったかも…。

5. 位置情報は関数インデックスでよかったのではないか?

後で思ったこと。カラムを追加して位置情報で検索させることでN+1はなくなったのだが、カラムを追加したせいでレスポンスの形が変わってしまい、エラーになりつづけて結構時間をロスした。追加じゃなくて関数インデックスで位置情報を保持しておけば、5.7のままでもよかったかもしれないし、カラムを追加していないのでjsonのレスポンスの形も変わらず、エラーでハマることはなかっただろう…(多分)

6. viに慣れてなさすぎ

vi滅多に使わないので修正に時間がかかった。まぁ2で言ってることと被るけど。

7. New Relic使えるの知らなかった

isucon.net

使えたんかい!?😖事前情報のチェックをしてなさ過ぎた。始まる前から終わっていた感ある…。 パフォーマンスチューニングなのに運営のベンチマークの値に頼り過ぎていた。何をやればいいかが手探りすぎてダメだった。New Relic見れていたらだいぶ違っただろうなぁ…。

8. アプリをブラウザで動かしてなさ過ぎた

もっと自分の手でブラウザから動かして、実際に遅そうなところを見ればよかった。ブラウザで動かさずにコードのほうばかりに注目しすぎた。chromeのdevtoolを見ても遅いところはわかるし、ちゃんとアプリに向き合えてなかったと思う。

これから

もうほんまにダメダメだったので、他の人のブログを読みながら試していけそうなところとかを勉強させてもらおうと思います。あといつもPaaSのHerokuを使っているからミドルウェアの設定に弱すぎて個人的にダメだった。やはり何か自分でウェブアプリを運用してみるしかない。最近やってないからな…。Conohaでなんか作るかなぁ~。

スクラムで開発しようとしている

弊社の他のプロジェクトでスクラムを導入しているので、今期から、うちのプロジェクトでもスクラムやってみるか~と思って勉強中です。勉強中というかチケットの管理方法とかを本を読みながら模索してルールを作ったりしました。

元々ある課題

結論からいうと、開発チームがなかった。正確には、1人だからチームじゃなかった。

私が担当しているプロジェクトは、開発チームではなく、長いことほぼ1人で担当していたため、そもそもスクラムをやるというモチベーションがありませんでした。途中で開発に参加してくれる人もいたのですが、元々違うプロジェクトを任されていて兼任になるため、私的にもなんか気を遣ってしまっていました。兼務なので、そちらが忙しくなるとフェードアウトしていってそのまま…という感じ。で、ようやく昨年新卒の後輩さんが私の下についたので、徐々に教えている状態。まだ本格的にプロダクト開発には入ってないので、これから。

まぁ他にも色々と課題はあるのですが、それは一旦置いといて(書けない😛)、とりあえず2人体制になれたので、メイン開発を後輩さん、スクラムマスター兼開発サポート・裏方を私がやろうかなというところです。これがいい体制なのかどうかといえばよくはないのだが人数が足りないんだ!(スクラムの開発チームは3~9人とか…)

やったこと

本を読んだ

とりあえず、スクラムに関する知識が齧ってる程度だったので、指針になるものを仕入れようと思って本を読みました。参考にした本は、SCRUM BOOT CAMP THE BOOKです。

非常に面白くて読みやすいし、こういう時どうするの?っていうのが載っててよかったです。これを読んで、うちのプロダクトは月末リリースなので、スプリント期間を3週間、リリーススプリントを1週間にしようという考えになれました。また何度も読み返すことになりそう。

チケットを整理した

元々、顧客からの要望やアンケートの結果、運用の課題などについてはRedMineのチケットにして管理していたのですが、開発上の課題はそこには書かず、GitHubのissueで管理していました。まぁそれは当初は、開発上の課題は見せなくてもええやろという感じだったのですが、それだと要望と開発の課題のどちらが重要かという判断が難しいし(両方見えているのは私だけになるので私の判断になる)、全然RedMineのチケットの消化が進んでいないときは、「開発(というか私)は一体何やってるの?🤔」っていうのが関係者に見えないのが辛いなと思ってました。いや、前期はRails6にアップグレードしたり、webpackerに移行したり、やってるんやで…😭

スクラムでスプリントを回そうと思ったら、ベロシティの計測しなければ開発の予測が立てられませんし、開発の課題も関係者に認識してもらうためにRedMineのチケットとして登録しました。GitHubからはissueからRedMineのチケットにリンクを貼るようにしました。その際に、「RedMineのチケットへのリンクが面倒やなぁ~」と悩んでいたら、同僚がGitHubのAutolink referenceの記事を書いてたのでそれを参考にさせてもらいました!🙏

ryosms.livedoor.blog

これを設定すると、例えばGitHubのissueでTICKET-12345と書くと、自動的にRedMineのチケット番号12345にリンクが貼られるようにできます。圧倒的に楽!見た目も綺麗!👍

チケットのルールを定義した

スクラムだと、課題はプロダクトバックログと呼び、プロダクトバックログに対するタスクはスプリントバックログと呼びます。

最初はRedMineのチケットの呼び方を「チケット」と変えずに、関係者には軟着陸しようかと考えていましたが、多分ごちゃごちゃしてくるなと思ったので、ルールをRedMineWikiに書きました。その時に、チケットはプロダクトバックログとスプリントバックログの2種類になりますが、皆さんは今までのチケットの運用と同様にプロダクトバックログのみを意識してもらえばいいです、開発者が何しているかはスプリントバックログを見ればわかります、と言えるようにしました。

要は、「言葉の定義や呼び方は変わるけれど、皆さんのチケット運用方法はほぼ変わりませんよ~😀」ということで、大きな変化ではないと思ってもらえるようにしました。まぁ受け手がどう感じたかはわかりませんが…。

あとは、開発側はTiDDチケット駆動開発)にしたいと思っていたので、スプリントバックログには必ず親チケットとしてプロダクトバックログのIDを付けるように!というルールにしました。これで、どのプロダクトバックログを実現するために何をやっているのかが明白になるはず。また、スプリントバックログに関しては、ステータスを簡潔にするために、

  1. 新規
  2. 対応中
  3. 終了

の3つだけにしました。

チケットのテンプレートを作った

今まではチケットのフォーマットが特に決まっていなかったので(こう書いてほしいという指針はあった)、書く人によっては情報が足りなかったり、以前に却下された要望が何度もゾンビのように復活してきたりということがありました。そこで、テンプレートを作りました。

テンプレートでは、ユーザーストーリー形式で書いてほしいことを伝え、フォーマットとサンプルを付けているのと、もう対応しないことになっている件をリスト表示しておきました。

後輩とプランニングポーカーした

した、と過去形にしていますが、正確には、している最中です。基本的に作業をしてもらうのは後輩さんだけれど、まだ実際に開発していないので、プロダクトバックログの内容を私が製品の画面のここの修正のこと、と説明して、それからプランニングポーカーでお互いに投票して、数値が違えば意見を聞いたりして、基本的には後輩さんの主張したストーリーポイントを設定していく、という感じ。

今は在宅勤務することが多いので、プランニングポーカーもオンラインでやっています。これもまた同僚が教えてくれたやつですが、hatjitsuというプランニングポーカーをするためのサービスがあって便利😁

hatjitsu.toolforge.org

これからどうするか?

現時点でも、後輩さんにメインをやってもらうと決めたので、開発の課題なのに後回しになっていたところ(ミドルウェアのアップデートとか、リファクタリングとか)に切り込んでいきやすくなったので、現時点でもちょっと効果は実感できてます。 とはいえ、色々あってまだ準備段階。まだちゃんとしたスプリントには入れてません…。とりあえずスプリントが開始できたら、関係者からフィードバックをもらって、修正していきたいと考えてます。

aws-sdk-s3を使いつつ、timecopで時間をずらす

ストレージを伴うテストのためにminioを使うようにしようとしたのですが、設定をしただけではうまく動きませんでした。

timecopを使っているところで、aws-sdkがエラーを起こしました。timecopは時間を固定したり過去・未来に移動したりするライブラリです。

https://github.com/travisjeffery/timecop

エラーメッセージは以下のようなものでした。

Aws::S3::Errors::RequestTimeTooSkewed: The difference between the request time and the server's time is too large.

このメッセージでググるとわかるのが、アプリケーションサーバとS3のサーバ(minio)の間で時刻が離れすぎているから起きる例外でした。timecopで3年前とかに移動しているため、当然起きました…😩

解決方法

解決方法は、aws-sdk-s3から時刻の確認をするときだけ、Time.nowが現在の時刻を返せばいいというアプローチでした。

参考にしたのは以下のページです。

qiita.com

このページの、例外に備えよう、という見出しのところに書いてあります。

まんまでは動かない

しかし、記事自体が3年前のものなためか、aws-sdkのバージョンが変わり、まんまでは動かなくなっていました。

# aws-sdk-s3では動かないので注意
class Time
  class << self
    def now_wrap
      if (caller || []).first.match('aws-sdk')
        now_without_mock_time # return real time
      else
        mock_time || now_without_mock_time # return mock time or real time
      end
    end
    alias_method :now, :now_wrap
  end
end

修正

置き換えなければならない箇所の呼び出し元のパス情報が、aws-sdkではなく、aws-sigを含むものに変わっていたので、修正しました。

class Time
  class << self
    def now_wrap
      # p caller.first if caller.first.match('aws') # awsという文字列でなんとなくあたりを付けた
      # すると、aws-sigというものを発見
      if (caller || []).first.match('aws-sig') # aws-sdkをaws-sigに変更
        now_without_mock_time # return real time
      else
        mock_time || now_without_mock_time # return mock time or real time
      end
    end
    alias_method :now, :now_wrap
  end
end

修正後、テストを実行したところ、timecopを使っている箇所でもテストが成功しました👍

他に試したこと

最初は、エラーメッセージから、アプリケーションサーバとS3サーバの時刻の比較をしないで済むオプションがあるのではないか?と考え、S3クライアントにそれっぽいものを設定してみたのですが、結局ダメでした。

s3_client = Aws::S3::Client.new(
    access_key_id: Rails.application.credentials.aws_access_key_id,
    secret_access_key: Rails.application.credentials.aws_secret_access_key,
    region: Rails.application.credentials.aws_region,
    correct_clock_skew: false, # default true
    retry_mode: 'standard', # default 'legacy'
)