かなで技術日誌

プログラミングやエンジニアリング周りについて

主なアウトプットはScrapboxObsidianにまとめてます。

F欄大学文系からソフトウェアエンジニア6年目の振り返りと現在

2023年Advent Calendar集まれ文系エンジニア!25日目の投稿です。

この記事は自分自身が真のF欄大学を、それも新卒就活をせずに現在6年目のソフトウェアエンジニアとして生き延びている軌跡を書き記したものです。

これまでの自分のやってきたことと現在地を踏まえ、それぞれのフェーズでどういったことを気にして生き延びてこられたかを考えようと思います。

とりあえずAdvent Calendar書こうと思って書いたのでちょっと(?)散らかっているかもしれません。

一部でも誰かの参考になれば幸いです。

前提

まず私がどのような生い立ちで人間であるかです。

  • 東京都23区の西の端が実家
  • 実家は中ぐらい?(リーマンショック以後自分が大学生の頃は大体700ぐらい、その前は+200ぐらいはあったみたい)
  • 父がエンジニア(ただし何も教わっておらず、自分がエンジニアになるまで職業を知らなかった)
  • 母はフルタイム→弟が生まれた後はパートタイムで介護職とかやっている
  • 中学は少しだけ勉強した(自称進学校ぐらい)
  • 高校は本当に勉強せず無事F欄大学へ入学
  • 色々あり新卒就活せず
  • 1年フリーター後に大学院受験で落ちてエンジニアとして就職

ここで東京都23区に実家というのを見て帰らないでください!確かに実家が東京23区内にあったのは幸運でしたが、それとは別にも参考になることもあるかもしれないのでブラウザバックせず見てもらえればと思います。

大学生から現在に至るまで

大学生

高校生であまりにも勉強しなかった結果、ほぼ全て落ちてF欄大学に行くこととなりました。たまたま前職で一人先輩がいました。

www.google.com

入学当初は二人ほどたまに話す程度の知り合いはいたものの、当時はインターネットで知り合ったヲタクとずっと遊戯王で遊んでいたのもありだんだん疎遠になって最終的には大学の知人は文字通りゼロになりました。 そんなこともあり大学に馴染めず、適応障害になって授業に行けなくなった時期もありましたが、なんとか治療していけるようになり卒業しました。

ひどい大学でしたが学問を修めると言う観点では様々な気づきと知見を得ることができました。ここでの学びからクルーグマン教授の経済入門を読んで、以降経済学にを研究したいというモチベーションへとつながることとなりました。

しかしその時期が大学3年も終わりの時期で、すぐ院試を受けるか就職するかでしたが

  • 就職したところでこんなF欄大学の新卒の行き場なんてよくわからない企業に行く羽目にしかならなそう

  • そもそも就職とかしたくないと言う甘え

  • 変に就職するぐらいならある程度時間が守られるバイトをしながら学費を稼いで、その間に勉強もして受験した方がいいのでは

と言う理由と、1年~2年あたりの適応障害で相当数の単位を落としたのが理由で単位も取りまくって卒研もやらなければならない事情もあり、卒業後に諸々やって行くこととなりました。

フリーター時代

全く自分では遊ばないけど時給がいいのでパチ屋のアルバイトをしていました。朝もそこそこ早いのと慣れると楽、忙しい時間帯はほぼフロアを走っていることもあるので運動不足解消にも一役買っていたかもしれません。

この時期はバイトをしない時間はほぼ勉強していました。

無職時代

ちょうど一年バイトしてお金も貯まって勉強のラストスパートのためバイトを辞めました。それでも結局TOEFLはSpeakingとWritingがボロボロで全然使える点数が取れなかったので語学学習は本当に向いていないかもしれません。

結局TOEFLのスコア提出がいらない京大・北大・東北大の公共政策大学院を受験し、京大は一次は通過しましたが二次で落ちました。しかもボーダーで落ちた(京大は後で試験の成績と順位を問い合わせたら教えてくれる)のでとても悔しかったですが、今では特に後悔はありません。

そしてこの頃、Rによるやさしい統計学を読んでいたので多少プログラミングの経験があり、Pythonもちょっとだけ触っていたので「落ちたらプログラマーにでもなるかw」と漠然と思っていました。

そして見事落ちたのでプログラマーになることに決めました。プログラミングと就活両方を無料でサポートしてくれると言うことで確か10月ぐらいにUZUZカレッジに話を聞きに行きました。

uzuz-college.jp

