30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

もうすぐ 40 歳になるが労働を 3 年以上続けられたことがない IT エンジニアの話

「30歳からのプログラミング」と題したこのブログを書き始めたのが 2016 年 3 月。
そこから月日が立ち、立派なアラフォーとなったわけだが、私はこれまで 3 年以上継続して働いたことがない。プログラマに転身する前も含めて、である。一度もない。
3 年経つ前に、必ず無職になってしまう。労働して貯めた貯金を食い潰しながら無職生活を送り、カネが無くなりそうになってまた働く、ということを繰り返している。

だが、今の勤務先(株式会社HERP)に入社したのは 2021 年 10 月 1 日であり、入社してからもうすぐ 3 年になる。
つまり、 3 年以上労働を続けることになる可能性が高い。
仮にこの記事を投稿した直後に退職を決意したとしても、引き継ぎや有給休暇の消化などで、さすがに 9 月末までは在籍していると思う。そうなれば 3 年到達である。

今までの会社を辞めてきた理由は様々だ。同様に、今の会社に在籍し続けている理由も、複合的なものである。

ひとつ大きいのは、やりたいことをやれており、それを楽しめているということ。
以前はそもそも、何をやりたいのか、ソフトウェアエンジニアとしてどうなりたいのか、分からなかった。その結果、新しい技術や流行りの技術を覚えなければいけない、市場価値を上げなければいけない、もっと勉強しなければいけない、という焦燥感に駆られ、疲弊したりしていた。
今の環境では働きながら内省を深めることができるので、自分は何をしたいのか、何に夢中になれるのか、何に情熱を持てるのか、折に触れて考えるようにしていた。そうすると、今の自分がやりたいこと、やってみたいことが、見えてきた。

単純に言えば、「どう作るか」だけでなく「何のために何を作るのか」から考え、戦略を立て、それを実行することによって、大きな成果を生み出したい。それによって事業や組織を次の段階に進められる、そういう人になりたい。それがやりたいことだった。
以下の記事に書かれている内容が、自分のやりたいことに近い。

onk.hatenablog.jp

そもそも何が問題なのかを明らかにするだとか、最適な解決策を見つけるのが、仕事をしていて一番面白いところだと僕は思っている。
(中略)
やるだけになっているものを進めるのでも十分難しいとは思うが、誰も明らかにできていなかった問題に対して自分が輪郭を削り出すことに成功するのは最高に気持ちいい。
(中略)
目標や成果物を明らかにし、それを達成するための戦略やアプローチを定義するプロセスは、戦略的な思考を活かすことができ、自己効力感がある。

もうひとつ、やりたいこと、というか好きなことがある。
それは、仕組みや機構を理解すること。仕組みが動作している背景にあるルールや原則を見出し、要素と要素の関係性を掴み、なぜどのような仕組みで動作しているのか理解したい。そういうことに、強い関心がある。これは何か理由があるわけではなく、理解することそれ自体に強い関心を持っている。
自分がプログラミングのことを好きな理由のひとつでもあると思う。それから、このブログも、「理解の証明」として書いているという側面がある。読んでいる人に分かりやすく説明するためには、まず自分が理解していないといけない。自分が理解しているからこそ、適切な構成で説明を記述していける。だから自分にとってブログは、理解していることを証明しようとして書いているもの、と言える。

自分がやりたいことが分かったので、そういうことをしたいと、社内の 1on1 などで話すようにしていた。
そのおかげかどうかは分からないが、課題を発見しそれを解決するための戦略を立て実行していく、ということを明示的に求められるロールにアサインされた。
今年の春から、社内のデータプラットフォームを構築し運用するチームのリーダーになった。

先ほど紹介した記事、が紹介しているスライドの中に「高い山の中腹ではなく、低い山の頂上に立て」という話があるが、まさにそれをやれそうだった。今の自分には全社的なことを見て考える能力はないので、まずはチーム単位でやってみようと思った。

仕組みを理解したい、そして理解したことを言語化したい、という志向とも、このロールは相性がいいと思っている。

