課題管理に対する考え方とテンプレ

自分の中の課題管理に対する考え方を整理するため、ブログにまとめておく。

課題を管理する

ソフトウェア開発ではGitHub IssueやRedmineなどで課題を管理する。

基本的に「課題を登録する量 >> 完了する量」であり、多くの課題が登録されるため、優先度をつけて順番に対応していく事になる。

課題に優先度をつける

課題が 一意に並ぶ ように優先度をつけて、上から順番に対応する。

「どの "緊急" の課題を対応すべきですか?」と質問しなくて済む状態にする。

課題の管理工数を意識する

課題が増えると管理する工数も増えるため、以下の2つを意識する必要がある。

  1. 情報を過不足なく、分かりやすく記載する
    • 課題の登録者に質問しないようにする
  2. 少ない工数で解決する
    • 工数の少ない代案を積極的に挙げる

また、代案を出しやすくするため、課題を登録するときに Why(なぜ)とHow(どのように)が混ざらない ように気を付ける。

課題として登録する内容

以下を課題として登録する。

  • 機能要望(Feature Request)
  • バグ修正(Bug Report)
  • 雑用(Chore)
    • ライブラリのアップグレード対応など

開発者の「Modelを実装する」などのタスクを登録し始めると管理工数が増えてしまうため、これらは登録しない。*1

仕様を確認するより、改善要望として登録する

機能が分かりづらいという事なので「UIを改善する」「コードにコメントを残す」などの改善を検討する。

課題に記載する項目

課題管理のテンプレで使えるように、マークダウン形式で項目を記載しておく。

「Howは課題の登録者の提案であり、唯一の解決方法ではない」ことを意識し、開発者は 少ない工数で済むように提案する ことを意識する。

機能要望

<!-- 現状の顧客の課題と要望する機能について記載してください。 -->

## 現状の課題(Why)

## 要望する機能の説明(How)

## 備考

バグ報告

<!-- 開発者がバグを再現・修正できるように、具体的な再現手順を記載してください。 -->

## 概要(Why)

## 再現手順

## 実際の挙動

## 期待する挙動(How)

## 備考

雑用

## 作業内容(How)

## 作業の利点、実施しない場合のリスク(Why)

## 備考

作業内容だけを書きがちなので、実施した時の利点や実施しない場合のリスクも書いておくこと。

*1:一覧に出ない形で別管理はアリ。課題のコメント欄に残すなど

RustでRubyのgemを書く part2

RustでRubyのgemを書く part1 の続き。

クラスやインスタンス変数を使うメソッドを実装したので、まとめておく。

Rubyで実装したいコードのイメージ

module Wasabi
  class Object
    def initialize(name)
      @name = name
    end
    attr_reader :name

    def say
      "say, #{name}"
    end
  end
end

実際のRustのコード

github.com

クラスの定義

まず、RubyのObjectをRustで使うために extern を書く。

extern {
    static rb_cObject: Value;
}

これをを継承するクラス Wasabi::Object を定義する。

let rb_class = class::rb_define_class_under(rb_mod, str_to_cstring("Object").as_ptr(), rb_cObject);

initialize の実装

第一引数はインスタンスになるため、 引数を2つ書く必要がある。

extern fn rb_object_initialize(obj: Value, name: Value) {
    unsafe {
        let name_id = util::rb_intern(str_to_cstring("name").as_ptr());
        class::rb_ivar_set(obj, name_id, name);
    }
}

rb_ivar_setインスタンス変数に引数を保存している。

name の実装

initializeで保存した値をrb_ivar_getで取得して返す。

unsafe extern fn rb_name(obj: Value) -> Value {
    let name_id = util::rb_intern(str_to_cstring("name").as_ptr());
    class::rb_ivar_get(obj, name_id)
}

say の実装

インスタンス変数の値の型はValueなので、Rustで使いやすくするため value_to_string でStringに変換する。 format! で文字列を組み立てた後、またValueに変換して返す。

