ベースブランチにマージされていないGitHub Actionを動作検証する
環境
gh --version gh version 2.25.1 (2023-03-21) https://github.com/cli/cli/releases/tag/v2.25.1
手順
1.action作成
.github/workflows/foo.yaml
name: Foo on: pull_request: # pull_requestにすることでworkflow listに追加される jobs: foo: runs-on: ubuntu-latest steps: - name: foo run: | echo "foo"
2.リモートブランチにプッシュ
git push origin foo
3.gh workflow listで確認
gh workflow list ... Foo active xxx
4.gh workflow runで実行
gh workflow run foo.yaml --ref foo
参考
zxでTerraformのファイルにlifecycle prevent_destroyを一括で付与する
概要
Terraformの任意のリソースにprevent_destory = trueを付与したい。
zxでスクリプトを書いたので記録。
- 挙動
lifecycleブロックがなければ追記してprevent_destory = trueをセット。
lifecycleブロックがある場合は、falseのものもtrueにする。
環境
zx --version 7.1.1
スクリプト
#!/usr/bin/env zx // eg. zx scripts/addPreventDestroy.mjs --dir path/to/resource --resource aws_s3_bucket const { resource, dir } = argv; $.verbose = true async function hasLifeCycle(file, block) { const res = await $`cat ${file} | hcledit block get ${block}.lifecycle` if (res.stdout === "") return false return true } async function hasPreventDestroy(file, block) { const res = await $`cat ${file} | hcledit attribute get ${block}.lifecycle.prevent_destroy` if (res.stdout === "") return false return true } async function appendLifeCycleWithPreventDestory(file, block) { await $`cat ${file} | hcledit block -f ${file} -u append ${block} lifecycle --newline` await $`cat ${file} | hcledit attribute -f ${file} -u append ${block}.lifecycle.prevent_destroy true` } async function setPreventDestroy(file, block) { await $`cat ${file} | hcledit attribute -f ${file} -u set ${block}.lifecycle.prevent_destroy 'true'` } async function appendPreventDestroy(file, block) { await $`cat ${file} | hcledit attribute -f ${file} -u append ${block}.lifecycle.prevent_destroy 'true'` } function isCommentOut(blocks) { return (blocks.length === 1 && blocks[0] === "") } async function listFliles() { const files = (await $`grep -l -r --exclude-dir .terraform "resource \\"${resource}\\"" ${dir} || true `).stdout.trim().split('\n') return files.filter((v) => v) } async function addPreventDestroy(resource) { const files = listFliles() for await (const file of files) { const res = (await $`cat ${file} | hcledit block list | grep "resource.${resource}\\." || true `) const blocks = res.stdout.trim().split('\n') if (isCommentOut(blocks)) continue for await (const block of blocks) { const ifHasLifleCycle = await hasLifeCycle(file, block) if (ifHasLifleCycle) { const ifHasPreventDestroy = await hasPreventDestroy(file, block) if (ifHasPreventDestroy) { await setPreventDestroy(file, block) } else { await appendPreventDestroy(file, block) } } else { await appendLifeCycleWithPreventDestory(file, block) } } } } await addPreventDestroy(resource)
実行方法
zx scripts/addPreventDestroy.mjs --dir path/to/resource --resource aws_s3_bucket
これでpath/to/resource配下のすべてのaws_s3_bucketにprevent_destory=trueを付与する。
参考
リリース作業を委譲するためにやったこと
概要
現在SREとして働いているが、複数あるアプリケーションのリリース作業を依頼されることが多かった。
依頼者の職種はエンジニアやマネージャなど。
そのリリース依頼のほとんどを委譲したので、やったことを記録する。
課題
手作業
- 作業ミス
- 時間がかかる
- sshログインなどの権限管理
作業依頼
- コミュニケーションコストが増える
- デプロイしたものに問題があるかどうかわからない
- SREが休むととデプロイできない
- 作業依頼によるコンテキストスイッチ
- デプロイを忘れてしまう
- 誰に依頼するかわからない
- 作業状況がわからない
やったこと
なお過程も含むので現段階で使われてないものもある
自動化
- ローカルでおこなう手作業をCIで自動化
- オペレーションサーバに接続しておこなう手作業をバッチ化
- aws ecs run-taskとして実行
- Slackのワークフローによるテンプレ化
- 依頼のメンション先や作業順を固定化
- 作業状況をオープンに
- 通知
リリースを誰がいつやっているかわかるように
ドキュメンテーション
大体下記のようなことに気をつけて手順書を作成
いい手順書とは - tom-256.log
ほかにも下記のようなことを工夫した。
- 手順書を実際に使ってモブ作業する
- モブ作業の前に自分で手順書を使って動作確認する
説明するときに躓くと相手側が不安な気持ちになり、スムーズに委譲できないので作業時間を割いて実施 - 画像を用意して伝わりやすくする
- リリーステンプレを用意して、作業が終わったらチェックボックスを埋める
コミュニケーション
- モブ作業
- 移行期間を設ける
一度のモブ作業で終わるのではなく、複数回実施して段階的におこなう
- 移行期間を設ける
- ドキュメント
- 責任範囲の説明(定常作業担当と仕組みづくり・監視担当の棲み分け)
- 双方にとってメリットがあるという理由
- アフターフォロー
- 作業自体は委譲するが、改善案や相談があればすぐに伝えてもらう
- 問題があれば前のやり方に戻すことも可能であると伝える
ほかに気をつけること
- 自動化せずに委譲すると事故を引き起こす可能性が上がるので自動化してから委譲する
- 運用を見据えた設計をする
自動化可能か、仕組みとしてスケール可能か、チームとしてスケール可能か - 自動化のコストと実行頻度を考慮する
コスパの悪い自動化をしない
まとめ
やったことをまとめた。
一度委譲しようとして難色を示されたことがあったが、モブ作業などを整備して再びコミュニケーションをとったら委譲することができたので、 十分に準備をしてコミュニケーションする。
最近やっていること2022年夏
概要
直近1年くらいでやっていたことをまとめる
全体の方向性
- 持続可能な開発組織づくり
- 開発生産性の向上
- 各チームが自律的に開発できるように責任と権限を整備
- 開発チームのフィードバックサイクルの向上
- SREチームのトイル削減
具体的にやったこと
- 課題の洗い出し
- 定常作業のドキュメンテーション
- ドキュメント削除会
- トイル削減のための調整
- リリースフローの整備
- ワークフローのSlack化
- DesignDocの提案とそれを用いたコミュニケーション
- TerraformのCI改善
- GitHub ActionsのOIDC連携
- モノレポの設計・実装
- コンテナのビルド時間削減
- MongoDBのスロークエリ改善
- 社内LT
- 社外登壇
- 壁打ち(アプリケーション設計、テスト設計)
まとめ
振り返るタイミングがあったのでまとめた。
粒度があれだけど一旦書き出してみた。
掘り下げるといろいろかけることがありそう。
今後を考えるいいきっかけになった。
too many open filesエラーが出たときにlsofコマンドで確認する
概要
too many open filesエラーが出たときにlsofコマンドで確認するときの手順を説明する機会があったのでまとめる。
環境
node -v v16.16.0 docker -v Docker version 20.10.17, build 100c701
用意するもの
Dockerfile file.txt index.js
Dockerfile
FROM node:16.16.0-bullseye-slim COPY . . RUN apt update && apt install -y lsof procps linux-perf CMD ["node","index.js"]
サンプルプログラム index.js
const fs = require('fs'); const openFileAndDontClose = () => { fs.open('file.txt', 'r', (err, fd) => { console.log(`fd: ${fd}`) console.log(`err: ${err}`) }); } setInterval(openFileAndDontClose, 1000)
1秒ごとにfile.txtを開き続ける。
検証
docker buildx build -t test . docker run --name test test docker exec -it test /bin/bash
nodeプロセスのPIDを確認
ps aux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND root 1 0.0 0.0 4100 3340 pts/0 Ss 13:41 0:00 /bin/bash root 10 0.1 0.4 352596 33644 pts/0 Sl+ 13:41 0:00 node index.js root 21 0.0 0.0 4100 3520 pts/1 Ss 13:41 0:00 /bin/bash root 34 0.0 0.0 6700 3012 pts/1 R+ 13:42 0:00 ps aux
ulimit -Sn 1048576 ulimit -Hn 1048576
ファイルディスクリプタの確認
/proc/PID/fd
ls -l /proc/1/fd total 0 lrwx------ 1 root root 64 Jul 16 14:00 0 -> /dev/null l-wx------ 1 root root 64 Jul 16 14:00 1 -> 'pipe:[56376]' lr-x------ 1 root root 64 Jul 16 14:00 10 -> 'pipe:[55593]' l-wx------ 1 root root 64 Jul 16 14:00 11 -> 'pipe:[55593]' lrwx------ 1 root root 64 Jul 16 14:00 12 -> 'anon_inode:[eventfd]' lrwx------ 1 root root 64 Jul 16 14:00 13 -> 'anon_inode:[eventpoll]' lr-x------ 1 root root 64 Jul 16 14:00 14 -> 'pipe:[50868]' l-wx------ 1 root root 64 Jul 16 14:00 15 -> 'pipe:[50868]' lrwx------ 1 root root 64 Jul 16 14:00 16 -> 'anon_inode:[eventfd]' lr-x------ 1 root root 64 Jul 16 14:00 17 -> /file.txt lr-x------ 1 root root 64 Jul 16 14:00 18 -> /dev/null lr-x------ 1 root root 64 Jul 16 14:00 19 -> /file.txt l-wx------ 1 root root 64 Jul 16 14:00 2 -> 'pipe:[56377]' lr-x------ 1 root root 64 Jul 16 14:02 20 -> /file.txt lr-x------ 1 root root 64 Jul 16 14:02 21 -> /file.txt lr-x------ 1 root root 64 Jul 16 14:02 22 -> /file.txt lr-x------ 1 root root 64 Jul 16 14:02 23 -> /file.txt ...
23 -> /file.txt
fdの番号 -> 開いている対象
lsofコマンド
lsof -p PID # もしくは # lsof -c プロセス名
lsof -p 1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME node 1 root cwd DIR 0,142 4096 270825 / node 1 root rtd DIR 0,142 4096 270825 / node 1 root txt REG 0,142 81180048 2890255 /usr/local/bin/node node 1 root mem REG 254,1 2890255 /usr/local/bin/node (path dev=0,142) node 1 root mem REG 254,1 2885690 /lib/x86_64-linux-gnu/libc-2.31.so (path dev=0,142) node 1 root mem REG 254,1 2885735 /lib/x86_64-linux-gnu/libpthread-2.31.so (path dev=0,142) node 1 root mem REG 254,1 2885704 /lib/x86_64-linux-gnu/libgcc_s.so.1 (path dev=0,142) node 1 root mem REG 254,1 2885711 /lib/x86_64-linux-gnu/libm-2.31.so (path dev=0,142) node 1 root mem REG 254,1 2886466 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.28 (path dev=0,142) node 1 root mem REG 254,1 2885698 /lib/x86_64-linux-gnu/libdl-2.31.so (path dev=0,142) node 1 root mem REG 254,1 2885678 /lib/x86_64-linux-gnu/ld-2.31.so (path dev=0,142) node 1 root 0u CHR 1,3 0t0 5 /dev/null node 1 root 1w FIFO 0,11 0t0 56376 pipe node 1 root 2w FIFO 0,11 0t0 56377 pipe node 1 root 3u a_inode 0,12 0 12340 [eventpoll] node 1 root 4r FIFO 0,11 0t0 50866 pipe node 1 root 5w FIFO 0,11 0t0 50866 pipe node 1 root 6r FIFO 0,11 0t0 50867 pipe node 1 root 7w FIFO 0,11 0t0 50867 pipe node 1 root 8u a_inode 0,12 0 12340 [eventfd] node 1 root 9u a_inode 0,12 0 12340 [eventpoll] node 1 root 10r FIFO 0,11 0t0 55593 pipe node 1 root 11w FIFO 0,11 0t0 55593 pipe node 1 root 12u a_inode 0,12 0 12340 [eventfd] node 1 root 13u a_inode 0,12 0 12340 [eventpoll] node 1 root 14r FIFO 0,11 0t0 50868 pipe node 1 root 15w FIFO 0,11 0t0 50868 pipe node 1 root 16u a_inode 0,12 0 12340 [eventfd] node 1 root 17r REG 0,142 0 3807111 /file.txt node 1 root 18r CHR 1,3 0t0 5 /dev/null node 1 root 19r REG 0,142 0 3807111 /file.txt node 1 root 20r REG 0,142 0 3807111 /file.txt node 1 root 21r REG 0,142 0 3807111 /file.txt ...
lsof -p 1 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME ... node 1 root 20r REG 0,142 0 3807111 /file.txt
- FDは20番をreadで開いているという意味
- TYPEのREGはregular file
- NAMEは開いている対象
今回のプログラムはfile.txtを開き続けるので、file.txtが多く表示される。
総数だけ見たいときは下記
lsof -p 1 | wc -l
ulimitを変更する
docker run --name test --ulimit nofile=100:100 test
ulimit -Sn 100 ulimit -Hn 100
上限を30にすると29まで使った次に、too many open filesのエラーが出ることを確認
docker run --name test --ulimit nofile=30:30 test fd: 17 err: null fd: 19 err: null fd: 20 err: null fd: 21 err: null fd: 22 err: null fd: 23 err: null fd: 24 err: null fd: 25 err: null fd: 26 err: null fd: 27 err: null fd: 28 err: null fd: 29 err: null fd: undefined err: Error: EMFILE: too many open files, open 'file.txt'
まとめ
too many open filesのエラーが出たとき、どのプロセスが、なんのファイルを開こうとしているのか確認する手順は下記
ps auxでプロセス番号確認lsof -p PIDでファイルの上表を表示- NAME欄で多く表示されているものを確認する
対応として、
- プログラムで不具合はないか確認
- ulimitで開くことのできるファイルの上限を上げる
参考情報
man lsof
...
FD is followed by one of these characters, describing the
mode under which the file is open:
r for read access;
w for write access;
u for read and write access;
space if mode unknown and no lock
character follows;
`-' if mode unknown and lock
character follows.
...
man lsof ... TYPE is the type of the node associated with the file - e.g., GDIR, GREG, VDIR, VREG, etc... ... or ``REG'' for a regular file; ...
https://nodejs.org/dist/latest-v16.x/docs/api/fs.html#fsopenpath-flags-mode-callback
マイクロサービス移行とモノレポを選択した理由について
概要
仕事でマイクロサービス移行にともなってモノレポを採用した。 その理由をまとめる。 具体的な内容には触れない。
課題
長らくモノリスで開発してきたことによる
- コード量の増加
- テスト時間の増加
- 依存関係によるデプロイの遅れ
- 開発者への依存
が課題になっていた。
改善方針
これに対して、
- マイクロサービス分離
- モノレポによる開発体験の統一
によって解決を図った。
具体策
サービス分離
機能単位でモノリスアプリケーションを分離し、サービスごとに開発サイクルを回せるように
コードの変更差分に対してのみCI実行、サービスごとにCI並列実行サービスごとに独立したデプロイ
Gitタグで任意のタイミングでサービスごとにDockerイメージを作成し、デプロイ
これにより
- CI実行時間の削減
- ワークフローの統一化
- デプロイの依存関係の解消
を実現した。
モノレポ
npm workspaceを利用、モノレポフレームワークなし
開発ワークフローの統一
差分実行、並列実行
GitHub Actionsで変更差分に対してのみCI実行、サービスごとにCIを並列実行セットアップの自動化
Makefileを実行するだけで開発をはじめられるように
なぜモノレポか?
工夫するとポリレポでもモノレポのメリットの一部は実現できる
- GitHub Template Repository(マイクロサービスのテンプレート)
- GitHub Actions Reusable Workflow(CIワークフローの共通化)
- GitHub Actions Repository Dispatch(リポジトリ間のCIトリガー)
- npm packageによる設定ファイルの共通化
チームのコラボレーションのため
- 特定の機能が特定の開発者に依存しつつある
- 特定の開発者しかメンテナンスしないリポジトリがある
- コードを共有する仕組みが整っていない
これらの課題を解決するため
セットアップの仕組み化と適切なサービス分離により、認知不可を低めつつ
担当していないプロダクトコードも目に入りやすい状況を作る。
また、プロダクトコードはすべて一つのリポジトリにある状況を作ることで、リポジトリの管理が不要になる。
依存関係の解決のため
現状OpenAPIを別リポジトリで開発しており、 開発時の依存関係解決のワークフローを簡潔にしたい
今後必要なこと
CIプロセスの継続的な改善
開発が進むにつれビルドや静的解析の速度が課題になるCDプロセスの改善
プログレッシブデリバリーマイクロサービス化に向けた組織構成について
現状の組織構成のままやっていけるかチーム内で検討
まとめ
開発ワークフローの課題をサービス分離にともなって解決していっているという話を書いた。
一番の目的は開発サイクルを早め、市場の変化に対応すること
現在、開発基盤を大方作り終え、今後フィードバックを受けながら改善していく
マイクロサービスや開発チームが増えてきたときに、開発体験が悪くならない仕組みを作る
CIの差分実行・並列実行、CIの共通化、セットアップの簡略化などで実現モノレポを採用することでチームのコラボレーションを促進する
npm workspaceとnpm installの挙動確認
環境
% node -v v16.13.0 % npm -v 8.11.0
概要
npm workspaceでnpm installを実行すると
1. ワークスペースルート
2. (バージョンが異なるものは)各々のワークスペース
の順にインストールされる
構成
npm-workspace % tree . -a -I node_modules . ├── .gitignore ├── .npmrc ├── app-a │ └── package.json ├── app-b │ └── package.json ├── package-lock.json └── package.json
save-exact=true
{ "name": "npm-workspace", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "workspaces": [ "app-a", "app-b" ] }
挙動の確認
%npm i rimraf -w app-a %npm i rimraf@2 -w app-b
結果A
% tree . . ├── app-a │ └── package.json ├── app-b │ ├── node_modules │ │ └── rimraf │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bin.js │ │ ├── package.json │ │ └── rimraf.js │ └── package.json ├── node_modules │ ├── app-a -> ../app-a │ ├── app-b -> ../app-b │ ├── balanced-match │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.js │ │ └── package.json │ ├── brace-expansion │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ └── package.json │ ├── concat-map │ │ ├── LICENSE │ │ ├── README.markdown │ │ ├── example │ │ │ └── map.js │ │ ├── index.js │ │ ├── package.json │ │ └── test │ │ └── map.js │ ├── fs.realpath │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ ├── old.js │ │ └── package.json │ ├── glob │ │ ├── LICENSE │ │ ├── README.md │ │ ├── common.js │ │ ├── glob.js │ │ ├── package.json │ │ └── sync.js │ ├── inflight │ │ ├── LICENSE │ │ ├── README.md │ │ ├── inflight.js │ │ └── package.json │ ├── inherits │ │ ├── LICENSE │ │ ├── README.md │ │ ├── inherits.js │ │ ├── inherits_browser.js │ │ └── package.json │ ├── minimatch │ │ ├── LICENSE │ │ ├── README.md │ │ ├── minimatch.js │ │ └── package.json │ ├── once │ │ ├── LICENSE │ │ ├── README.md │ │ ├── once.js │ │ └── package.json │ ├── path-is-absolute │ │ ├── index.js │ │ ├── license │ │ ├── package.json │ │ └── readme.md │ ├── rimraf │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bin.js │ │ ├── package.json │ │ └── rimraf.js │ └── wrappy │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── wrappy.js ├── package-lock.json └── package.json 21 directories, 65 files
package-lock.jsonを削除し、コマンドの実行順を変えてみる
% rm package-lock.json % rm -rf node_modules && rm -rf app-b/node_modules
%npm i rimraf@2 -w app-b %npm i rimraf -w app-a
結果
% tree . . ├── app-a │ ├── node_modules │ │ └── rimraf │ │ ├── CHANGELOG.md │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bin.js │ │ ├── package.json │ │ └── rimraf.js │ └── package.json ├── app-b │ └── package.json ├── node_modules │ ├── app-a -> ../app-a │ ├── app-b -> ../app-b │ ├── balanced-match │ │ ├── LICENSE.md │ │ ├── README.md │ │ ├── index.js │ │ └── package.json │ ├── brace-expansion │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ └── package.json │ ├── concat-map │ │ ├── LICENSE │ │ ├── README.markdown │ │ ├── example │ │ │ └── map.js │ │ ├── index.js │ │ ├── package.json │ │ └── test │ │ └── map.js │ ├── fs.realpath │ │ ├── LICENSE │ │ ├── README.md │ │ ├── index.js │ │ ├── old.js │ │ └── package.json │ ├── glob │ │ ├── LICENSE │ │ ├── README.md │ │ ├── common.js │ │ ├── glob.js │ │ ├── package.json │ │ └── sync.js │ ├── inflight │ │ ├── LICENSE │ │ ├── README.md │ │ ├── inflight.js │ │ └── package.json │ ├── inherits │ │ ├── LICENSE │ │ ├── README.md │ │ ├── inherits.js │ │ ├── inherits_browser.js │ │ └── package.json │ ├── minimatch │ │ ├── LICENSE │ │ ├── README.md │ │ ├── minimatch.js │ │ └── package.json │ ├── once │ │ ├── LICENSE │ │ ├── README.md │ │ ├── once.js │ │ └── package.json │ ├── path-is-absolute │ │ ├── index.js │ │ ├── license │ │ ├── package.json │ │ └── readme.md │ ├── rimraf │ │ ├── LICENSE │ │ ├── README.md │ │ ├── bin.js │ │ ├── package.json │ │ └── rimraf.js │ └── wrappy │ ├── LICENSE │ ├── README.md │ ├── package.json │ └── wrappy.js ├── package-lock.json └── package.json
以上の結果より、コマンドが実行された順番でワークスペースルートにインストールされることを確認できた。
npm installの実行順について
下記のような状態で
app-a/package.json
... "dependencies": { "rimraf": "3.0.2" }, ...
app-b/package.json
... "dependencies": { "rimraf": "2.7.1" }, ...
- node_modulesとpackage-lock.jsonを削除してnpm iしたところAと同じ構造になることを確認
- package.jsonのworkspaceの順序を入れ替えてnpm iしてもAと同じ構造になることを確認
package.json
"workspaces": [ "app-b", "app-a" ]
- app-bの名前を変えてnpm iを実行すると、ワークスペースルートにrimraf@2.7.1がインストールされることを確認
%mv app-b app
% npm i
よってnpm iの実行順序はディレクトリ名順で実行されることがわかった。
まとめ
npm workspaceとnpm installの挙動について初めて見たときよくわからなかったのでまとめた。