リーダーになって半年ほど経ったが、成功したり失敗したりしながら、楽しめている。
四半期の頭に自分が決めた方針が妥当なものであり、それが成果を生んでいたら、満足感がある。と同時にそれをもっと速く推進できていれば防げた失敗もあり、今度は「どこに向かうか」だけでなく「チームとしてどうやってそこに速く向かうか」も考える必要が出てくる。
こんな風に、考えることは山程あり、楽しくやれている。

楽しくやれているのは、リーダーというロールにアサインしてもらったからだけではない。
会社の文化によるところも大きい。

HERP に入社した際に、以下の記事を書いた。

numb86-tech.hatenablog.com

この記事で自分は、企業文化や透明性こそが大切であり、そして HERP は健全であり、柔軟な組織なのではないかと書いた。
当たっていた。HERP は透明性があり、政治や忖度はなく、仕事について誠実に会話ができる。
そして柔軟性もある。私も変わったが、組織も変わった。まだまだ多くの問題を抱えてはいるが、変わることのできる組織である。

先程の記事には、「コンプレックスのおかげでここまでやってこれたけど、今後はもっと前向きで健全なモチベーションでやっていきたい」とも書いた。
既に書いたように楽しくやれており、前向きなエネルギーで働くことができている。

そしてこれは、今まで働いてきた環境では絶対に不可能だったと思っている。
特に、新卒で入った信用金庫では、本当に上手くいかなかった。
セクハラや暴力が横行している時点で論外だったのだが、それらがなくても、明らかに向いていなかった。根性と愛嬌、政治と人脈、という世界だった。あそこにいたとき、成果は出せなかったし、評価もされなかったし、何より全く楽しくなかった。

今の会社に来たからこそ、多少なりとも能力を発揮し、少しでも大きなインパクトを生み出すべく、楽しく働けている。

しかし、皆が皆、自分に合った、自分のポテンシャルを発揮できるような、そういう環境に辿り着けるわけではない。
特に、学歴に恵まれず、20 代でのキャリア形成が上手くいかなかった人の場合、どうしても選択肢に強い制限が生まれるのではないかと思っている。採用市場というのは、強者がより強者になる、という構造になりがちだと思う。ネームバリューのある会社でよい経験を得られた人は、それを武器にして、さらにステップアップしていく。それを繰り返していく。一方で、そのサイクルに入り損ねてしまうと、経験や能力を獲得する機会を得づらくなり、それゆえに経験や能力を得られる環境に移ることが難しくなる、という悪循環に陥ってしまう。

私自身も、 10 代の頃は引きこもっていて高校を中退しており、大検(今は「高卒認定試験」)を取得して比較的入りやすい大学に入り、新卒で入った信用金庫は精神を病んで 3 年持たずに辞めて、そのまま 1 年近く無職をやっていた。こういう状態になってしまうと、なかなか難しいと感じる。
そんな自分が、有名大学出身で著名な企業で働いてきた人が多い今の会社で働いているのは、正直なところ「運」であり「偶々の巡り合わせ」だと思っている。

運や偶然ではなく、それを望んだ誰もが、自分に合った環境、楽しく働ける環境に移っていけるようにしたい。学歴や職歴がガタガタだろうが、性別がなんだろうが、家庭環境がどうだろうが、そういったことは関係なく、その人のポテンシャルを発揮できる場所で働ける社会にしたい。

どうやればいいのか、そもそもそんなことが可能なのか、よく分からない。
だがそういう社会が実現できたら素晴らしいと思う。少しでもそういう世界に近づけたいと思う。

HERP がやろうとしているのはそういうことだと、私は解釈している。
採用を変えて、出会うべき人と企業が出会える社会を実現しようとしている。そのために、「採用」に関する様々なサービスを提供している。

そして私はまず、データという観点から、そういった社会の実現に貢献すべく働いている。
データが貢献できる余地はとても大きくて、データを活用することでよりよいプロダクトやカスタマーサクセスを提供していけるし、新鮮で質の高い大量のデータを管理しているからこそ提供可能な事業やサービスというものもある。