unsafe extern fn rb_say(obj: Value) -> Value {
    let name_id = util::rb_intern(str_to_cstring("name").as_ptr());
    let ivar_name = class::rb_ivar_get(obj, name_id);
    let name = value_to_string(ivar_name);
    let message = format!("say, {}", name);
    string::rb_utf8_str_new(str_to_cstring(&message).as_ptr(), message.len() as c_long)
}

value_to_string の実装はこんな感じ。

fn value_to_string(value: Value) -> String {
    unsafe {
        let str = string::rb_string_value_cstr(&value);
        CStr::from_ptr(str).to_string_lossy().into_owned()
    }
}

テストコード

ここまで実装したことで、以下のテストが動作した。

RSpec.describe Wasabi::Object do
  let(:obj) { Wasabi::Object.new("foo") }

  describe "#name" do
    it "returns the initialization args" do
      expect(obj.name).to eq "foo"
    end
  end

  describe "#say" do
    it "returns the greeting message" do
      expect(obj.say).to eq "say, foo"
    end
  end
end

調査中

Rustの構造体を使う方法

せっかくRustを使っているのに、関数を使う実装になってしまっている。 Rustの構造体やメソッドをRuby側で呼び出す方法を調べたい。

RustでRubyのgemを書く part1

RubyGems 3.3.11でRust拡張の対応が実験的に入ったので、少し触ってみたメモ。

In particular, it includes experimental supppot for Rust extensions.

引用: https://blog.rubygems.org/2022/04/07/3.3.11-released.html

wasabi

2019年に鹿児島Ruby会議01で作ったRust製gemがあるので、これを修正しました。

speakerdeck.com

github.com

thermite の削除

refs: b16bfd1e2864adbb698f5225f86e7fd18da0b95e

thermite はRust拡張を作るために使用していたが、RubyGemsが対応したことで不要になったので削除する。

ext/wasabi/extconf.rb の内容は正直よく分かっていない。 サンプルコードであるrb-rust-gemのext/rust_ruby_example/extconf.rb をコピペしたら動いた。

GitHub Actionsでのビルド

refs: 4c8b35774b4b1d6e2b94c1b6dc7aee4bf0e97180

GitHub ActionsのUbuntuにはRust環境が含まれているので、以下のYAMLでビルド + テストが可能になる。

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@v3

    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true

    - run: gem update --system
    - run: bundle exec rake

実行結果を見ると、 .so のビルドが動いた後にRSpecが流れているのが確認できる。

▶︎ Run bundle exec rake
mkdir -p tmp/x86_64-linux/wasabi/2.7.6
cd tmp/x86_64-linux/wasabi/2.7.6
/opt/hostedtoolcache/Ruby/2.7.6/x64/bin/ruby -I. ../../../../ext/wasabi/extconf.rb
cd -
mkdir -p tmp/x86_64-linux/stage/lib/wasabi
install -c tmp/x86_64-linux/wasabi/2.7.6/wasabi.so lib/wasabi/wasabi.so
cp tmp/x86_64-linux/wasabi/2.7.6/wasabi.so tmp/x86_64-linux/stage/lib/wasabi/wasabi.so
/opt/hostedtoolcache/Ruby/2.7.6/x64/bin/ruby -I/home/runner/work/wasabi/wasabi/vendor/bundle/ruby/2.7.0/gems/rspec-core-3.9.0/lib:/home/runner/work/wasabi/wasabi/vendor/bundle/ruby/2.7.0/gems/rspec-support-3.9.0/lib /home/runner/work/wasabi/wasabi/vendor/bundle/ruby/2.7.0/gems/rspec-core-3.9.0/exe/rspec --pattern spec/\*\*\{,/\*/\*\*\}/\*_spec.rb

Wasabi
  has a version number
  .sum
    1 + 2 = 3
  .call_to_s
    1.to_s
      is expected to eq "1"
    class with :to_s defined
      is expected to eq "foo"

Finished in 0.00192 seconds (files took 0.08639 seconds to load)
4 examples, 0 failures

rb-sys or ruby-sys