Javaをやると言うことで帰り道に池袋のジュンク堂でスッキリわかるJava入門・実践編・サーブレット&JSP入門を買い、以前からやっていたProgateと合わせて勉強していました。

現在は動画講義だと思いますが(最後に見に行ったのが2019年とかだった気がするので確証はない)当時はオンサイトで目の前で教えてもらっていました。

webの所謂な掲示板を作って、その後は面接対策を行いなんとか年明けてだったか内定をいただきました。

あとこの頃はPythonが好きだったのとLinuxにも興味がありVagrant+VirtualBoxで仮想環境を構築してPythonDjangoをherokuにデプロイして遊ぶと言うのをやっていました。

この面接対策は今でも生きていて

  • ハキハキと話す
  • 結論から話す

こう言うのは駆け出しであろうがなかろうが変わらないと思いました。

あとはちゃんと自分の主張に対して、相手が腹落ちできるようなストーリーと理由をつけると言うのも今もやっています。

Railsポートフォリオ作るぐらいならちゃんと面接対策しましょう。

一社目

晴れて正社員エンジニアとして岩本町にある会社に就職することとなりました。

結論としては三ヶ月で退職することとなるのですが、前提として自分ももう少しできることがあったなと言う認識があり反省があります。

入社してすぐに受託開発をやることとなり、とりあえず技術選定をしてと言われます。駆け出しにそんなことできるはずもなく、Javaの方が詳しいけどC#の会社なのに急にJava使ったらやばいだろと思って今まで使っている技術資産があるからと言うことでC#とAngularJSでどうでしょうかと提案しました。(今考えるとあれは何の意味があったんだ?)この頃Angular自体も初めて知ったのですが、どうやらAngularはver1とver2でだいぶ違っていて、Angular2の方はTypeScriptを使っているしこちらの方がモダンらしいと言うことで業務外ではAngular2で遊んでいました。

そして実質一人でC#とAngularJSを使ってWebアプリケーションを作ることとなりました。C#Javaと似ているからってそんなに書けるはずもなく、MyBatisとMyBatisの社内ラッパーも使い方もよくわからんしOJTどころかただのOJでとんでもないストレスに苛まれていました。最終的にIISに公開するのも紙ペラ一枚だけ渡されて何が何だかわからず、めっちゃ業務委託の中国人のお兄さんに聞いてなんとか予定通り一ヶ月ぐらいでリリースできました。本当にしんどかったですね。

そしてここは書けないですが、端的に言えば雇用契約書と言ってることが違うじゃんという事態が発生し、契約書を巻き直すことになりました。この辺であまりにも意味がわからず転職エージェントに相談して、奇跡的に内定をもらったので3ヶ月での退職となりました。

二社目

業界ではかなり大きめのSES企業でした。

ここでも最初の現場が全く合わず、最初はオフィスの最寄駅の手前で体調が終わるのが最終的にベッドから起き上がれなくなるぐらい体調が悪くなってしまい抜けることとなりました。

その後は日○の孫会社に出向し、Java+AngularJS+SVNで色々不満はありながらも比較的まともな開発ができるようになりました。

めちゃめちゃ不満があるほどではなかったですが、ここではパブリッククラウドに触ることもできないしモダンフロントエンドを業務で触ることも難しいということで転職活動を行い、無事現時点での前職から内定をいただきました。辞める時に現場の上司の方が、当時やっていた統合テストが終わったらもう少し腰を据えて開発経験ができるところに異動させようと思っていたらしいです。

最終的に三ヶ月と九ヶ月で合計一年在籍していました。

今となってはこの九ヶ月が一番色々やっていた時期で、プライベートでほぼ誰とも会ったり遊んだりせずひたすら何かをしていた気がします。(DjangoやVueやAWSPythonのwebフレームワークを自作しようとしたりScalaとPlay Frameworkとか本当に色々でした)

三社目

こちらもSES企業でしたが、エンド直か二次請けの商流が浅いところでした。

転職時の期待はほぼあっており、一部よくわからないのはありながらもLaravel+Vue.js+AzureやTypeScript+Next.js+MySQL+GCPなどでの0→1フェーズを経験することができました。ここでフロントエンドからインフラまで一貫して携わったり、テストコードの導入や自動化などの旗振り、Terraformの導入、クライアントの新卒の育成など様々なお手伝いをさせていただきました。

エンジニア採用にも携わるようになり、(前職のレベル感故かもしれませんが)基準の高いエンジニアの少なさと面接が下手なエンジニアの多さというのもここで知ることとなりました。