そしてこの環境もまた、誰かにとってポテンシャルを発揮できる環境だと思っている。

課題を発見し、それを解決するための戦略を立て、関係者を巻き込みながら解決案を実行していく、というのは何もリーダー特有の仕事ではない。
少なくとも今データチームが求めているのは、そういったことにオーナーシップを持って取り組んでくれる人材である。

それが貴方にとって楽しいかは分からないけど、私は楽しい。
興味が湧いたのなら応募して欲しいし、応募する意思はないが話を聞いてみたいのであれば私に連絡して欲しい。

herp.careers

https://x.com/numb_86

そして、データエンジニア以外の職種もたくさん募集しているので、「データ」に強い関心はないが HERP のミッションや文化に興味を持った、という方がいれば、ぜひ応募を検討してみて欲しい。

herp.careers

target や config を理解して dbt model の出力先を制御できるようになる

dbt を使おうとすると、profile や target、config、property など、様々な概念が出てくる。
それらをあまり理解できていなくても、何となく動かすことはできるかもしれない。
しかし、これらの概念を理解していないと、意図した通りに動かしたり他者が記述した設定内容を理解したりするのは難しい。

この記事では、最初は理解が難しいこれらの概念について、「model の出力先の設定」を題材にして説明していく。
dbt runした際にどこにテーブルやビューが出力されるのか、設定を読み解いて理解できるようになることを目指す。

この記事の内容は dbt のバージョン1.8.5で動作確認している。
データウェアハウスは BigQuery を使用する。

概要

どのデータウェアハウスのどのテーブルにあるデータを使うのか、データの出力先はどこになるのか。
そういった設定には「profile による基本設定」と「config による個別設定」の 2 つがあり、個別設定によって基本設定を上書きすることができる。
この仕組みによって、特定の model についてだけ出力先を変える、といったことが可能になる。

まずは基本設定から見ていく。

基本設定

dbt では profile という概念によって、データウェアハウスとの接続に関する情報を扱う。
そしてその profile について記述するためのファイルが、profiles.yml。このファイルに、どのデータウェアハウスを使うのか、どのデータベースを使うのか、といったことを記述していく。

以下は、データウェアハウスとして BigQuery を使い、dbt-project-a-432612というプロジェクトのdestデータセットに model を出力する場合のprofiles.yml

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery # 接続先のデータウェアハウスの種類
      method: oauth # 認証に使う方法
      project: dbt-project-a-432612 # 使用するプロジェクト
      dataset: dest # 使用するデータセット

ひとつの profile は、ひとつ以上の target を持つ。データウェアハウスについての具体的な情報は target に書く。target はprofiles.yml<profile-name>.outputsに書いていく。
上記の例では、devという target を定義している。
詳細は後述するが、ひとつの profile のなかで複数の target を定義できるため、デフォルトで使う target を指定しておくことができる。profiles.yml<profile-name>.targetで指定する。
上記の例ではtarget: devがそれにあたる。

上記の例では target で 4 つの項目を設定している。
このうちmethodは認証に関するものであり、この記事の内容との関係は薄いので割愛する。
詳細は、公式ドキュメントの以下のページで確認できる。
https://docs.getdbt.com/docs/core/connect-data-platform/bigquery-setup

重要なのはprojectdatasetである。これで、接続先を指定している。
項目名はデータウェアハウスによって異なるので注意する。例えば Snowflake の場合はdatabaseschemaを使って設定を行う。
https://docs.getdbt.com/reference/dbt-jinja-functions/target

projectは、データの出力先として使われるだけでなく、source の参照先としても使われる。
例えば以下のように source を定義してfrom {{ source('src', 'user') }}とする場合、dbt-project-a-432612プロジェクトのsrcデータセットのuserテーブルが使われる。

version: 2

sources:
  - name: src # dataset を省略した場合は name が dataset として使われる
    tables:
      - name: user
        columns:
          - name: id
          - name: name