RustからRuby APIを呼ぶときに使用できそうなクレート。

  • oxidize-rb/rb-sys
    • RubyGemsの対応した人がコントリビュータ
    • 使い方がよく分からない...
    • 現在も更新されている
  • steveklabnik/ruby-sys
    • 2017年で更新停止
    • コード読めば、なんとなく使い方は分かる

仕組みがよく分かっていなくて、このコードでなぜRubyAPIが呼べるのかは謎。

Rust の実装

C拡張のgemでは Init_xxx が最初に呼ばれるので、ここでモジュールやメソッドを定義する。

#[no_mangle]
pub extern fn Init_wasabi() {
    unsafe {
        let rb_mod = class::rb_define_module(str_to_cstring("Wasabi").as_ptr());
        class::rb_define_singleton_method(rb_mod, str_to_cstring("sum").as_ptr(), rb_sum as CallbackPtr, 2);
        class::rb_define_singleton_method(rb_mod, str_to_cstring("call_to_s").as_ptr(), rb_call_to_s as CallbackPtr, 1);
    }
}

Wasabi.sum のRustの実装は以下の通りで、Value型を数字に戻して計算した後にValueに戻して返している。

// 引数の合計数を返す。
extern fn rb_sum(_mod: Value, a :Value, b: Value) -> Value {
    let a = unsafe { fixnum::rb_num2int(a) as i64 };
    let b = unsafe { fixnum::rb_num2int(b) as i64 };
    let sum = a + b;

    unsafe { fixnum::rb_int2inum(sum as SignedValue) }
}

Wasabi.call_to_s の実装はこんな感じ。

// 引数の `to_s` を呼んで返す。
extern fn rb_call_to_s(_mod: Value, obj: Value) -> Value {
    unsafe {
        let method_id = util::rb_intern(str_to_cstring("to_s").as_ptr());
        util::rb_funcallv(obj, method_id, 0, ptr::null())
    }
}

調査中

調べ終わったら、part2の記事を書きたい。

Rustの構造体をRubyで使う

構造体をData_Wrap_StructValueに変換する必要があるらしい。 ただ、この辺りのRubyのマクロをどうやってRustから呼び出すのか理解できてない...。

RustのメソッドをRubyで使う

関数をRubyで使う方法は分かったが、メソッドをRubyで使う方法はまだ分かっていない。 Rustの構造体 + メソッドをRubyのクラス + メソッドとして使いたい。

RailsアプリをRBS + Steepで型チェックするまでの手順

rails new してから steep check が通るまでにやってみた作業をまとめておく。

GitHub

コードはGitHubで公開しているので、詳細な手順を知りたい方は参照してください。

github.com

手順

gemを入れる

steeprbs_railsをGemfileに追加する。

# Gemfile
group :development do
  gem 'rbs_rails', require: false
  gem 'steep', require: false
end

bundle install を実行する。

$ bundle install

rbs_rails のREADMEの手順に従って lib/tasks/rbs.rake を作成する。

# lib/tasks/rbs.rake
require 'rbs_rails/rake_task'

RbsRails::RakeTask.new

gemのrbsを取得する

いくつかのgemの型定義はgem_rbs_collectionから取得して利用できる。

$ bundle exec rbs collection init
$ bundle exec rbs collection install

取得したrbsファイルはGitで管理する必要はないので .gitignore に追加しておく。

/.gem_rbs_collection

rbs_railsrbsを生成する

Railsの提供するメソッド定義をいくつか自動生成してくれる。

bin/rails rbs_rails:all

自動生成されるファイルなので .gitattributes に追加しておくと良い。*1