一定満足していましたが、結局何かを変える時って成果はクライアントの熱量を超えることは無いので、今自分がやっていることに対して虚無感のようなものを感じるようになりました。(この辺は日本のDXというものに対しても近いものを感じていますがここでは割愛)

最終的にはその企業の中にいないと難しいと思うこととなり、そうなると

  • そもそも事業会社がどんな感じなのかもわからない

  • かといって儲かっていない事業会社はただの地獄なので踏みたくない

  • 次の選択肢はかなり重要だという直観

ということから、退職してフリーランスをして内部を見て考えるという方針にしました。

フリーランス時代

ありがたいことにTwitterで次の職を募集したところ、多くのお声がけをいただきました。面接も並行でしながら、フリーランスでたくさんのお客様のお手伝いをさせていただきました。特にフリーランス初期は書類周りや業務の進め方がなっていなかった点が多々あり、初期にお手伝いさせていただいていた企業様には大変ご迷惑をお掛けしたと思います。この場を借りてお詫び申し上げます。

この頃はGraphQLやったりGoやったり

フリーランスをして

  • 税金も保険料も高いがだいぶ稼げるし外資系とかじゃない限りはフリーランスは金銭的にはお得

  • 一方でスキルと時間の切り売り感は否めない(企業によるが面白い仕事は社員に行きがち)

  • 目安としてテックリードとかマネージャー相当のスキルがない状態で長くやると、リーダー・マネジメント経験がないのにまあまあ年齢が高い人が出来上がってキャリアが詰む可能性が上がる

という感想を持ちました。

こういったことを身をもって感じたり書類・税金に多少詳しくなったり、色々な会社を見ることができたのでやってみて良かったと思います。

また自分がマネージャー相当の職位を経験したらどうなるかも気になるのでやっていみたいですね。

この期間に転職活動も行い、最終的には当時フリーランスでお世話になっていた企業にそのまま入社することとなりました。

四社目(現職)

決め手としては

  • 事業について高い解像度で解釈を伝えてくれたり会話ができる人がいる

  • 大規模広告配信

  • チームで自分が明確にポジションを持てる領域が存在しており、そこがチームの課題になっている

  • 相対的に色々整っていない(気がする)

といったところから現職に決めました。

まだあまり書けることはない(様々な理由で)ですが、技術負債の解消だったりアドタグ周りの改善、外部連携など色々やっています。

技術的にはGoにPythonに業務で触ることはあまりなかったAWSと関わり、所謂なWebフロントエンドアプリケーションとは違ってそのままのブラウザと向き合うこともしています。

タスクマネジメント・プロジェクトマネジメントみたいなのは苦手ですが、エンジニアが少ないのもあり一人一プロジェクトでやっているので昔よりはマシになっているかもしれません。

来年は自分から提案した取り組みをやるかもしれないのと事業に対する解像度をさらにあげていきたいですね。

来年度からは放送大学に入学して、学位授与機構を使って情報工学の学位を取ろうと思っています。

現在地のスキル

だいたいこの辺をやっています。

順番は特に関係ないです。

言語・FW

TypeScript、Express.js、React.js、、Vue.js、Go、Python、Laravel、Rails(嫌いだけど)

最近はソフトウェア設計方面に重心を重心を置こうとしている。

DB

MySQLPostgreSQL

ただしそれぞれの仕様について特別詳しいとかではない

Public Cloud

AWSGCP

Cloud RunとBigQueryとかはまあまあ使っている。AWSは現職でちゃんと使うようになった。

DevOps

Terraform、GitHub Actions、Jenkins、Renovate、Airflow

Terraformが最高。GitHub Actionsもめちゃいい。

各フェーズの生き残りスキル

駆け出し

ポートフォリオを作らない

よくあるプログラミングスクールでRails教えたりポートフォリオ作らせたりするを虚無だと思っているのでここについて書くとそれだけで記事にできてしまうのですが、ざっくり書くと

  • ポートフォリオ(ここではとりあえずなんか作ったアプリケーション)はストーリーがないので評価しにくい

  • よく使われるRailsは中級者がローコードで生産性上げるためのフレームワークで駆け出しとの相性は最悪

  • ポートフォリオを作っている時間があるなら面接対策してさっさと就職した方がいい(他職種で実績がある人はともかく、ほとんどの駆け出しは対等なテーブルに立てない)

といった理由です。

