2021年5月31日月曜日

Gitlab helm chart にリストアする方法

Gitlab helm chart にリストアする方法

概要

Gitlab helm chart を使って別の Gitlab から取得したバックアップデータをリストアしてみました 流れやポイントなどを紹介します

環境

  • helm v3.5.2
  • k8s v1.20.1
  • Gitlab 13.12.0

データのバックアップ

Gitlab のバックアップ機能を使えば OK です 過去に紹介しているので以下の記事を参考にバックアップデータを取得してください

なお今回バックアップファイルはクラウドストレージに配置していることを想定しています

リストアする際も同じバケットからバックアップファイルを取得してリストアします

# s3cmd ls s3://backups  
2021-05-27 01:14 339169280   s3://backups/1622077898_2021_05_27_13.12.0-ee_gitlab_backup.tar

secrets.yml の作成

データのバックアップの他にシークレット情報もバックアップしておきます helm chart 環境であれば以下のコマンドで取得しておきましょう

  • kubectl get secrets gitlab-rails-secret -n gitlab -o jsonpath="{.data['secrets\.yml']}" | base64 --decode > secrets.yaml

新規で Gitlab を構築する

今回は helm chart で構築した Gitlab に対してリストアします 各自の環境に合わせて values.yaml を作成してデプロイしましょう giltab namespace 配下にデプロイします

  • helm install gitlab gitlab/gitlab --namespace gitlab --timeout 600s -f values.yaml

過去の記事を参考にしても OK です ポイントとしてはバックアップを取得したバージョンとリストアする Gitlab のバージョンは可能な限り同一バージョンにしましょう

マイナーバージョンくらいであれば違っていても問題ないかなと思います

secrets を再登録する

新規の Gitlab の構築が完了したらリストアしていきます まずは先程バックした secrets.yml を k8s に secret として登録します

  • kubectl delete secrets gitlab-rails-secret -n gitlab
  • kubectl create secret generic gitlab-rails-secret -n gitlab --from-file=secrets.yml=/path/to/secrets.yaml

Pod を再作成する

登録した secret 情報を使用する Pod を再作成します

  • kubectl delete pods -lapp=sidekiq,release=gitlab -n gitlab
  • kubectl delete pods -lapp=webservice,release=gitlab -n gitlab
  • kubectl delete pods -lapp=task-runner,release=gitlab -n gitlab

rollout restart しても良いと思います 再作成できるまで待ちましょう

クラウドストレージからバックアップデータを取得してリストアする

ではバックアップしたデータをリストアします バックアップ時同様に task-runner を使います

  • kubectl exec gitlab-task-runner-57467bf498-j2lzq -n gitlab -it – backup-utility --restore -t 1622077898_2021_05_27_13.12.0-ee

「1622077898_2021_05_27_13.12.0-ee」の部分はバックアップ時に作成された tar ファイルの前半のファイル名を指定します 冒頭で記載しているバックアップファイルのタイムスタンプ部分とバージョン部分だけを指定すればそのバックアップファイルを指定のバケットから取得してリストアしてくれます

バケットの指定は Gitlab 起動時に values.yml で指定します

また今回は s3 を使っていますが tar ファイルを単純に http でアクセスできる場所に配置しても OK です その場合は直接 URL を指定すればそこからバックアップファイルをダウンロードしてリストアしてくれます

エラーなく最後までデータが取り込まれれば成功です が、まだこの時点ではログインできないません

注意事項: バックアップファイルが大きい場合はストレージに余裕がないと失敗する

バックアップファイルは一度 Gitlab のローカルストレージにダウンロードされます なのでダウンロードファイル分のローカルストレージ or PVC が消費されます

事前にバックアップファイル分のストレージ容量が空いていることを確認してからリストアしてください

ERROR: must be owner of extension pg_trgm

リストア時にデータベースのマイグレーションが走ります 自分は上記のエラーが発生しましたが問題なくリストアできました Gitlab のバージョンが同一であれば同じ PostgreSQL が k8s 上にデプロイされるはずですが外部の PostgreSQL を使っている場合は注意してください

# kubectl exec -it $(kubectl get pods -n gitlab -l app=postgresql -o custom-columns=NAME:.metadata.name --no-headers) -n gitlab -- bash  
Defaulting container name to gitlab-postgresql.  
Use 'kubectl describe pod/gitlab-postgresql-0 -n gitlab' to see all of the containers in this pod.  
I have no name!@gitlab-postgresql-0:/$ PGPASSWORD=$(cat $POSTGRES_POSTGRES_PASSWORD_FILE) psql -U postgres -d gitlabhq_production  
psql (11.9)  
Type "help" for help.gitlabhq_production=# \dx  
                                     List of installed extensions  
    Name    | Version |   Schema   |                            Description  
------------+---------+------------+-------------------------------------------------------------------  
 btree_gist | 1.5     | public     | support for indexing common datatypes in GiST  
 pg_trgm    | 1.4     | public     | text similarity measurement and index searching based on trigrams  
 plpgsql    | 1.0     | pg_catalog | PL/pgSQL procedural language  
(3 rows)

gitlab-runner の再登録

もし gitlab-runner を使っている場合はトークンが再生成されているためランナーの再登録が必要になります 今回は gitlab-runnner は使っていないので省略しますがもしランナーを使っている場合はこちらを参考に再登録してください

アプリの設定を再設定する

ロギングなど内部の処理で使われる値をリセットする必要があります task-runner 内にあるリセットスクリプトを実行すれば OK です

  • kubectl exec gitlab-task-runner-57467bf498-j2lzq -n gitlab -it – /srv/gitlab/bin/rails runner -e production /scripts/custom-instance-setup
Disabling authorized keys write in the database.  
Enabling incremental logging of CI jobs.  
Disabling access to disk storage for GitLab Pages  
Registering OAuth applications.

となれば成功です

パスワードのリセット

Gitlab の root ユーザの初期パスワードをリセットします もし root ユーザのパスワードを初期パスワードからすでに変更している場合は実施する必要はありません もし gitlab-initial-root-password を使ってログインしている場合は必ず実施しましょう

  • kubectl get secret gitlab-gitlab-initial-root-password -n gitlab -ojsonpath='{.data.password}' | base64 --decode ; echo

でまず現在のパスワードをメモしておきます そして rails コマンドを使って直接変更します

kubectl exec gitlab-webservice-default-67bb769fc8-8dmbn -n gitlab -it -- bash  
git@gitlab-webservice-default-67bb769fc8-8dmbn:/$ /srv/gitlab/bin/rails runner "user = User.first; user.password='xxx'; user.password_confirmation='xxx'; user.save!"

xxx の部分をメモしておいたパスワードに置き換えてから実行してください これで上記コマンドで取得できるパスワードで Gitlab にログインすることができるようになります

最後に