dbt-project-a-432612.src.userを用意し、それを参照しているfooという model を書いて$ dbt run -s fooを実行してみると……。
エラーになる。

$ dbt run -s foo
15:48:52  Running with dbt=1.8.5
15:48:52  Encountered an error:
Runtime Error
  No dbt_project.yml found at expected path <カレントディレクトリのパス>/dbt_project.yml
  Verify that each entry within packages.yml (and their transitive dependencies) contains a file named dbt_project.yml

dbt runを実行するためには、dbt_project.ymlを用意する必要がある。そして、dbt_project.ymlで、どの profile を使うか指定しなければならない。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1' # profiles.yml で定義した my_profile_1 を指定

nameversionに、この dbt プロジェクトの名前とバージョンを指定する。config-version2にする決まり。
そしてprofileキーに、使用する profile を指定する。

この状態で$ dbt run -s fooすると今度は上手くいき、dbt-project-a-432612.dest.fooというビューが作られる。

target の使い分け

ひとつの profile に対して、複数の target を定義できる。
以下の例では、先ほど定義したdevに加えて、prodという target を定義した。そしてprodではdbt-project-b-432615プロジェクトを使うと定義している。

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-a-432612
      dataset: dest
    prod: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-b-432615
      dataset: dest

dbt runする際に--targetフラグを使うことで、使用する target を指定できる。
例えば$ dbt run -s foo --target prodを実行すると、dbt-project-b-432615.dest.fooビューが作られる。その際はもちろん、dbt-project-b-432615.src.userが source として使われる。
--targetフラグを使わなかった場合は、<profile-name>.targetで指定した target (上記の例だとdev)が使われる。
--targetフラグを使わず<profile-name>.targetも定義されていなかった場合はエラーになる。

$ dbt run -s foo
16:57:30  Running with dbt=1.8.5
16:57:30  target not specified in profile 'my_profile_1', using 'default'
16:57:30  Encountered an error:
Runtime Error
  The profile 'my_profile_1' does not have a target named 'default'. The valid target names for this profile are:
   - dev
   - prod

config

model 毎に、その出力先を設定することができる。
その仕組みを理解するためにはまず、config という概念を理解する必要がある。

model に対しては property を設定できる(model だけでなく seed や test などにも property を設定できるが、この記事では割愛する)。
そして property の中には、 config と呼ばれる特殊な property がある。

config は以下の 3 つの方法で設定できる。

  1. config()関数
  2. 個別の model について記述した.ymlファイル
  3. dbt_project.yml

上記の順番で優先度が高く、ひとつの config に対して複数の方法で設定されていた場合、優先度が高い方法で設定した内容が採用される。
詳細はこれから具体例を用いて説明していくので、config には複数の設定方法がある、優先順位が決まっている、ということをまずは覚えておけばよい。

model に設定できる config のうち、model の出力先に関係するのがdatabaseschema
BigQuery の場合はprojectdatasetという config も設定できる。それぞれ、databaseschemaと互換性がある。
BigQuery の用語(プロジェクトとデータセット)と合わせたほうが分かりやすいので、この記事ではprojectdatasetを使うことにする。

dbt_project.yml で config を設定する

まずはdbt_project.ymlproject config を使ってみる。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される

model に対する config はmodels:の下に書いていく。まずは project の名前を書き、その下に具体的な設定を書いていく。
今回はmartsディレクトリ以下の model に対して config を設定したいので、まずはmartsと書く。そして、+<config名>: 設定内容を書く。上記の例ではproject config にdbt-project-xを設定している。

こうすると、martsディレクトリ以下にある model はdbt-project-xに出力されるようになる。

例えばmodels/の中身が以下のときに$ dbt run --target devすると、dbt-project-a-432612.dest.foodbt-project-x.dest.barが作られる。

models
├── foo.sql
├── marts
│   └── bar.sql
└── sources.yml