とにかく面接対策

駆け出しエンジニアはそもそも企業と対等な立場では無いので、とにかく潜り込んで経験年数を早期に獲得するのが至上命題だと思っています。

面接対策は同じスキルでも採用担当者からの見え方が変わりますし、今後のキャリアにおいても効いてくるのでちゃんとやりましょう。

1年目~3年目

自分のスキルよりちょっと上をいつも試す

自分のできることの少し上を常に試してみましょう。

自分の場合は1年目ではソフトウェアテストについて勉強してエクセルテストの時に提案をしてみたり、足りていないテスト項目を自分で試してみて未知のバグを見つけたこともありました。2年目ではソフトウェアテストを自分も詳しく無いけど提案してメンバーと一緒に試してみたり、色々自動化してみたりと何か新しいことを常に取り込んできました。

こう言った習慣が身についている人とそうでない人では大きな差が生まれるでしょう。

設計に携わる

最低限詳細設計、できれば基本設計と呼ばれるフェーズをやるといいと思います。

SIerは無縁かもしれませんが、ChatGPTやGitHub Copilotの出現でエンジニアの頭数が必要になることが減ってくると思っています。現時点で他の外部要因があるにせよ、駆け出しエンジニアや詳細設計しかできないエンジニアの採用は厳しくなっています。設計という行為は何らかのトレードオフに対して技術的な判断が必要になるものであり、機械だけでは直近はできそうにないと考えます。

できればこの期間のうちに駆け出しアーキテクトになっておきたいです。

全体としての所感

F欄しか受からなかった時は人生詰んだと思いましたし、院試の勉強をしている時は貯金が日々減っていく中で落ちたらどうなってしまうのかという不安に苛まれていました。今となっては、給料もそこそこもらえてフリーランスでも月単価税抜80万ぐらいはもらえる(し今後はもっと上げられる余地がある)というのも分かったのでこの業界でしばらくはやっていけそうな安心感があります。

やはり世の中エンジニアの適性が明らかに無い人も多いです。 適性の有無をどう考えるかで言えば、好きか嫌いかではなく他人よりも楽しているのに評価してもらえるものが適正だと考えています。自分は今でも休日に暇さえあれば色々試してみたりOSSを探してみたりと比較的エンジニアとしての適性があったように思います。

8月読んだ本

www.shuwasystem.co.jp

業界のお金の流れとかWeb広告の数値の関係性とかを見るのに良かった。

www.kobunsha.com

これはこれで興味深かったけどWeb広告の話がほぼ出てこなかったのでちょっとnot for meだった。

イシューからはじめよ――知的生産の「シンプルな本質」eijipress.co.jp

ざっくり読んだ。

まとめとかまあまあ読んでたのでなんとなくは分かっていたけどその通りだったので、ここまで有名になったらわざわざ読まなくても良かったかも。

www.iwanami.co.jp

HSPがどの文脈から発生したか、学術的なHSPの定義と巷で広まっているHSPの認識の相違、HSP周辺の怪しいビジネスの実態などが書かれていて興味深かった。

HSP自体は心理学のちゃんとした研究対象であるらしいが、心理学(さらには経済学との学際分野である行動経済学)の有名な研究が追試に失敗していること、提唱者のエレイン・アーロンが吊り橋効果という胡散臭い実験を行ったアーサー・アーロンの妻らしいのでそもそもHSPという概念自体がどうなのかとは思う。

note.com

ja.wikipedia.org

9月はMySQL関連を読んでいく。

GitHub ActionsとTerraformで差分に基づく条件付きCI実行の実装

目次

  1. はじめに:差分に基づくCI実行の必要性
  2. Terraformリポジトリのディレクトリ構造
  3. GitHub Actionsの設定手順
  4. 差分の判定とディレクトリの取得
  5. まとめ

はじめに:差分に基づくCI実行の必要性

開発者の日々の作業において、無駄な時間とリソースを浪費することは避けたいものです。特に、GitHub Actionsを使ったCIが、全ディレクトリに対してトリガーされるとなると、その浪費は顕著になります。そのため、今回は変更があったディレクトリに対してのみCIを実行する方法を、具体的な手順とともに紹介します。この手法により、CIの実行時間が大幅に短縮され、リソースの使用も最小限に抑えられます。

以下では、Pull Requestをトリガーに、その差分に基づいてCIを実行する具体的な手法について詳しく解説します。具体的には、Terraformのリポジトリを例に、GitHub Actionsでの任意のディレクトリ配下の全てのディレクトリを取得し、特定の条件下でコマンドを実行する方法を説明します。