流れとしては

  1. バックアップ
    • データバックアップ (with s3)
    • secrets.yml
  2. リストア
    • 新規 Gitlab 構築 (with s3)
    • secrets 再登録
    • データリストア
    • もろもろ再設定

という感じになるかなと思います バックアップ時に忘れずに secrets 系も取得するようにしましょう これがなくなるとデータをリストアできても Gitlab を使えなくなってしまいます

参考サイト

2021年5月30日日曜日

Gitlab の helm chart でバックアップをオブジェクトストレージにアップロードする方法

Gitlab の helm chart でバックアップをオブジェクトストレージにアップロードする方法

概要

Gitlab helm chart のバックアップはデフォルトだと k8s 上に展開された minio になります オブジェクトストレージを指定することもできるのでその方法を紹介します

環境

  • helm v3.5.2
  • k8s v1.20.1
  • Gitlab 13.12.0

.s3cfg ファイルの作成

作成方法は何でも OK です 一番簡単なのは以下かなと思います

アクセスキーなど必要な情報をインタラクティブに入力することで ~/.s3cfg ファイルができます

  • s3cmd --configure

.s3cfg ファイルを secret に登録する

task-runner が .s3cfg を使うので secret に登録します

  • kubectl create secret generic gitlab-s3cmd-secret -n gitlab --from-file=config=/root/.s3cfg

values.yaml の設定

バックアップ用のオブジェクトストレージのバケットの指定と task-runner が参照する secret を設定します 必要な箇所だけ紹介しています

gitlab:  
  task-runner:  
    backups:  
      objectStorage:  
        config:  
          secret: gitlab-s3cmd-secret  
          key: config

global:  
  appConfig:  
    backups:  
      bucket: hawk-backups  
      tmpBucket: hawk-tmp

反映する

helm upgrade なり install をして values.yaml の設定を反映させましょう

  • helm upgrade gitlab gitlab/gitlab --namespace gitlab --timeout 600s -f values.yaml

動作確認

実際にバックアップを取得してオブジェクトストレージ側にバックアップファイルが作成されているか確認しましょう

  • kubectl get pods -lrelease=gitlab,app=task-runner -n gitlab
NAME                                  READY   STATUS    RESTARTS   AGE  
gitlab-task-runner-57467bf498-8cfmf   1/1     Running   0          6m27s
  • kubectl exec gitlab-task-runner-57467bf498-8cfmf -n gitlab – backup-utility
[DONE] Backup can be found at s3://hawk-backups/1622077898_2021_05_27_13.12.0-ee_gitlab_backup.tar

最後に

helm chart の場合いくつかの箇所 (registry or backup or uploads etc…)でオブジェクトストレージを使えますがそれぞれでシークレットの書き方が異なるので注意してください

2021年5月29日土曜日

Gitlab helm chart で https を使わない方法

Gitlab helm chart で https を使わない方法

概要

k8s 側は NodePort + http で受けて別途 nginx を用意してそっちで https を受けるという感じの構成が取れます

環境

  • helm 3.5.2
  • k8s 1.20.1
  • Gitlab helm chart 4.12.0

設定方法

helm install gitlab gitlab/gitlab \
--namespace gitlab \
--timeout 600s \
--version 4.12.0 \
--set global.hosts.domain=gitlab.example.com \
--set global.hosts.https=false \
--set global.ingress.tls.enabled=false \
--set certmanager.install=false \
--set global.ingress.configureCertmanager=false \
--set gitlab-runner.install=false \
--set nginx-ingress.controller.service.type=NodePort \
--set nginx-ingress.controller.service.nodePorts.http=32080 \
--set nginx-ingress.controller.nodeSelector."kubernetes\.io/hostname"="node1"

ポイント

global.hosts.https=falseglobal.ingress.tls.enabled=false ですべてのコンポーネントを http にすることができます

certManager は一応 false を設定していますがもしかすると不要かもしれません

あとは ingress-controller のポートとノードを固定しています こうすることでバランシングする nginx や LB 側でノードの IP を固定することができます 今回は使っていませんが DamonSet を使ってもいいと思います

参考サイト

2021年5月28日金曜日

Gitlab Performance Tool を使ってみた

Gitlab Performance Tool を使ってみた

概要

Gitlab Performance Tool (gpt) はその名の通り Gitlab のパフォーマンスを測ることができるツールです オンプレで構築した Gitlab がどれくらいの規模まで耐えられるかなどテストすることができます docker でどちらもサクット実行することができます

環境

  • Gitlab 13.11.3
  • Gitlab helm chart 4.12.0
  • docker 19.03.6
    • gpt-data-generator 1.0.21
    • gitlab-performance-tool

流れと仕組み

GPT は Gitlab API を使って「テストデータの 作成」と「負荷テスト」を行います なので要件としては

  • API をコールするためのアクセストークンの取得
  • Gitlab の API のエンドポイントにアクセスできる

の 2 つを満たしていれば基本的にはどんな Gitlab 環境にも実行することはできます もし動作しない場合は Gitlab の構築ミスか Gitlab 側のバグになります

root ユーザでパーソナルアクセストークンを作成する

GPT は Gitlab の API を使って負荷テストをします また管理者ユーザ権限が必要になるのでデフォルトで作成される root ユーザを使います

GUI にログインしてユーザの設定からパーソナルアクセストークンを作成しましょう トークンのスコープはすべてオンにします

テストデータの生成

まずは Gitlab にテスト用のプロジェクトやグループを作成します テストデータを作成するためのツールも用意されているのでそれを使います

設定ファイルの作成

GPT は規模に応じてテストのケースを変更することができます 例えば一番小さなテストであれば 1k.json を使います

これをダウンロードして url の部分を変更しましょう それ以外は特に変更の必要はありません

  • mkdir config/environments
  • vim config/environments/1k.json
{
  "environment": {
    "name": "1k",
    "url": "https://gitlab.example.com",
    "user": "root",
    "config": {
      "latency": "0"
    },
    "storage_nodes": ["default"]
  },
  "gpt_data": {
    "root_group": "gpt",
    "large_projects": {
      "group": "large_projects",
      "project": "gitlabhq"
    },
    "many_groups_and_projects": {
      "group": "many_groups_and_projects",
      "subgroups": 250,
      "subgroup_prefix": "gpt-subgroup-",
      "projects": 10,
      "project_prefix": "gpt-project-"
    }
  }
}

実行する

ではテストデータの作成用のコンテナを実行します ACCESS_TOKEN に作成した root ユーザのパーソナルアクセストークンを指定します

いくつか質問されますがすべて Yes で OK です また冒頭にも説明しましたが Gitlab の API をコールするので先程 url に指定した Gitlab へはアクセスできる環境で実行してください

  • mkdir results
docker run --rm -it \
-e ACCESS_TOKEN=xxxxx \
-v $(pwd)/config:/config \
-v $(pwd)/results:/results \
gitlab/gpt-data-generator --environment 1k.json