target としてdevを指定したので、profiles.ymlに記した内容に基づき、接続先はdbt-project-a-432612.destになる。
しかしmarts以下のディレクトリについてはdbt-project-xに出力するようにdbt_project.ymlに記したので、その設定が優先される。datasetについてはdbt_project.ymlで特に設定していないので、devで設定したdestがそのまま使われる。
そのため、marts/bar.sqlの実行結果がdbt-project-x.dest.barに書き込まれるのである。

次はdatasetも使ってみる。
dbt_project.yml+dataset: my_suffix_1を書き加える。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される
      +dataset: my_suffix_1 # target.dataset に _my_suffix_1 という接尾辞をつけたデータセットに出力される

この状態で$ dbt run --target devすると、dbt-project-a-432612.dest.foodbt-project-x.dest_my_suffix_1.barが出力される。
my_suffix_1というデータセットに出力されるのではなく、target.dataset(今回の場合dest)とmy_suffix_1_でつないだデータセットに出力されるので、注意が必要。

model のprojectdataset config で設定しているのはあくまでも、「model の出力先の設定」である。「source としてどのデータベースを参照するか」には影響を与えない。
そのためのfoobarも、from {{ source('src', 'user') }}している場合はdbt-project-a-432612.src.userを参照するので注意する。

config を入れ子にする

以下のように config をネストさせていくことも可能。

name: 'my_dbt_project'
config-version: 2
version: '1.0.0'

profile: 'my_profile_1'

models:
  my_dbt_project: # name で指定した project の名前を書く
    marts: # marts ディレクトリ以下にある model は……
      +project: dbt-project-x # dbt-project-x に出力される
      +dataset: my_suffix_1 # target.dataset に _my_suffix_1 という接尾辞をつけたデータセットに出力される
      special: # marts/special ディレクトリ以下にある model は……
        +dataset: my_suffix_2 # target.dataset に _my_suffix_2 という接尾辞をつけたデータセットに出力される

このように書くと、marts/specialディレクトリ以下にある model はdest_my_suffix_2データセットに出力されるようになる。

marts/special/baz.sqlを用意して確認してみる。

models
├── foo.sql
├── marts
│   ├── bar.sql
│   └── special
│       └── baz.sql
└── sources.yml

この状態で$ dbt run --target devすると、以下のビューが作られる。

  • dbt-project-a-432612.dest.foo
  • dbt-project-x.dest_my_suffix_1.bar
  • dbt-project-x.dest_my_suffix_2.baz

specialで設定したのはdatasetのみなので、projectmartsで設定したdbt-project-xmarts/special/baz.sqlにも適用される。

個別の model について記述した .yml ファイルで config を設定する

model と同じディレクトリに.ymlファイルを用意し、そこに個別の model に対する property を記述することができる。
config も property の一部なので、projectdatasetもそのファイルに書くことができる。

例として、models/marts/special/qux.sqlという model を用意し、その model について記述したmodels/marts/special/qux.ymlを用意する。
models/marts/special/qux.ymlには以下の内容を書く。

version: 2

models:
  - name: qux
    config:
      project: dbt-project-y

quxprojectdbt-project-yにしている。
datasetは特に上書きしていないので、dbt_project.ymlの内容がそのまま使われる。qux.sqlmarts/special以下にあるので、今回の例だとmy_suffix_2になる。
その結果、models/marts/special/qux.sqlの内容はdbt-project-y.dest_my_suffix_2.quxに出力される。

このように、個別の model について記述した.ymlファイルで設定した内容は、dbt_project.ymlの内容よりも優先される。

config() 関数で config を設定する

最後に、最も優先順位が高いconfig()関数を使った方法について述べる。

先ほど用意したmodels/marts/special/qux.sqlの内容を以下のようにする。

{{ config(
  project='dbt-project-z'
) }}
select name from {{ source('src', 'user') }}

そうすると、出力先はdbt-project-z.dest_my_suffix_2.quxになる。
このように、.ymlで定義した内容(今回の例だとproject: dbt-project-y)よりも、config()関数の内容(今回の例だとproject='dbt-project-z')が優先される。

generate_database_name と generate_schema_name