Terraformリポジトリのディレクトリ構造

本記事では、以下のようなディレクトリ構造を持つTerraformリポジトリを例とします。これは、各サービスと環境ごとにTerraform設定を分けて管理している例です。

root/
 └ terraform/
    ├── service1/
    │   ├── envs/
    │   │   ├── dev/
    │   │   │   └── main.tf
    │   │   ├── prd/
    │   │   │   └── main.tf
    │   │   └── stg/
    │   │       └── main.tf
    ├── service2/
    ├── service3/
    └── service4/
  • terraform ディレクトリの直下には、各サービスのディレクトリ(service1, service2 など)があります。
  • 各サービスのディレクトリ内には、環境ごとのディレクトリ(dev, prd, stg)があります。
  • 環境ディレクトリ内には、その環境で適用されるTerraformの設定を含む main.tf ファイルがあります。

次のセクションでは、このリポジトリ構造を基にしたCIの設定方法を解説します。

GitHub ActionsでのCI設定

本節では、上述のディレクトリ構造を持つTerraformリポジトリに対し、GitHub Actionsを使って差分に基づくterraform planの実行設定を行う方法をご紹介します。まず、設定に必要なGitHub Actionsのワークフローファイルの内容を以下に示します。

name: 'Terraform Plan'
on:
  pull_request:
    paths:
      - 'terraform/**'
env:
  TARGET_DIR: 'terraform'
jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      dirs: ${{ steps.dirs.outputs.dirs }}
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: 'Output dirs'
        id: 'dirs'
        run: |
          changed_dirs=()
          while IFS= read -r dir; do
            dir=${dir%*/}
            git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} --depth=1
            DIFF=$(git diff --name-only HEAD ${{ github.base_ref }} --relative -- ./${{ env.TARGET_DIR }}/$dir/ | wc -l)
            echo "$dir diff: $DIFF"
            if [ "$DIFF" != "0" ]; then
              changed_dirs+=("$dir")
            fi
          done < <(find ./${{ env.TARGET_DIR }} -maxdepth 1 -type d | tail -n +2 | sed 's|^./${{ env.TARGET_DIR }}/||')
          printf -v joined_dirs "\"%s\", " "${changed_dirs[@]}"
          arr=$(echo "[${joined_dirs%, }]" | jq -c)
          if [ "$arr" = '[""]' ]; then
            echo "No changed directories."
          fi
          echo "dirs=$arr" >> $GITHUB_OUTPUT
  plan:
    name: 'Terraform Plan'
    if: needs.setup.outputs.dirs != '[""]'
    needs: setup
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        envs: [ dev, stg, prd ]
        resources: ${{ fromJson(needs.setup.outputs.dirs) }}
    env:
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_DEFAULT_OUTPUT: json
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      TERRAFORM_WORKING_DIR: ${{ env.TARGET_DIR }}/${{ matrix.resources }}/environments/${{ matrix.envs }}
      TERRAFORM_VERSION: '1.4.6'
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v3
      - name: 'SetUp tfcmt'
        run: |
          sudo curl -fL -o tfcmt.tar.gz https://github.com/suzuki-shunsuke/tfcmt/releases/download/$TFCMT_VERSION/tfcmt_linux_amd64.tar.gz
          sudo tar -C /usr/bin -xzf ./tfcmt.tar.gz
        env:
          TFCMT_VERSION: v4.3.0
      - name: 'Setup Terraform'
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}
      - name: 'Terraform Format'
        run: |
          terraform fmt -check -recursive
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}
      - name: 'Terraform Init'
        run: |
          terraform init -lock-timeout=300s
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}
      - name: 'Terraform Validate'
        run: |
          terraform validate -no-color
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}
      - name: 'Terraform Plan'
        run: |
          tfcmt -var "target:${{ matrix.resources }}-${{ matrix.envs }}" plan -patch -- terraform plan -no-color
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}

このワークフローファイルは、プルリクエストに対する terraform planterraform/ 内の変更があったディレクトリに対してだけ実行します。具体的には以下の処理を行います:

  1. setup ジョブで、変更があったサービスのディレクトリを抽出します。各サービスディレクトリについて、現在のブランチとベースブランチとの差分を取り、差分が存在すればそのディレクトリ名を changed_dirs 配列に追加します。
  2. plan ジョブで、変更があったサービスのディレクトリごとに、各環境(devstgprd)について terraform plan を実行します。各実行結果はプルリクエストにコメントとして出力されます。