成功するとグループやプロジェクトが Gitlab 上に作成されているのが GUI 上でも確認できると思います

注意点としては大きいファイルをアップロードする処理があります そこで nginx など 413 に引っかかる可能性があるので注意しましょう

また大きいプロジェクトのインポートには時間がかかるため 504 が返ってくることがあります 504 が返ってきてもインポートは進んでいるので完了するまで待ちましょう 環境にもよりますが 30 分から 1 時間ほどかかります 504 はリバプロ側の nginx であれば以下で回避できます (server ディレクティブに以下を追記)

send_timeout 3600;
proxy_connect_timeout 3600;
proxy_read_timeout    3600;
proxy_send_timeout    3600;

それでも大きなプロジェクトのインポートに失敗する場合は GUI から tar ファイルを指定して gitlabhq1 という名前のプロジェクト名でインポートしてください またグループを gpt/large_projects 配下に移動してください

helm chart の Gitlab を使っている場合は

nginx-ingress の proxy-body-size の設定を変更する必要があります

--set global.ingress.annotations."nginx\.ingress\.kubernetes\.io/proxy-body-size"=0
  • kubectl get ingress gitlab-webservice-default -n gitlab -o jsonpath={.metadata.annotations."nginx\.ingress\.kubernetes\.io/proxy-body-size"}

で 0 になっていれば OK です

トラブルシューティング: gitlab-runner がいるとテスト用のパイプラインをインポートできない?

どうやら runner のあるなしは関係なさそうです

パイプラインも 11 個インポートするのですが gitlab-runner がいるとパイプラインが走ってしまうためインポートしてくれません なので specific-runner/shared-runner はオフにしておきましょう

- Project metadata validation failed: pipelines count '0' should be '11' or higher as specified in the Project Config file.

トラブルシューティング: スペックが足りない

インポートがうまくいかない場合は sidekiq と webservice のメモリと CPU の割当を増やしてみましょう 自分が成功し始めたのはそれぞれに 3GB のメモリを割り当ててから成功するようになりました

そもそもノードに 3GB 以上のメモリがない場合はホストのスペックを上げてから再度試してみてください

負荷の設定ファイルを作成

データの量に合わせて負荷の設定も変えられます 一応おすすめの組み合わせがあるようなので今回はそれに従います

  • 1k - 60s_20rps.json (今回はここ)
  • 2k - 60s_40rps.json
  • 3k - 60s_60rps.json
  • 5k - 60s_100rps.json
  • 10k - 60s_200rps.json
  • 25k - 60s_500rps.json
  • 50k - 60s_1000rps.json

設定ファイルはこちらにあるのでダウンロードしましょう 特に編集するところはありません

  • mkdir config/options
  • vim config/options/60s_20rps.json
{
  "stages": [
    { "duration": "5s", "target": 20 },
    { "duration": "50s", "target": 20 },
    { "duration": "5s", "target": 0 }
  ],
  "rps": 20,
  "batchPerHost": 0
}

パフォーマンステストの実行

ではパフォーマンステストを実行します

  • mkdir tests
docker run --rm -it \
-e ACCESS_TOKEN=xxxxx \
-v $(pwd)/tests:/tests \
-v $(pwd)/config:/config \
-v $(pwd)/results:/results \
gitlab/gitlab-performance-tool --environment 1k.json --options 60s_20rps.json -u

1 時間ほどかかるので待ちましょう 結果は results ディレクトリ配下にあります

ポイントとしては最後の「-u」オプションです これがないと git_push テストなどが行われないのでフルテストを行いたい場合は付与しましょう

結果の見方

Overrall Results Score という値があるのでこれが 100% になっていれば今回テストした規模のユーザ数 (1k.json) はクリアしていることになります

最後に

テストデータを入れるところで今回はハマりました helm chart 版の Gitlab を使ったのですがその場合は MinIO がデフォルトで使われるのでそれの影響かもしれません

参考サイト

2021年5月27日木曜日

Gitlab helm chart でバージョン3 からバージョン4 にアップグレードする方法

Gitlab helm chart でバージョン3 からバージョン4 にアップグレードする方法

概要

Gitlab の helm chart を使ってバージョン3 の chart からバージョン4 の chart へアップグレードする方法を紹介します

ポイントは

  • PostgreSQL のバージョンアップが必要
  • アップグレードするには順番がある

という点です

環境

  • helm 3.5.2
  • k8s 1.20.1
  • Gitlab 3.3.14 -> 4.0.12 -> 4.12.0

事前準備

default の storageClass を作成するか local を使って pvc を作成しましょう

また基本的にはドメインでアクセスすることを想定しているので helm chart にデプロイした Gitlab のレコードと minio のレコードを登録しましょう

バージョン3 のインストール

まずはバージョン3 をインストールします 今回は事前にワイルドカード SSL 証明書を登録しています

helm install gitlab gitlab/gitlab \
--namespace gitlab \
--timeout 600s \
--version 3.3.13 \
--set global.hosts.domain=gitlab.example.com \
--set certmanager.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=gitlab-tls \
--set nginx-ingress.controller.hostNetwork=true \
--set nginx-ingress.controller.service.type="" \
--set nginx-ingress.controller.kind=DaemonSet \
--set nginx-ingress.controller.service.externalTrafficPolicy="" \
--set gitlab-runner.install=false

ここでインストールされる Gitlab のバージョンは 12.10.4 になります

ログインするには以下のコマンドで root ユーザのパスワードを取得します

kubectl get secret gitlab-gitlab-initial-root-password -n gitlab -ojsonpath='{.data.password}' | base64 --decode ; echo

ログインできたらアップグレード後にもデータがあることを確認するために適当にプロジェクトを作成しておきましょう

データベースのバックアップ

次にデータベースのバックアップを作成します 専用のスクリプトが良いされているのでそれを実行するだけです

今回は gitlab namespace 上に helm を展開しているのでバックアップスクリプト実行時にも namespace を指定する必要があります またバックアップファイルは minio へアップロードします ドメインで minio にアクセスできる必要があるので事前に A レコードを設定しておきましょう

  • curl -s "https://gitlab.com/gitlab-org/charts/gitlab/raw/v4.0.12/scripts/database-upgrade" | bash -s -- -n gitlab pre

v4.0.12 は次のアップグレードする Gitlab の helm chart のバージョンになります 現在のバージョンではなく次期バージョンを指定する必要があります

Unable to use a TTY - input is not a terminal or the right kind of file
2021-05-24 06:24:05 +0000 -- Dumping database ...
Dumping PostgreSQL database gitlabhq_production ... [DONE]
2021-05-24 06:24:07 +0000 -- done
Packing up backup tar
WARNING: Module python-magic is not available. Guessing MIME types based on file extensions.
[DONE] Backup can be found at s3://gitlab-backups/database_upgrade_4_gitlab_backup.tar