sig/rbs_rails/** linguist-generated

Steepfileを作る

bundle exec steep init で雛形が作成されます。最小の設定だと以下の通り。

target :app do
  signature "sig"

  check "app"
end

既存ファイルのrbsを用意する

ディレクトリ構造にあわせてrbsのプロトタイプを生成するシェル - アジャイルSEの憂鬱 で紹介したワンライナーを使う。

$ find app/ -name '*.rb' | xargs -I{} bash -c 'mkdir -p sig/$(dirname {}); bundle exec rbs prototype rb {} > sig/{}s;'

app/models/application_record.rb のように最初から存在するファイルに対して、rbsのプロトタイプを生成する。

不足してるrbsを用意する

gem_rbs_collectionにないgemのrbsは自分で書く必要があるので書く。

rails new した直後の状態で不足していた型定義は以下の通り。(オプションによって変わるかもしれない)

# sig/patch.rbs
module ActiveRecord
  class Base
    def self.primary_abstract_class: () -> void
  end
end

module ActionMailer
  class Base
    def self.default: (untyped) -> void
    def self.layout: (untyped) -> void
  end
end

module ActionCable
  module Channel
    class Base
    end
  end

  module Connection
    class Base
    end
  end
end

型チェックを実行する

ここまで設定がおわれば、 steep check を実行できる。

$ bundle exec steep check
# Type checking files:

.....................................................................................................................................................

No type error detected. 🫖

ジェネレーター後にrbsを自動生成する

ジェネレーターを使ったときにrbsのプロトタイプを作成したり、 rbs_rails:all を自動で実行するようにしておく。 少し楽になる。

# config/environments/development.rb
Rails.application.configure do
  config.generators.after_generate do |files|
    files.each do |f|
      next unless f.match?(%r{^app/.+\.rb$})

      rb_path = Rails.root.join(f)
      rbs_path = Rails.root.join('sig', f.sub(/\.rb$/, '.rbs'))
      rbs_path.dirname.mkpath unless rbs_path.dirname.exist?
      system("bundle exec rbs prototype rb #{rb_path} > #{rbs_path}", exception: true)
    end

    if files.any? { |f| f.match?(%r{^app/.+\.rb$}) }
      system("bin/rails rbs_rails:all", exception: true)
    end
  end
end

scaffold を試す

ここまでの設定を済ませた状態で、scaffoldを試してみる。

$ bin/rails g scaffold book title author

型チェックを実行する。

$ bundle exec steep check
# Type checking files:

......................................................................................................................................F......................

app/controllers/books_controller.rb:28:34: [error] Type `::BooksController` does not have method `book_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to book_url(@book), notice: "Book was successfully created." }
                                    ~~~~~~~~

app/controllers/books_controller.rb:41:34: [error] Type `::BooksController` does not have method `book_url`
│ Diagnostic ID: Ruby::NoMethod
│
└         format.html { redirect_to book_url(@book), notice: "Book was successfully updated." }
                                    ~~~~~~~~

app/controllers/books_controller.rb:55:32: [error] Type `::BooksController` does not have method `books_url`
│ Diagnostic ID: Ruby::NoMethod
│
└       format.html { redirect_to books_url, notice: "Book was successfully destroyed." }
                                  ~~~~~~~~~

Detected 3 problems from 1 file

コントローラで呼び出しているメソッドで、エラーが出る。

エラーについて

web上の記事だと check "app/models" だけ型チェックしてる記事が多いので、現状だとコントローラの型チェックは難しいのかも?

直し方が分かったら、またブログに書くかもしれない。

おまけ

GitHub Actionsで型チェックを動かす

# .github/workflows/check.yml
name: check

on:
  push:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
    - uses: actions/checkout@v3

    - uses: ruby/setup-ruby@v1
      with:
        bundler-cache: true

    - run: bundle exec rbs collection install
    - run: bundle exec steep check

Steep VSCode Integration

steep-vscode を使うと、VSCode上で型チェックの結果を見ることができる。

f:id:sinsoku:20220309232348p:plain
VSCode

注意: devcontainerだとrbsの変更が反映されない?

devcontainerでsteep-vscodeを使ってもrbsのファイル変更が検知されない問題が起きた。 Command + Shift + P => Steep: Restart all で再起動すれば直る。

原因はよく分からないですが、ファイルIO関連なのでDocker for Mac限定の問題かもしれません。

*1:プルリクのdiffが閉じた状態になる

ディレクトリ構造にあわせてrbsのプロトタイプを生成するシェル

既存のリポジトリrbsをゼロから書くのは大変なので、とりあえず雑にプロトタイプを生成する方法。

ディレクトリ構造

app 以下のディレクトリ構造にあわせて sigrbsを置くディレクトリ構成です。

app/models/user.rb
sig/app/models/user.rbs

シェル

find と xargs を使ってディレクトリを作成しつつ、rbsを生成する。

$ find app/ -name '*.rb' |  xargs -I{} bash -c 'mkdir -p sig/$(dirname {}); rbs prototype rb {} > sig/{}s;'

ECSを運用で使っていて難しいと思った点

ECSを触っていて今まで難しいと思ったことを雑にまとめておく。

タスクロールとタスク実行ロールの違い

ECSを長く触っているのに、いつも混乱する。

  • タスクロール
    • コンテナ内の権限
    • S3やSESなどの権限をつける
  • タスク実行ロール
    • コンテナ外の権限
    • ECRやParameter Storeの権限をつける

ECSのデプロイ時に静的ファイルが404になる

ECSを触った初期に遭遇した。 詳細は以下のQiitaの記事が分かりやすい。

参照: ECSのデプロイ時に一定確率で静的ファイルが404になる問題を回避する

回避する方法はいくつかある。

  • 静的ファイルをS3に置く
  • CodeDeployの OneAtATime を使う
  • CodeDeployのBlue/GreenとALBのスティッキーセッションを使う

知らないと気づくのは難しい。

ECSサービスを無停止で更新する手順

ECSサービスをTerraformで変更するとき、一部の設定は replace になる。 無停止で更新するために、新しいECSサービスを作成してターゲットグループをうまく切り替える必要がある。

  1. 新しいECSサービスを作成
  2. Host: dummy.exmaple.com のリスナールールに新ECSサービスを紐づける
    • デフォルトリスナーは旧ECSサービス
  3. 新旧のECSサービスを両方ともデプロイするようにデプロイを設定する
  4. リスナールールの Host を正しい値に変更
    • この時点で新ECSサービスにリクエストが流れる
  5. 旧ECSサービスを削除

手順が多くて面倒なので、リソースを作成する前にecs_serviceの全オプションをしっかり読んだ方が良い。

  • EC2 -> Fargate の移行
  • ECSデプロイ -> CodeDeploy の移行
  • Propagate tagsの設定

少なくとも、これらの作業をするときに必要になった。

CannotPullContainerError でECSタスクが無限に再起動する

ECRにDockerイメージをプッシュしないでタスク定義を更新・デプロイすると起きる。

deployment circuit breaker の設定しておくと良い。

参考: Amazon ECS deployment circuit breaker のご紹介

execute-command は最大2セッション

ecs execute-command でECSタスクに接続できるのは 2セッション まで。

複数人がECSタスクに接続する運用がある場合には注意が必要。

Terraformでタスク定義の最新を参照する

デプロイするとタスク定義のリビジョンが変わるので、ecs_task_definitionのコードを参考にコードを書く必要がある。

data "aws_ecs_task_definition" "mongo" {
  task_definition = aws_ecs_task_definition.mongo.family
}

resource "aws_ecs_cluster" "foo" {
  name = "foo"
}

resource "aws_ecs_task_definition" "mongo" {
  family = "mongodb"

  container_definitions = "<略>"
}

resource "aws_ecs_service" "mongo" {
  name          = "mongo"
  cluster       = aws_ecs_cluster.foo.id
  desired_count = 2

  # Track the latest ACTIVE revision
  task_definition = "${aws_ecs_task_definition.mongo.family}:${
    max(aws_ecs_task_definition.mongo.revision, data.aws_ecs_task_definition.mongo.revision)
  }"
}

CodeDeployを使っているとECSサービスのタグが更新できない

AWSコンソールからは普通にタグをつけられるが、Terraformからは更新できずに下記のエラーが起きる。

Unable to update network parameters on services with a CODE_DEPLOY deployment controller. Use AWS CodeDeploy to trigger a new deployment

詳細は調べられていないけど、AWS Providerのバグかも?

CodeDeploy Blue/Greenを使うとTarget Groupは交互に変わる

リソースを用意するときはTerraformだけど、デプロイは別で行う場合にターゲットグループがズレるケースがある。

  1. Terraformでターゲットグループ(TG-1, TG-2)を用意する
  2. Terraformで TG-1 をリスナールールに紐付ける
  3. CodeDeployでTG-1, TG2のデプロイ設定を行う
  4. CodeDeployでデプロイを行う
    • リスナールールに紐づくリソースが TG-1 => TG-2 に変わる
  5. Terraformで設定を変える
    • コード上は TG-1 がリスナールールに紐づいている
    • apply するとECSサービスの紐づいていない TG-1 に変わり障害になる

terraform plan の出力をちゃんと読めば気づける。

タスク定義みたいにTerraformのコードを工夫すれば回避できるのかな...私は知らないが...

GitHub Actions vs CodePipeline

どちらも触った経験があるけど、どちらも難しくて慣れが必要。

  • GitHub Actions: 日本語の記事が少ない、コード例が少ない
  • CodePipeline: GitHubの連携が面倒、慣れないとIAMとVPCでハマる

比較して考察している記事もあるので、読んでみると良い。

参考: ECS用のCDパイプラインに対する考察

Gemfileに記載してあるgemの説明を一覧で表示する #fjordbootcamp

追記: bundle info でgemの情報を表示できるので、一覧じゃなければコレで十分かも。

$ bundle info rails
  * rails (6.1.3.2)
    Summary: Full-stack web application framework.
    Homepage: https://rubyonrails.org
    Documentation: https://api.rubyonrails.org/v6.1.3.2/
    Source Code: https://github.com/rails/rails/tree/v6.1.3.2
    Changelog: https://github.com/rails/rails/releases/tag/v6.1.3.2
    Bug Tracker: https://github.com/rails/rails/issues
    Mailing List: https://discuss.rubyonrails.org/c/rubyonrails-talk
    Path: /app/vendor/bundle/ruby/3.0.0/gems/rails-6.1.3.2

タイトルに #fjordbootcamp がついていますが、これは #fjordbootcamp の課題とかではありません。

フィヨルドブートキャンプとは

bootcamp.fjord.jp

きっかけ

フィヨルドブートキャンプのGitHubリポジトリに以下のIssueが登録されていた。*1

github.com

これを読んでいて、ふと「gemspecから説明を抜き出して、一覧で表示できると便利そう」と思いついたので、試しに実装してみた。

コード

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'bundler'

lockfile = Bundler.default_lockfile.read
parser = Bundler::LockfileParser.new(lockfile)

spec_per_name = parser.specs.group_by(&:name).transform_values(&:first)

parser.dependencies.keys.each do |name|
  spec = spec_per_name[name]

  if spec
    path = "#{Bundler.specs_path}/#{name}-#{spec.version}.gemspec"
    gemspec = Gem::Specification.load(path)
  end

  puts "#{name}: #{gemspec&.summary}"
end

実行結果

$ ./bundler-summaries
bootsnap: Boot large ruby/rails apps faster
byebug: Ruby fast debugger - base + CLI
capybara: Capybara aims to simplify the process of integration testing Rack applications, such as Rails, Sinatra or Merb
jbuilder: Create JSON structures via a Builder-style DSL
listen: Listen to file modifications
puma: Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server for Ruby/Rack applications
rack-mini-profiler: Profiles loading speed for rack applications.
rails: Full-stack web application framework.
sass-rails: Sass adapter for the Rails asset pipeline.
selenium-webdriver: The next generation developer focused tool for automated testing of webapps
spring: Rails application preloader
sqlite3: This module allows Ruby programs to interface with the SQLite3 database engine (http://www.sqlite.org)
turbolinks: Turbolinks makes navigating your web application faster
tzinfo-data:
web-console: A debugging tool for your Ruby on Rails applications.
webdrivers: Easy download and use of browser drivers.
webpacker: Use webpack to manage app-like JavaScript modules in Rails

*1:フィヨルドブートキャンプでは学習で使うサービス自体がGitHubオープンソースになっている