なお、plan ジョブでは tfcmt を使用して terraform plan の結果をプルリクエストにコメント出力しています。これは便利なツールなので、ぜひ利用してみてください。

差分の判定とディレクトリの取得

この章では、PRで変更されたディレクトリを正確に判定し、それらのディレクトリのみを対象にTerraformのplanを実行する方法について説明します。GitHub Actionsのworkflowファイルに含まれるsetupジョブがこの役割を果たします。

まず、actions/checkout@v3を使用して、Gitリポジトリをチェックアウトします。ここではPRのヘッドSHAを使用します。

- name: 'Checkout'
  uses: actions/checkout@v3
  with:
    ref: ${{ github.event.pull_request.head.sha }}

その後、以下のShellスクリプトを使用して、変更が発生したディレクトリを判定します。

changed_dirs=()
while IFS= read -r dir; do
  dir=${dir%*/}
  git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} --depth=1
  DIFF=$(git diff --name-only HEAD ${{ github.base_ref }} --relative -- ./${{ env.TARGET_DIR }}/$dir/ | wc -l)
  echo "$dir diff: $DIFF"
  if [ "$DIFF" != "0" ]; then
    changed_dirs+=("$dir")
  fi
done < <(find ./${{ env.TARGET_DIR }} -maxdepth 1 -type d | tail -n +2 | sed 's|^./${{ env.TARGET_DIR }}/||')
printf -v joined_dirs "\"%s\", " "${changed_dirs[@]}"
arr=$(echo "[${joined_dirs%, }]" | jq -c)
if [ "$arr" = '[""]' ]; then
  echo "No changed directories."
fi
echo "dirs=$arr" >> $GITHUB_OUTPUT

このスクリプトは、terraformディレクトリ直下のすべてのディレクトリ(各リソースのディレクトリ)に対して、ヘッドSHAとベースブランチとの間で変更があったかどうかを確認します。変更があったディレクトリだけがchanged_dirs配列に追加されます。最終的に、この配列はJSON形式でGitHub Actionsの出力に設定され、次のplanジョブで使用されます。

この方法により、変更が発生したディレクトリに対してだけterraform planが実行されるようになります。これにより、差分に基づくCI実行が可能となります。

まとめ

本記事で紹介した差分に基づくCI実行の方法は、大規模なTerraformリポジトリを管理する際の助けになるでしょう。GitHub Actionsの力を借りて、Pull Requestで変更が発生したディレクトリだけを対象にterraform planを実行することで、CIパイプラインの効率を大幅に向上することができます。

また、このアプローチはTerraformだけでなく、他のInfrastructure as Code (IaC)ツールやワークフローにも適用可能です。もっと良い方法があればぜひ教えてください!

(この記事はChatGPTに支援してもらって書いてみました!文体のチューニングが難しいですね。)

GolangでGitHubのrepositoryのcommitを取得する

実現方法

GitHubのPersonal Access Tokenを発行する

Repository accessはprivate repositoryまで取得するならAll repositoryを選択するかOnly select repositoryで直接指定してください。

PermissionsContentsread-onlyに変更して発行してください。

Contentsをread-onlyに変更する
commitを取得するGitHub Personal Access Tokenの設定項目

実装

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"

    "github.com/google/go-github/v52/github"
    "golang.org/x/oauth2"
)