バックアップファイルが minio にアップロードできれば完了です

データベースの削除

これで古いデータベースの Pod を削除することができます StatefulSet と PersistentVolumeClaim を削除しましょう

  • kubectl delete statefulset gitlab-postgresql -n gitlab
  • kubectl delete pvc data-gitlab-postgresql-0 -n gitlab

バージョン4 へアップグレード

ではバージョン4 の chart へアップグレードします --version オプションを使ってアップグレードする helm chart のバージョンを指定します 今回は 4 系の最新版にする前に必ず 4.0.12 を踏む必要があるのでそれを指定します

他のパラメータは install 時と全く同じ値を指定しましょう

helm upgrade gitlab gitlab/gitlab \
--namespace gitlab \
--timeout 600s \
--version 4.0.12 \
--set global.hosts.domain=gitlab.example.com \
--set certmanager.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=gitlab-tls \
--set nginx-ingress.controller.hostNetwork=true \
--set nginx-ingress.controller.service.type="" \
--set nginx-ingress.controller.kind=DaemonSet \
--set nginx-ingress.controller.service.externalTrafficPolicy="" \
--set gitlab-runner.install=false

Gitlab が起動できたらバージョンを GUI で確認してみましょう 13.0.4 になっていることが確認できると思います

あとは前のバージョンで minio に作成しておいたデータベースのバックアップをリストアすれば OK です

データベースの作り直し

バックアップ時と同じ用にリストア用のスクリプトが提供されています namespace の指定が必要になります minio へのアクセスがドメインで必要になります

  • curl -s "https://gitlab.com/gitlab-org/charts/gitlab/raw/v4.0.12/scripts/database-upgrade" | bash -s -- -n gitlab post

ALTER TABLE などが走ります 成功すると以下のようなログが最後に流れます

...
(以下最後の部分だけ抜粋)
...
Starting: gitlab-sidekiq-all-in-1-v1
deployment.apps/gitlab-sidekiq-all-in-1-v1 patched
Starting: gitlab-gitlab-exporter
deployment.apps/gitlab-gitlab-exporter patched
Starting: gitlab-webservice
deployment.apps/gitlab-webservice patched
configmap "database-upgrade" deleted

これで Gitlab を確認するとバージョン3 のときに作成しておいたプロジェクトが存在することが確認できると思います

おまけ: バージョン4 の最新版へアップデート

執筆時の最新版の chart --version 4.12.0 を指定して upgrade しましょう

helm upgrade gitlab gitlab/gitlab \
--namespace gitlab \
--timeout 600s \
--version 4.12.0 \
--set global.hosts.domain=gitlab.example.com \
--set certmanager.install=false \
--set global.ingress.configureCertmanager=false \
--set global.ingress.tls.secretName=gitlab-tls \
--set nginx-ingress.controller.hostNetwork=true \
--set nginx-ingress.controller.service.type="" \
--set nginx-ingress.controller.kind=DaemonSet \
--set nginx-ingress.controller.service.externalTrafficPolicy="" \
--set gitlab-runner.install=false

完了すれば Gitlab 13.12 になっているはずです

トラブルシューティング

データベースが古いときに出るエラー

Gitlab12 から Gitlab13 にアップグレードする場合は PostgreSQL のバージョンが 10 -> 11 になります PostgreSQL の chart が古いバージョンのまま upgrade しようとすると以下のようなエラーが発生します

# kubectl logs -n gitlab gitlab-postgresql-0 -c gitlab-postgresql
2021-05-24 06:09:53.785 GMT [1] DETAIL:  The data directory was initialized by PostgreSQL version 10, which is not compatible with this version 11.7.

データベースのバックアップの取得に失敗する場合

バックアップ時に tar ファイルを作成するのですがそれを s3 (minio) にアップロードします minio へのアクセスはドメイン経由で行うので minio にアクセスすることができる A レコードが DNS に登録されていないとバックアップに失敗します

upgrade チェックをする方法

データベースのバージョンや upgrade パスを確認すことができます どこまで信頼できるかわかりませんが事前に実行して必要な条件を確認しておいても良いと思います

helm upgrade コマンドを実行すると upgrade チェック用の pod が起動するのでそれのログを確認すれば OK です

# kubectl logs -n gitlab gitlab-gitlab-upgrade-check-6b5gv
It seems you are upgrading the GitLab Helm Chart from 3.3.13 (GitLab 12.10.14) to 4.12.0 (GitLab 13.12.0).
It is required to upgrade to the latest 4.0.x version first before proceeding.
Please follow the upgrade documentation at https://docs.gitlab.com/charts/releases/4_0.html
and upgrade to GitLab Helm Chart version 4.0.x before upgrading to 4.12.0.

最後に

今回は 1 プロジェクトしかデータ移行していませんが環境によってはもっと多くのデータがあると思います その場合はバックアップとリストアにもっと多くの時間がかかるのと状況によってはバックアップするために別途ストレージの拡張が必要になるケースもあるかなと思います

また helm chart もデータベースが外部にあったりするとまた手順も変わってくるようです

手順的には簡単なのですがやはりトライアンドエラーが必要になるような気がします

参考サイト

2021年5月26日水曜日

Selenium でダイアログなしでファイルをダウンロードする方法

Selenium でダイアログなしでファイルをダウンロードする方法

概要

重要なのは Firefox を使わずに Chrome を使う点です

参考記事

環境

  • macOS 11.3.1
  • Ruby 3.0.0
    • selenium 4.0.0.beta3
  • Chrome

サンプルコード

一部抜粋です ポイントはプリファレンスを使って Chrome にオプションを渡すところです

download_pref = {
  'prompt_for_download' => false,
  'default_directory' => '/tmp',
  'directory_upgrade' => true
}
options = Selenium::WebDriver::Chrome::Options.new
options.add_preference(:download, download_pref)

@driver = Selenium::WebDriver.for :chrome, options: options

最後に

自分が試した限りこれならダイアログなしでファイルのダウンロードができました 念を押しますが Firefox はハマるので諦めて ChromeDriver を使いましょう

なお Selenium は localhost でも動作しましたし remote (docker) でも動作しました

2021年5月25日火曜日

ruby3 で hanami-router を動かす

ruby3 で hanami-router を動かす

概要

hanami-router を Ruby3 で動作させる場合には安定版ではなくプレリリース版を使う必要があります

環境

  • macOS 11.3.1
  • Ruby 3.0.0
    • hanami-router 2.0.0alpha5

インストール

  • bundle init
  • vim Gemfile
gem "hanami-router", "2.0.0.alpha5"
gem "thin"
  • bundle config path vendor
  • bundle install

アプリ作成

  • vim app.rb
require 'hanami/router'