ここまで、model の出力先がどのように決まるのか見てきたが、その挙動は実は macro によって制御されている。
dbt には予めgenerate_database_nameという macro とgenerate_schema_nameという marco が用意されている。
そしてこれらの macro はdbt runなどを実行したときに暗黙的に呼び出され、そこに書かれているロジックによって macro の出力先が決まる。

そしてどちらの macro も、自分で定義して既存の振る舞いを上書きすることができる。

generate_database_name

generate_database_nameは、出力先の database を決めるための macro 。projectdatabaseと互換性があるため、databaseはそのままprojectに読み替えてしまって問題ない。
デフォルトの実装は以下のようになっている。

{% macro generate_database_name(custom_database_name=none, node=none) -%}

    {%- set default_database = target.database -%}
    {%- if custom_database_name is none -%}

        {{ default_database }}

    {%- else -%}

        {{ custom_database_name | trim }}

    {%- endif -%}

{%- endmacro %}

引数のcustom_database_nameは、 model に設定されたdatabase(もしくはproject) config 。
これが設定されない場合はdefault_database、つまりtarget.database(BigQuery の場合はtarget.project)が使われる。
custom_database_nameが設定されている場合は、それがそのまま(trimだけして)使われる。

このように、この記事でここまで説明してきた挙動は、この macro によって定義されているのである。

generate_schema_name

generate_schema_nameも同様に、出力先の schema を決定するロジックを定義している。datasetschemaと互換性があるため、schemaはそのままdatasetに読み替えてしまって問題ない。
デフォルトの実装は以下。

{% macro generate_schema_name(custom_schema_name, node) -%}

    {%- set default_schema = target.schema -%}
    {%- if custom_schema_name is none -%}

        {{ default_schema }}

    {%- else -%}

        {{ default_schema }}_{{ custom_schema_name | trim }}

    {%- endif -%}

{%- endmacro %}

独自定義によるオーバーライド

macros/generate_database_name.sqlあるいはmacros/generate_schema_name.sqlを定義することで、挙動を上書きすることができる。

例として、以下の内容でmacros/generate_database_name.sqlを作成して、出力先の database(もしくは project)を決めるロジックを自分で定義してみる。

{% macro generate_database_name(custom_database_name=none, node=none) -%}

    {{ 'dbt-project-y' }}

{%- endmacro %}

このように定義してdbt runすると、target や config でprojectをどのように設定していても、その設定は使われず、全ての model がdbt-project-yに出力されるようになる。

source の接続先

projectdatasetは source にも設定できる。
しかし source に対してはgenerate_database_namegenerate_schema_nameが実行されることはなく、projectdatasetに設定した値がそのまま使われる。
そのため、以下のように設定した状態でfrom {{ source('src', 'user') }}とすると、dbt-project-b-432615.src.userが参照される。

version: 2

sources:
  - name: src # dataset を省略した場合は name が dataset として使われる
    project: dbt-project-b-432615
    tables:
      - name: user
        columns:
          - name: id
          - name: name

target によって参照先を変えるということも可能。
以下のようにすると、devのときはdbt-project-a-432612を、prodのときはdbt-project-b-432615を参照するようになる。

version: 2

sources:
  - name: src
    project: |
      {%- if  target.name == "dev" -%} dbt-project-a-432612
      {%- elif target.name == "prod" -%} dbt-project-b-432615
      {%- else -%} invalid_database
      {%- endif -%}
    tables:
      - name: user
        columns:
          - name: id
          - name: name

そして例えばprofiles.ymlを以下のようにしておくと、--target devdbt runしたときはdbt-project-a-432612.src.userを参照してdbt-project-xに model を出力し、--target prodのときはdbt-project-b-432615.src.userを参照してdbt-project-yに model を出力するようになる。
config で model の出力先を設定している場合は、もちろんそれが採用される。

my_profile_1: # profile の名前
  target: dev # デフォルトで使う target の名前
  outputs:
    dev: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-x
      dataset: dest
    prod: # target の名前
      type: bigquery
      method: oauth
      project: dbt-project-y
      dataset: dest

参考資料