func main() {
    // GitHubのPersonal Access Token
    token := os.Getenv("PAT")
    ctx := context.Background()
    tokenSource := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: token})
    oauth2Client := oauth2.NewClient(ctx, tokenSource)
    githubClient := github.NewClient(oauth2Client)
    opt := &github.RepositoryListOptions{
        Sort:        "updated",
        Direction:   "desc",
        ListOptions: github.ListOptions{PerPage: 10},
    }
    var (
        allRepos []*github.Repository
        nextPage = -1
    )
    repoCommits := make(map[string][]*github.RepositoryCommit)
    for nextPage != 0 {
        repos, r, err := githubClient.Repositories.List(ctx, "kanade0404", opt)
        if err != nil {
            if _, ok := err.(*github.RateLimitError); ok {
                log.Fatalln("error: hit rate limit")
            }
            log.Fatalln(err)
        }
        nextPage = r.NextPage
        opt.Page = nextPage
        allRepos = append(allRepos, repos...)
        commitOpt := &github.CommitsListOptions{
            ListOptions: github.ListOptions{
                PerPage: 10,
            },
        }
        repoCommitNextPage := -1
        for _, repo := range repos {
            if repo.GetOwner().GetLogin() == "kanade0404" && (repo.GetName() == "resume" || repo.GetName() == "hello") {
                for repoCommitNextPage != 0 {
                    commits, commitRes, err := githubClient.Repositories.ListCommits(ctx, "kanade0404", repo.GetName(), commitOpt)
                    if err != nil {
                        log.Fatalln(err)
                    }
                    repoCommits[repo.GetName()] = append(repoCommits[repo.GetName()], commits...)
                    if commitRes.NextPage == 0 {
                        break
                    }
                    repoCommitNextPage = commitRes.NextPage
                    commitOpt.Page = repoCommitNextPage
                }
            }
        }
    }
    for k, v := range repoCommits {
        if err := json.NewEncoder(os.Stdout).Encode(v); err != nil {
            log.Fatalln(err)
        }
    }
}

SageMaker Studioでexec format errorを解決する

結論

m1 macからのbuiildだったためアーキテクチャの差異が原因。 m1 macでのdocker build時は--platform=amd64を指定する。

詳細

SageMaker Studioのカスタムイメージを使うためにdocker build→docker pushまで実行してSageMaker Studioの立ち上げるも、カスタムイメージをを指定すると正常起動せず最終的にエラーになる。

CloudWatchで該当のappのログを見ても出力されているのはexec /opt/.sagemakerinternal/conda/kgw_variant: exec format error.というエラーのみでパッと見よくわからない。

最終的に辿り着いたのが以下のissueです。

github.com

m1 macでビルドした際にarm64のイメージが優先して取得されます。SageMaker StudioはEFSボリュームが今回だとKernelGatewayにマウントされる際にアーキテクチャが合わずにKernelの起動に失敗したという感じでしょうか。(推測が入っているので正しいかはわかりません)

qiita.com

なのでm1 macであればplatformオプションをamd64に指定することで解決するということです。

qiita.com

運用するときはGitHub ActionsやCircleCIなどを使うと思うのでアーキテクチャの違いに悩まされることは無いと思いますが、調査の時はローカルでの作業がメインになるので気をつけましょう。

余談ですが、調査しているときに同じDockerfileを使っているのにSageMaker Studioで起動するイメージと起動しないイメージがあり、更に謎が深まっていました。理由はローカルからbuildした時とGitHu Codespacesでbuildした時の違いでした。

部分和問題のビット全探索をやっと理解した

アルゴリズム力を鍛えるためにけんちょんさんの本を読んでいて、部分和問題のビット全探索でうまく理解できなかったので自分が理解できるまで調べたという話です。

より詳細な解説を読みたいのであればご本人のブログを読むのが良いかと思います。 drken1215.hatenablog.com

部分和問題でのビット全探索

部分和問題について

「部分和問題 (subset-sum problem)」は、要素が数値の集合 S において、要素の総和が M となる部分集合があるか判定する問題です。

www.nct9.ne.jp

ビット演算について

&(AND)

二つのスイッチを比較してどちらも1なら1、どちらか0なら0を返す

c = a & b

a: 10110101

b: 01000101

c: 00000101

用途

ビットでフラグ管理をしている場合の探索

|(OR)

二つのスイッチを比較してどちらかが1なら1、どちらも0なら0を返す

c = a | b

a: 10110101

b: 01000101

c: 11110101

用途

ビットでフラグ管理をしている場合にフラグを立てる場合

!(NOT)または~

スイッチのON/OFFを全て逆転させる

c = !a

a: 10110101

c: 01001010

XOR

二つのスイッチを比較してどちらか片方だけ1なら1、どちらも1または0なら0を返す

c = a ^ b

a: 10110101

b: 01000101

c: 11110000

用途

二つのスイッチの差分を調べる

>>(右ビットシフト)

スイッチのON/OFFを一つ右にずらす。

空いたビットには0埋めが行われて、溢れたビットは消える。

c = a >> 1

a: 10110101

c: 01011010

用途

ループでビット操作する時

<<(左ビットシフト)

スイッチのON/OFFを一つ左にずらす。

他の操作は右ビットシフトと同様。

c = a << 1

a: 10110101

c: 01101010

用途

右ビットシフトと同様

参考

https://qiita.com/Ingward/items/43acda931c8a62c70d2f