app = Hanami::Router.new do
  get '/', to: ->(env) { [200, {}, ['Welcome to Hanami::Router!']] }
end

Rack::Server.start app: app, Port: 2300

動作確認

  • bundle exec rackup app.rb
  • curl localhost:2300

トラブルシューティング

安定版の hanami-router だと Unexpected error while processing request: undefined method unescape for URI:Module というエラーが発生します

これはこのコミットにより削除された unescape メソッドを hanami-router が依存している http_router という gem がまだ使っているため発生するエラーです

Ruby3 だと unescape メソッドが削除されているため動作しません

Ruby3

irb(main):002:0> URI.escape
Traceback (most recent call last):
        4: from /usr/local/opt/ruby/bin/irb:23:in `<main>'
        3: from /usr/local/opt/ruby/bin/irb:23:in `load'
        2: from /usr/local/Cellar/ruby/3.0.0_1/lib/ruby/gems/3.0.0/gems/irb-1.3.0/exe/irb:11:in `<top (required)>'
        1: from (irb):2:in `<main>'
NoMethodError (undefined method `escape' for URI:Module)

Ruby 2.6.5

irb(main):002:0> URI.escape
Traceback (most recent call last):
        6: from ./irb:23:in `<main>'
        5: from ./irb:23:in `load'
        4: from /Users/hawksnowlog/.rbenv/versions/2.6.5/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        3: from (irb):2
        2: from /Users/hawksnowlog/.rbenv/versions/2.6.5/lib/ruby/2.6.0/uri/common.rb:103:in `escape'
        1: from /Users/hawksnowlog/.rbenv/versions/2.6.5/lib/ruby/2.6.0/uri/rfc2396_parser.rb:300:in `escape'
ArgumentError (wrong number of arguments (given 0, expected 1..2))

2021年5月24日月曜日

Ruby + selenium + chrome のシンプルなサンプルコード

Ruby + selenium + chrom のシンプルなサンプルコード

概要

動作に必要な最低限の設定のみ紹介します

環境

  • macOS 11.3.1
  • Ruby 3.0.0
    • selenium-webdriver 4

gem インストール

  • bundle init
  • vim Gemfile
gem "selenium-webdriver", "4.0.0.beta3"

chromedriver のインストール

  • brew install chromedriver

サンプル

  • vim test.rb
require 'selenium-webdriver'

driver = Selenium::WebDriver.for :chrome
driver.get('https://www.yahoo.co.jp/')

実行

  • bundle exec ruby test.rb

2021年5月23日日曜日

Selenium + Firefox でファイルをダウンロードのダウンロードはできなさそう

Selenium + Firefox でファイルをダウンロードのダウンロードはできなさそう

概要

ブラウザでダウンロードする際にダウンロードするファイルをどうするかという確認のダイアログが表示されます これを Selenium + Firefox で消そうと思ったのですがどうやってもできなかったので紹介します

環境

  • macOS 11.3.1
  • Ruby 3.0.0
    • selenium-webdriver 4.0.0.beta3
  • Firefox 88

Ruby3 を使う場合は selenium-webdriver 4 が必要になります

インストール

  • bundle init
  • vim Gemfile
gem "selenium-webdriver", "4.0.0.beta3"
  • bundle install

プロファイルを指定して Firefox を起動すれば特定のファイルのダウンロードルールを使えるのでは

既存の Firefox でダウンロードしたいファイルを一度ダウンロードします その際に指定のファイルは次回以降同じ設定でダウンロードすることができるという機能があります

この機能を使ってプロファイルを作成しそのプロファイルを selenium で指定することでプロファイル付きの Firefox を起動することができるようになります

options = Selenium::WebDriver::Firefox::Options.new(args: ['-profile', '/path/to/profile'])

@driver = Selenium::WebDriver.for :remote, url: "http://#{host}:4444/wd/hub", options: options

一応公式でも手順が公開されてはいるのですがバグの回避策として紹介されている感じです 確かにプロファイル付きで Firefox は起動できるのですがそのあと selenium に接続することができません

どうやらプロファイル付きだと geckodriver が見つからなくなってしまうようで結局動作しません

調べてみると marionette を有効にすれば動作するという記事もみたのですが自分はダメでした

プロファイルを設定すればいけるという情報がかなりあったがダメだった

例えば以下のような感じでプロファイルを設定し Firefox ドライバを作成します これで localhost でも docker 上でも試したのですが結局ダイアログが出なくなることはありませんでした

profile = Selenium::WebDriver::Firefox::Profile.new
profile['browser.download.folderList'] = 2
profile['browser.download.useDownloadDir'] = true
profile['browser.download.dir'] = '/tmp'
profile['browser.download.lastDir'] = '/tmp'
profile['browser.download.downloadDir'] = '/tmp'
profile['browser.download.defaultFolder'] = '/tmp'
profile['browser.download.manager.showWhenStarting'] = false
profile['browser.download.manager.useWindow'] = false
profile['browser.download.manager.focusWhenStarting'] = false
profile['browser.download.manager.showAlertOnComplete'] = false
profile['browser.download.manager.closeWhenDone'] = true
profile['browser.download.viewableInternally.enabledTypes'] = ""
profile['browser.helperApps.alwaysAsk.force'] = false
profile['browser.helperApps.neverAsk.saveToDisk'] = 'text/plane,text/csv,application/x-ofx,text/comma-separated-values,application/octet-stream,application/vnd.ms-excel,application/x-gzip,text/txt,application/gzip,application/zip,text/html,txt,text'

options = Selenium::WebDriver::Firefox::Options.new(:profile => profile)

@driver = Selenium::WebDriver.for :remote, url: "http://#{host}:4444/wd/hub", options: options

原因を考える

もしかしたら自分の設定が足りていないだけかもしれないので少し原因を考えてみました

content-type が足りていない

browser.helperApps.neverAsk.saveToDisk にダイアログを表示しない content-tyep を記載するのですがそこにダウンロードするファイルの content-type がない可能性があります

しかし自分の場合はちゃんとブラウザなどで content-type を調べた上で記載したので漏れはないかなと思っています

handlers.json が優先されているのか

上記のプロファイルの記載よりもプロファイルフォルダ配下にある handlers.json の設定が優先されているのかもしれません むしろここに記載がないと意味がない可能性すらありそうです

なので今回プロファイル付きの Firefox を起動するようにしたのですが結局 selenium 経由では動作できませんでした 別の方法で handlers.json を上書きできればもしかしたら動作させることができるかもしれません

最後に

いろいろ試してみたのですが結局 Selenium + Firefox でダウンロードのダイアログを消すことはできませんでした

回避策としては chrome や edge ドライバを使うことかなと思います

参考サイト

2021年5月21日金曜日

flask と celery で循環参照を避けるコツ

flask と celery で循環参照を避けるコツ

概要

Python で循環参照は避けては通れないエラーだと思います 今回は flask + celery を例に循環参照のエラーが発生した際の回避策を紹介します

環境

  • macOS 11.3.1
  • Python 3.8.7
    • flask 2.0.0
    • celery 5.0.5

循環参照する例

Controller を View ベースのクラスにし Celery のワーカーを Task ベースのクラスにした場合に素直に作成すると以下のよう感じになると思います 以下のような依存関係が発生しているためアプリを起動できません

  • controllers -> tasks に依存
  • tasks -> proj に依存
  • proj -> controllers に依存

なお起動コマンドはアプリ、ワーカーそれぞれ以下の通りです

  • pipenv run celery -A tasks.tasks worker -l info
  • FLASK_APP=proj.my_flask pipenv run flask run

proj モジュール

  • vim proj/my_flask.py
from flask import Flask

from proj.make_celery import make_celery
from controllers.user_controller import UserController

app = Flask(__name__)
app.add_url_rule('/user', view_func=UserController.as_view('user_controller'))

app.config.update(
    CELERY_BROKER_URL='redis://localhost:6379',
    CELERY_RESULT_BACKEND='redis://localhost:6379'
)
celery = make_celery(app)
  • vim proj/make_celery.py
from celery import Celery

def make_celery(app):
    celery = Celery(
        app.import_name,
        backend=app.config['CELERY_RESULT_BACKEND'],
        broker=app.config['CELERY_BROKER_URL']
    )
    celery.conf.update(app.config)

    class ContextTask(celery.Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)

    celery.Task = ContextTask
    return celery

controllers モジュール

  • vim controllers/user_controller.py
from flask.views import View

from tasks.user_task import UserTask

class UserController(View):

    def dispatch_request(self):
        UserTask().delay()
        return "<html><body>username: hawksnowlog</body></html>"

tasks モジュール

  • vim tasks/user_task.py
from proj.my_flask import celery, app
from celery.log import get_default_logger

class UserTask(celery.Task):

    def __init__(self, default_username="hawk", default_age=10):
        self.name = "user_task" # required
        self.default_username = default_username
        self.default_age = default_age
        self.default_mail = "no-reply@mail.domain"
        self.logger = get_default_logger(__name__)

    def run(self, *args, **kwargs):
        self.logger.info(app.config["CELERY_BROKER_URL"])
        self.logger.info(kwargs.get("username", self.default_username))
        self.logger.info(kwargs.get("age", self.default_age))
        self.logger.info(kwargs.get("mail", self.default_mail))
  • vim tasks/tasks.py
from proj.my_flask import celery
from tasks.user_task import UserTask

celery.tasks.register(UserTask())

循環参照を解消してみる

いくつかの方法で循環参照を解消してみます

成功例その1: proj と controllers の依存関係を逆にする

proj と controllers の参照を逆にすることで循環させないようにしてみます

  • controllers -> tasks に依存
  • tasks -> proj に依存
  • proj -> controllers に依存
  • controllers -> proj に依存

コードの変更箇所は以下の通りです

  • vim controllers/user_controller.py
from flask.views import View

from tasks.user_task import UserTask
from proj import app

class UserController(View):

    def dispatch_request(self):
        UserTask().delay()
        return "<html><body>username: hawksnowlog</body></html>"

app.add_url_rule('/user', view_func=UserController.as_view('user_controller'))
  • vim proj/my_flask.py
from flask import Flask

from proj.make_celery import make_celery

app = Flask(__name__)

app.config.update(
    CELERY_BROKER_URL='redis://localhost:6379',
    CELERY_RESULT_BACKEND='redis://localhost:6379'
)
celery = make_celery(app)

これで依存関係は解消されてアプリは起動できますが add_url_rule が読み込まれないため意図した動作にはなりません なのでそれは起動コマンドを変更することで解消します

  • FLASK_APP=controllers.user_controller pipenv run flask run

成功例その2: tasks が proj に依存しないようにする

proj モジュールで flask アプリを管理するのではなく別のモジュール (root モジュール) で管理するようにします root モジュールからはどこへの依存も持たないため循環参照がなくなります

  • controllers -> tasks に依存
  • tasks -> proj に依存
  • tasks -> root に依存
  • proj -> controllers に依存
  • controllers -> proj に依存
  • controllers -> root に依存

コードの変更と追加箇所は以下の通りです root/make_celery.py は proj/make_celery.py を移動するだけで OK です

  • vim root/__init__.py
rom flask import Flask

app = Flask(__name__)
app.config.update(
    CELERY_BROKER_URL='redis://localhost:6379',
    CELERY_RESULT_BACKEND='redis://localhost:6379'
)
  • vim root/make_celery.py
from celery import Celery

def make_celery(app):
    celery = Celery(
        app.import_name,
        backend=app.config['CELERY_RESULT_BACKEND'],
        broker=app.config['CELERY_BROKER_URL']
    )
    celery.conf.update(app.config)

    class ContextTask(celery.Task):
        def __call__(self, *args, **kwargs):
            with app.app_context():
                return self.run(*args, **kwargs)

    celery.Task = ContextTask
    return celery
  • vim proj/my_flask.py
from root import app
from root.make_celery import make_celery

from controllers.user_controller import UserController

app.add_url_rule('/user', view_func=UserController.as_view('user_controller'))

celery = make_celery(app)
  • vim tasks/user_task.py
from root import app
from root.make_celery import make_celery

from celery.log import get_default_logger

celery = make_celery(app)

class UserTask(celery.Task):

    def __init__(self, default_username="hawk", default_age=10):
        self.name = "user_task" # required
        self.default_username = default_username
        self.default_age = default_age
        self.default_mail = "no-reply@mail.domain"
        self.logger = get_default_logger(__name__)

    def run(self, *args, **kwargs):
        self.logger.info(app.config["CELERY_BROKER_URL"])
        self.logger.info(kwargs.get("username", self.default_username))
        self.logger.info(kwargs.get("age", self.default_age))
        self.logger.info(kwargs.get("mail", self.default_mail))

user_task.py の celery の管理はもう少し良い方法があるかもしれません

make_celery.py は状況によっては proj 配下でも問題ないことがあります proj/__init__.py がありその中で flask の app を扱っている場合は移動したほうがよいでしょう

最後に

まとめると循環参照した場合に回避策としては

  • 参照方法を逆にする
  • どこにも参照しないモジュールとして切り出しそこを参照するようにする

あたりが回避策のポイントかなと思います

2021年5月20日木曜日

celery のタスクでクラスとして作成する方法

celery のタスクでクラスとして作成する方法

概要

過去に紹介してきた celery の使い方はすべて app を使ってタスクを定義してきました クラスベースで作成するといろいろと都合の良いことがあるので今回はクラスベースの celery タスクを作成する方法を紹介します

環境

  • macOS 11.3.1
  • Python 3.8.7
    • celery 5.0.5

インストール

今回は broker に redis を使うので redis ライブラリもインストールします

  • pipenv install celery redis
  • brew services start redis

celery オブジェクトの管理と生成

celery オブジェクトを管理するモジュールを作成します タスクを管理するパッケージとは別のパッケージにしたほう良いかなと思います

  • vim proj/my_celery.py
from celery import Celery

my_celery_app = Celery('test', broker='redis://localhost')

celery 自体の設定はここで行います

タスク作成

my_celery_app を元にタスクを作成します my_celery_app.Task を継承したクラスを作成しそこに run メソッドを実装します

今回は run しか実装していませんがエラーハンドリングなどを行う on_failure メソッドなどを実装すればエラーハンドリングのカスタマイズも可能です

  • vim tasks/user_task.py
from proj.my_celery import my_celery_app
from celery.log import get_default_logger

class UserTask(my_celery_app.Task):

    def __init__(self, default_username="hawk", default_age=10):
        self.name = "user_task" # required
        self.default_username = default_username
        self.default_age = default_age
        self.default_mail = "no-reply@mail.domain"
        self.logger = get_default_logger(__name__)

    def run(self, *args, **kwargs):
        self.logger.info(kwargs.get("username", self.default_username))
        self.logger.info(kwargs.get("age", self.default_age))
        self.logger.info(kwargs.get("mail", self.default_mail))

ポイント

いくつかポイントがあります まず name フィールドは必須のフィールドになります タスクとして登録し動作する際の名前になるので一意になるようなしましょう

また同じようにコンストラクタで設定するフィールド名の指定は celery.Task クラスの引数名と被らないようにしましょう https://docs.celeryproject.org/en/stable/reference/celery.app.task.html

カスタムタスククラスのコンストラクタでフィールドのデフォルト値を指定できます あとでタスクを実行しますがタスク実行時にもコンストラクタを呼べるのですがフィールドを変更することはできませんでした なのであくまでもデフォルト値として使うのが良いかなと思います

run は args と kwargs を引数に持ちましょう こうすることでタスクが呼ばれた際に指定された引数を受け取ることができるようになります

タスクの登録

作成した UserTask を登録します 今回はタスクをまとめて登録する専用ののモジュールを作成します

  • vim tasks/tasks.py
from proj.my_celery import my_celery_app
from tasks.user_task import UserTask

my_celery_app.tasks.register(UserTask())

my_celery_app.tasks.register を使って登録します ここで UserTask のコンストラクタが呼ばれるのでここでデフォルト値を上書きしても OK です

タスクの起動

ではタスクを起動しましょう

  • pipenv run celery -A tasks.tasks worker -l info

これで登録したすべてのタスクが起動できます キューごとにタスクを分けたい場合は tasks/tasks.py をキューごとに分割して起動すると良いかなと思います

動作確認

REPL で確認します UserTask を生成し delay メソッドなどで呼び出せば OK です 今回の場合だと UserTask 生成時の引数はすべて無視されます 値を変更したい場合は delay などのキーワード引数を使って書き換えます

  • pipenv run python3
>>> from tasks.user_task import UserTask

>>> UserTask().delay()
<AsyncResult: 9db21ce1-d63b-4e9a-88a2-0023e139fb11>

>>> UserTask().delay(username="user01", age=20, mail="user01@mail.domain")
<AsyncResult: 6f8dc3a1-18ad-4807-ad21-be3652cab1e1>
[2021-05-19 15:14:33,938: INFO/MainProcess] Received task: user_task[9db21ce1-d63b-4e9a-88a2-0023e139fb11]  
[2021-05-19 15:14:33,941: INFO/ForkPoolWorker-2] hawk
[2021-05-19 15:14:33,942: INFO/ForkPoolWorker-2] 10
[2021-05-19 15:14:33,942: INFO/ForkPoolWorker-2] no-reply@mail.domain
[2021-05-19 15:14:33,943: INFO/ForkPoolWorker-2] Task user_task[9db21ce1-d63b-4e9a-88a2-0023e139fb11] succeeded in 0.002128031999999891s: None

[2021-05-19 15:32:24,838: INFO/MainProcess] Received task: user_task[6f8dc3a1-18ad-4807-ad21-be3652cab1e1]  
[2021-05-19 15:32:24,841: INFO/ForkPoolWorker-2] user01
[2021-05-19 15:32:24,842: INFO/ForkPoolWorker-2] 20
[2021-05-19 15:32:24,843: INFO/ForkPoolWorker-2] user01@mail.domain
[2021-05-19 15:32:24,844: INFO/ForkPoolWorker-2] Task user_task[6f8dc3a1-18ad-4807-ad21-be3652cab1e1] succeeded in 0.0032719819999442734s: None

最後に

タスクが増えるとクラスが増えるのでそれが面倒な人は app.task デコレータなどを使ってどんどんタスクを定義するのが良いかなと思います

参考サイト

2021年5月19日水曜日

Python で型入門

Python で型入門

概要

Python にはタイプヒントという機能がありこれを使ってある程度の型指定を行うことができます 今回は型の指定方法や静的チェックを試してみました

環境

  • macOS 11.3.1
  • Python 3.8.7
    • mypy 0.812

Type Hints

あくまでもヒントとして型を記述することができます 例えば以下のような感じで引数や返り値に型を指定することができます

class User():
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def change_name(self, name: str):
        self.name = name

    def add_age(self, age: int) -> int:
        return self.age + age

    def show_profile(self):
        print(self.name)
        print(self.age)

user = User("hawk", 10)
user.change_name("snowlog")
user.add_age(2)
user.show_profile()

しかしあくまでもヒントでしかないので定義した型以外の型でも実行できてしまいます

user = User(-1, 10)
user.change_name(-2)
print(user.add_age("error"))
Traceback (most recent call last):
  File "test.py", line 23, in <module>
    print(user.add_age("error"))
  File "test.py", line 10, in add_age
    return self.age + age
TypeError: unsupported operand type(s) for +: 'int' and 'str'

事前に型チェックする

mypy というツールを使うと事前に型チェックできるため上記のように型が間違っている場合にエラーとなっている箇所を見つけることができます

  • pip3 install mypy

これで先程のコードに対してチェックすると型が違っているというエラーを確認できます

test.py:21: error: Argument 1 to "User" has incompatible type "int"; expected "str"
test.py:22: error: Argument 1 to "change_name" of "User" has incompatible type "int"; expected "str"
test.py:23: error: Argument 1 to "add_age" of "User" has incompatible type "str"; expected "int"
Found 3 errors in 1 file (checked 1 source file)

型エイリアスを使う

プリミティブな型を独自の型名として使うことができます あくまでも元の型として扱われます コードのリファクタなどに使えます

my_int = int

class User():
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def change_name(self, name: str):
        self.name = name

    def add_age(self, age: int) -> my_int:
        return self.age + age

    def show_profile(self):
        print(self.name)
        print(self.age)

user = User("hawk", 10)
print(user.add_age(2).__class__)

NewType を使う

新しい型を定義します エイリアスとは異なり実際に型チェックに引っかかります

from typing import NewType

my_int = int
Name = NewType('Name', str)
Age = NewType('Age', int)

class User():
    def __init__(self, name: str, age: int):
        self.name = name
        self.age = age

    def change_name(self, name: Name):
        self.name = name

    def add_age(self, age: Age) -> my_int:
        return self.age + age

    def show_profile(self):
        print(self.name)
        print(self.age)

user = User("hawk", 10)
name = Name("snowlog")
user.change_name(name)
age = Age(2)
print(user.add_age(age))
user.show_profile()

もし以下のようにちゃんと型を生成してから指定していない場合は型チェックでエラーになります

user = User("hawk", 10)
# name = Name("snowlog")
user.change_name("snowlog")
age = Age(2)
print(user.add_age(age))
user.show_profile()
test2.py:24: error: Argument 1 to "change_name" of "User" has incompatible type "str"; expected "Name"
Found 1 error in 1 file (checked 1 source file)

その他

ジェネリクス、Any 型、Optional 型などもあるのでかなり正確に型付けを行うことができます

参考サイト

2021年5月18日火曜日

Ruby3 で型入門

Ruby3 で型入門

概要

Ruby3 で導入された rbs, typeprof を試してみました 厳密には型を記述できるわけではなく型を事前に推論してくれる機能になります

環境

  • macOS 11.3.1
  • Ruby 3.0.0
    • steep 0.44.1

テストコード

まずは普通に Ruby のコードを書きます このコールの引数や戻り値の型を抽出することができます

  • vim user.rb
class User
  def initialize(name, age)
    @name = name
    @age = age
  end

  def fake_name
    @name + "_jose"
  end

  def fake_age
    @age + 1
  end
end

型を抽出してみる

定義したクラスの型を推論、抽出するには typeprof というコマンドを使います 試しに実行してみます

  • typeprof user.rb
# Classes
class User
  @name: untyped
  @age: untyped

  def initialize: (untyped name, untyped age) -> untyped
  def fake_name: -> untyped
  def fake_age: -> untyped
end

型がわからなかった場合には untyped と表示されるようです どうやら実際にクラスのオブジェクトを生成してメソッドを実行していないと型判定できないようです

ちょっと修正: ちゃんとコールしたり、initizlize で返り値を設定する

先程の user.rb を実行するコードを追記してみます

  • vim user.rb
class User
  def initialize(name, age)
    @name = name
    @age = age
  end

  def fake_name
    @name + "_jose"
  end

  def fake_age
    @age + 1
  end
end

user = User.new("hawk", 10)
puts user.fake_name
puts user.fake_age

これで再度 typeprof してみるとちゃんと引数と戻り値に型が設定されています

  • typeprof user.rb
# Classes
class User
  @name: String
  @age: Integer

  def initialize: (String name, Integer age) -> Integer
  def fake_name: -> String
  def fake_age: -> Integer
end

しかし initizlize の返り値がまだおかしいです 本来は User クラスのオブジェクトが返却されるべきですが Integer が返却されてしまっています

この場合は initialize で self を返すようにすれば OK です 以下のように修正しましょう

  • vim user.rb
class User
  def initialize(name, age)
    @name = name
    @age = age
    self
  end

  def fake_name
    @name + "_jose"
  end

  def fake_age
    @age + 1
  end
end

user = User.new("hawk", 10)
puts user.fake_name
puts user.fake_age

これで再度 typeprof するとちゃんと目的の型抽出ができるようになっていると思います

  • typeprof user.rb
# Classes
class User
  @name: String
  @age: Integer

  def initialize: (String name, Integer age) -> User
  def fake_name: -> String
  def fake_age: -> Integer
end

これが typeprof の機能になります

rbs ファイルを生成する

次に rbs ファイルを生成してみます と言っても先程実行した typeprof の実行結果を rbs ファイルとして保存するだけです

  • typeprof user.rb > user.rbs

rbs ファイルを使って型チェックを行う

steep というツールを使います グローバルインストールで良いと思います

  • gem install steep

自分は mac + homebrew の Ruby を使っていたのですが以下も必要でした

  • ln -s /usr/local/lib/ruby/gems/3.0.0/bin/steep /usr/local/opt/ruby/bin/steep

インストールできたら init します

  • steep init

Steepfile という型チェックを行うルールを記載する DSL ファイルが作成されるので編集します

  • vim Steepfile
target :app do
  check "."
  signature "."
end

check は user.rb があるディレクトリを指定し signature は user.rbs ファイルがあるディレクトリを指定します 今回はどちらも同じカレントディレクトリにあるのでカレントを指定します

これで実行してみましょう

  • steep check
# Type checking files:

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

No type error detected. 🫖

こんな感じでエラーにならなければ OK です

引数の型を変更してエラーを発生させてみる

わざと型を間違えてエラーを発生させてみます user.rb を書き換えてみましょう

  • vim user.rb
class User
  def initialize(name, age)
    @name = name
    @age = age
    self
  end

  def fake_name
    @name + "_jose"
  end

  def fake_age
    @age + 1
  end
end

user = User.new(-1, 10)
puts user.fake_name
puts user.fake_age

本来は String である必要がある引数を -1 として Integer を指定してみます これで steep check を再度実行してみましょう

# Type checking files:

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

lib/user.rb:17:16: [error] Cannot pass a value of type `::Integer` as an argument of type `::String`
│   ::Integer <: ::String
│     ::Numeric <: ::String
│       ::Object <: ::String
│         ::BasicObject <: ::String
│
│ Diagnostic ID: Ruby::ArgumentTypeMismatch
│
└ user = User.new(-1, 10)
                  ~~

Detected 1 problem from 1 file

こんな感じでエラーになるのが確認できると思います

使い所は

単純に引数や帰り値で型を明確に指定したいときには当然使えます

それ以外でぱっと思いつくところだとエディタ上で型チェックするときや CI に組み込んで型チェックするときかなと思います

vscode であれば steep と組み合わせて型チェックできるプラグインがあるようです https://github.com/soutaro/steep-vscode

最後に

rbs ファイルは自動生成してくれますが ruby スクリプトと側の記述によっては意図していない型定義を出力してしまう必要があることを考慮すると実質 rbs ファイルもメンテナンスしていく必要があるかなと思います

インタフェースを定義している感覚であればそこまで負担にはならないかなと思いますが規模が大きくなると当然コストも増えるかなと思います

すべてのクラスで rbs ファイルを作成しないで特定のクラスだけで使うのはありかもしれません

参考サイト