ビット全探索の解説

例題

N個の正の整数a0,a1,...,aN-1と正の整数Wが与えられます.a0,a1,...,aN-1の中から何個かの整数を選んで総和をWとすることができるかどうかを判定してください.

例題解説

N個の整数の集合の2Nある部分集合からどの組み合わせでも良いので総和Wになる組み合わせが存在するかを判定する。

たとえば{1,3,7}という整数の集合の部分集合は{},{1},{3},{7},{1,3},{1,7},{3,7},{1,3,7}の8通りある。(23=8通り) 総和が10の場合、{3,7}が該当する。 実装は以下のようになる。以降でこの実装について解説する。

def main():  
    n, k, a, is_exist = 3, 10, [1, 3, 7], False
    for bit in range(2 ** n):  
        sum = 0  
        for i in range(n):  
            if bit & (1 << i):  
                sum += a[n - 1 - i]  
        if sum == k:  
            is_exist = True  
    print("Yes" if is_exist else "No")  


if __name__ == '__main__':  
    main()

解法

bit全探索

方針としては 1. N個の数列それぞれにビットを当てはめる 2. bit&(1<<i)でそれぞれの数列に当てはめたビットの論理積をとって足し合わせて総和が一致するかを判定 と解いていきます。

N個の数列それぞれにビットを当てはめる

仮に$N=4$の場合、全ての数列は{0, 1, 2, 3}の4つになります。このそれぞれにビットを当てはめると0は001、1は010、2は100、3は1000と置くことができます。

N個の数列の組み合わせを復元する

では総和Wが5である組み合わせを復元するにはどうするか。 以下のように数列N個全てに対して調べ上げます。

for i in range(n):  
    if bit & (1 << i):  

$N=4$,$bit=13$の場合にカウンタ変数$i$とif文の条件式の状態は以下のようになります。

i 1<<i bit&(1<<i)
0 00000001 00001101 & 00000001 = 00000001(True)
1 00000010 00001101 & 00000010 = 00000000(False)
2 00000100 00001101 & 00000100 = 00000100(True)
3 00001000 00001101 & 00001000 = 00001000(True)

総和Wが5となる組み合わせは{0,2,3}ですが、それぞれに当てはまるカウンタ変数$i$でTrueとなります。 1<<iは1(00000001)をiの分だけ左にシフトさせます。これがN個ある整数のフラグとなっています。 そしてbitとの論理積を取ると、フラグとしてシフトさせた位置のビットがどちらも1になっていれば1、どちらかが0であれば0となります。これで最初に数列の各要素に当てはめた数字を足し合わせると総和が求まり、これを全探索して終了となります。

終わりに

この方法だと計算量は$O(2N^{2})$となりあまり優秀とは言えません。動的計画法を使うと$O(NW)$に抑えられるので次はDPに挑戦です。

2022年振り返り&2023年目標

2022年振り返り

良かったこと

  • 新しい挑戦ができたこと

  • GoとGraphQL、AWSを業務で使えるようになった

改善すべきこと

  • 他がこれまでのスキルの貯金でやってるところがある

  • 特にこれといったインパクトがある実績がない(かもしれない)

    • 2.5回ほど環境が変わっているのである程度は仕方ないかもしれない(と言い聞かせる)
  • インプットとアウトプットの量と質共に減ってしまった

  • 特にAWSわからんで色々ハマってしまった

2023年目標

スキルアップ

  • 特定のスキルに集中投資する

    • Go、GraphQL、データベース、Next.js、AWSGCP
    • できたらk8sとかRustとかHaskellやりたい
  • アウトプットを増やす

    • 量は質を兼ねるの精神
    • まずは毎週からzennやっていき
    • scrapboxもいいがobsidianも活用したい
  • AWSをもっと知る

    • RSSフィードを購読するようにしたので、まずは毎週気になった内容についてscrapboxにまとめてみる
  • 英語を頑張る

    • youtubeで意図を誤らずに聴けるぐらい
    • 最終的には英語で面接を受けられるようにする
  • 女性とのコミュニケーションについて更に学ぶ

    • 彼女を怒らせる回数を今年よりも減らす
  • 個人開発しているアプリをリリースして運用・改善する

健康

  • もっと運動する

    • 最終的には週4程度で有酸素と筋トレ
    • ジムには行かないでお金をかけずに健康を維持する
  • 歯医者を月1で行く

  • アレルギー(ハウスダスト、ダニ、杉、檜)を克服

お金