<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Keisei Kinjo (JC) on Keisei Kinjo (JC)'s Blog</title><link>https://www.kinjo.tech/</link><description>Recent content in Keisei Kinjo (JC) on Keisei Kinjo (JC)'s Blog</description><generator>Hugo</generator><language>en</language><copyright>© 2026 kinjo.tech</copyright><lastBuildDate>Thu, 23 Jul 2026 00:42:11 +0000</lastBuildDate><atom:link href="https://www.kinjo.tech/index.xml" rel="self" type="application/rss+xml"/><item><title>X-GhostのインフラをAzureからCloud PF Type Aへ移行した話</title><link>https://www.kinjo.tech/posts/20260723/</link><pubDate>Thu, 23 Jul 2026 00:42:11 +0000</pubDate><guid>https://www.kinjo.tech/posts/20260723/</guid><description>&lt;p&gt;会社のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;script src="https://embed.zenn.studio/js/listen-embed-event.js"&gt;&lt;/script&gt;&#10;&lt;span class="zenn-embedded-card"&gt;&#10; &lt;iframe&#10; id="zenn-embedded__cd22f9eeca6912a2c2ac5bee5090682d-0"&#10; src="https://embed.zenn.studio/card#zenn-embedded__cd22f9eeca6912a2c2ac5bee5090682d-0"&#10; data-content="https%3A%2F%2Fzenn.dev%2Fgenax_corp%2Farticles%2F2f6f5e0d1f281e"&#10; title="X-GhostのインフラをAzureからCloud PF Type Aへ移行した話"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10; &gt;&lt;/iframe&gt;&#10;&lt;/span&gt;&#10;&lt;noscript&gt;&lt;a href="https://zenn.dev/genax_corp/articles/2f6f5e0d1f281e" target="_blank" rel="noopener noreferrer"&gt;X-GhostのインフラをAzureからCloud PF Type Aへ移行した話&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>太陽隊2025-2026賽季回顧</title><link>https://www.kinjo.tech/posts/20260429/</link><pubDate>Wed, 29 Apr 2026 05:41:01 +0000</pubDate><guid>https://www.kinjo.tech/posts/20260429/</guid><description>&lt;blockquote&gt;&#10;&lt;p&gt;we all can feel proud of what this team did, not only over the course of the entire season, but tonight as well&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;季後賽第一輪4:0被強大的雷霆橫掃，之後主教練Jordan Ott說道。&lt;/p&gt;&#10;&lt;p&gt;整個系列賽有過幾分鐘的領先可整體上沒有任何還手之力。即便是這樣本賽季還是遠遠超出了包括我在內多數人的預期。&#10;受到傷病困擾三巨頭同時在場的比賽不會超過30場，馬威，艾倫連續不能出戰。即使在這樣的情況下，賽季中無明星幹掉湖人，雙殺灰狼，絕殺雷霆，附加賽第一場惜敗拓荒者後，第二場對勇士強勢追回來都讓人眼前一亮。對太陽球迷的我來說是一個充滿驚喜沒有遺憾的賽季。特別是在經歷過去年那樣一個&lt;a href="../20250510"&gt;絕望的賽季&lt;/a&gt;之後。&#10;整理了下心情剛好在等飛機的時候有時間回顧本賽季的同時給教練和各個球員打個分。球員按照薪資排序，除去已經被交易或者上場時間過短的球員。&lt;/p&gt;&#10;&lt;h3 id="jordan-ott-s"&gt;Jordan Ott: S&lt;/h3&gt;&#10;&lt;p&gt;第一年當主教練，經驗不足能把這副天賦明顯就欠缺的牌打著麼好多半要歸功於Ott.&#10;從騎士隊帶來的需要大量跑動wheel offense戰術，進攻明顯變得流暢，空間開闊三分機會多，少有硬解。&#10;他也不墨守成規，能用的鋒線只有兩個年輕人Fleming和Dunn，而且兩個人的三分過於不穩定，例行賽後期開啟了我從我見過的DBook, Jalen, Goodwin/Royce, Collin, Ighodaro/Williams四後衛一中鋒的極端陣容。&#10;相信每一個球員，最後時刻往往能出奇制勝帶走比賽。與此同時Grayson, Royce, Collin都打出了生涯年。&lt;/p&gt;&#10;&lt;p&gt;球隊文化不是一年內養成的，冠軍球隊也不是單靠堆疊球星加老牌明星教練就能實現的。Ott正在幫太陽對建立新的球隊文化。這是前面三個失敗的賽季都沒有過的。&lt;/p&gt;&#10;&lt;h3 id="devin-booker-c"&gt;Devin Booker: C&lt;/h3&gt;&#10;&lt;p&gt;可能是因為沒有了KD的防守牽制力，也沒有3年前攻受平衡的陣容加持。carry全場的能力下降了不少，特別是在面對鋒線強大的球隊的時候很容易就被封死。賽季的三分命中率33%在後衛裡面算很低。期待下個賽季能夠找回狀態。&lt;/p&gt;&#10;&lt;h3 id="jalen-green--b"&gt;Jalen Green: B&lt;/h3&gt;&#10;&lt;p&gt;如果沒有Jalen的爆發可能已經倒在了跟勇士的那一場附加賽。&#10;但賽季初期缺陣太長跟球隊的體系有點不兼容，跟在火箭的時期一樣組織跟傳控能力太差勁，防守也基本是漏勺。作為第二得分點太差，如果能像幾年前路威或者JC一樣作為第六人發揮一點神經刀屬性還行。做為核心還差的太遠，這也是火箭隊去年為什麼要把他當作交易的籌碼。&lt;/p&gt;&#10;&lt;h3 id="dillon-brooks-s"&gt;Dillon Brooks: S&lt;/h3&gt;&#10;&lt;p&gt;Dillon一向以防守強硬性格更強硬出名，不是這個賽季我都不知道他的單打可以這麼強，&#10;有一段時間沒有DBook跟Jalen就是靠Dillon帶隊贏了西部強隊多次。跟DBook一樣帶個髮帶，如果不是投籃姿勢太醜甚至有點難以分辨。不僅如此他還給太陽帶來的精神屬性，整體的防守積極性。比如贏球後的賽后採訪學狼叫就能看出整個團隊氣氛很不錯。&lt;/p&gt;&#10;&lt;h3 id="grayson-allen-b"&gt;Grayson Allen: B&lt;/h3&gt;&#10;&lt;p&gt;兩年前有一段時間是聯盟最強神射手三分投出46%&#10;去年開始增肌後也沒有下降多少。這個賽季下降到35%但產量來到生涯新高，在場就很穩，但缺席場次太多了。&lt;/p&gt;&#10;&lt;h3 id="royce-oneale-a"&gt;Royce O&amp;rsquo;Neale: A&lt;/h3&gt;&#10;&lt;p&gt;可以說沒有Royce的精準投彈式的邊線球，很多比賽在根本就拖不到最後，絕殺的幾場也幾乎都是靠他的發球。&#10;除了三分沒有Allen穩定啞火之外無可挑剔。&lt;/p&gt;&#10;&lt;h3 id="mark-williams-b"&gt;Mark Williams: B&lt;/h3&gt;&#10;&lt;p&gt;以年齡來說算不錯是太陽自不躺平的Ayton之後最好的中鋒，可經常會被對面更加強悍的中鋒爆前板。&#10;同樣的問題是傷病太多，季后賽如果能出戰贏雷霆一到兩場應該還是可以的。&lt;/p&gt;</description></item><item><title>如果關乎到體驗，我還是選擇自己來</title><link>https://www.kinjo.tech/posts/20260301/</link><pubDate>Sun, 01 Mar 2026 07:46:12 +0000</pubDate><guid>https://www.kinjo.tech/posts/20260301/</guid><description>&lt;p&gt;前段時間跟老婆聊起來，我出生於鄉下所以第一次去星巴克，居酒屋，義大利菜都是在18歲以後到了上海之後才體驗到的。&#10;她覺得不可思議，因為對她出生與首爾，這些事物早已司空見慣成了生活的一部分，甚至也回憶不起來第一次去到底是什麼時候。&lt;/p&gt;&#10;&lt;p&gt;一般來說體驗過一次之後就會因為邊際效應的遞減，完全沒有了第一次的新鮮感，另外經歷越多年齡越大對新鮮事物的感度也會下降。&#10;如果說人生是由體驗加總而成的，無疑去上海的上大學對我自己來說極大豐富了人生的體驗。可以說是鄉下人的精神勝利法，但這確實是出生在上海或者其它大城市的人所無法體會到的。&lt;/p&gt;&#10;&lt;p&gt;隨著AI或者LLM的出現體驗有被替代的趨勢，AI本質上來說是用自己的現有的認知水平來快速變現換取產出，可是提高自己的認知水平還是得花時間學習（學習本身也是體驗的一部分），AI可以加速最開始catch up的速度，但作為碳基生命的我們體驗目前看來只能自己來獲得不能簡單地複製粘貼。讓我想起了去年在Platform Engineering Kaigi 2025聽到的一個演講。&lt;/p&gt;&#10;&lt;script async class="docswell-embed" src="https://www.docswell.com/assets/libs/docswell-embed/docswell-embed.min.js" data-src="https://www.docswell.com/slide/ZG2VJ9/embed" data-aspect="0.5625"&gt;&lt;/script&gt;&lt;div class="docswell-link"&gt;&lt;a href="https://www.docswell.com/s/zoetro/ZG2VJ9-2025-09-18-142816"&gt;認知負荷理論で挑むPlatform Engineering：サイボウズにおけるKubernetesプラットフォームの拡大に向けて by @zoetro&lt;/a&gt;&lt;/div&gt;&#10;&lt;/br&gt;&#10;「認知負荷」也就是「Cognitive」，中文應該譯作「認知負擔」或者「認知成本」。&#10;這個演講強調的是減少認知負擔的過程的真正目的是--達成更深層次的學習。並且認知負擔也是有分類的&#10;&lt;ul&gt;&#10;&lt;li&gt;課題內在的負擔：比如難用的UI&lt;/li&gt;&#10;&lt;li&gt;課題外在的負擔：比如學語法這個過程很難&lt;/li&gt;&#10;&lt;li&gt;學習理解的負擔：運用學會的語法來解決問題的時候的負擔&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;一蹴而就得到的答案雖然能削減掉所有的認知負擔，但相反會失去真正的理解。關於課題內在和外在的負擔確實應該要削減，但對於學習理解的負擔反而應該增多。套用到AI上就是，用AI來減掉課題內在和課題外在的負擔，學習理解要自己花時間去體驗。&lt;/p&gt;&#10;&lt;p&gt;被運作了幾百年的資本主義機器給裹挾了的我們很容易理解並相信下面這條邏輯鏈：&lt;strong&gt;提高生產力 → 給公司帶來利益 → 有利於自己的績效考核 → 金錢報酬/職涯發展&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;這套邏輯經過了時間驗證在多數情況下都最能擬合現實。迫於生活壓力或者攀比心裡開始向現實妥協，傾向於用唯一的標準來衡量事物，比如金錢，比如生產力。&lt;/p&gt;&#10;&lt;p&gt;但如果體驗與學習這個過程本身對我來說很有趣，那我還是選擇自己來，不想“外包”給優秀的AI&lt;/p&gt;</description></item><item><title>日本打工人的稅務陷阱</title><link>https://www.kinjo.tech/posts/20251227/</link><pubDate>Sat, 27 Dec 2025 05:03:08 +0000</pubDate><guid>https://www.kinjo.tech/posts/20251227/</guid><description>&lt;p&gt;高投票率的老年人使用多數人暴力拖累社保，絕望的冰河世代要面對工資還沒有漲上去就要縮水，政府以公司這個中介瘋狂抽取中產階層，年輕人低薪加民粹。日本的未來看起來並不樂觀。&lt;/p&gt;&#10;&lt;p&gt;首先聲明我完全無意否認稅跟社保的重要性。到了年末算了一下稅金又重新學了點稅務相關的知識，想以一個打工人的身份來總結一下日本複雜的稅制以及對打工人來說有限的節稅方法。&lt;/p&gt;&#10;&lt;p&gt;先擺上數字比較容易理解。一個住在東京都中野區的普通上班族A，拿著略高於平均水平的年收入 &lt;strong&gt;500萬yen&lt;/strong&gt;，無撫養，沒有車，工資的50%用於租房以外的消費。&lt;/p&gt;&#10;&lt;h2 id="舉個例子"&gt;舉個例子&lt;/h2&gt;&#10;&lt;p&gt;那麼A在2025年這個要交的稅是 &lt;strong&gt;39.03%&lt;/strong&gt;&#10;這還是在實行減稅並假定A不喝酒不抽煙不開車的情況下，如果沒有還會高出很多。&lt;/p&gt;&#10;&lt;p&gt;這裡的 &lt;strong&gt;39.03%&lt;/strong&gt; 是怎麼算出來的呢，拆開看看。需要繳的稅大概有以下幾種，太多的話比較複雜，只寫了比較常見的。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;厚生年金&lt;/li&gt;&#10;&lt;li&gt;健康保險&lt;/li&gt;&#10;&lt;li&gt;雇用保險&lt;/li&gt;&#10;&lt;li&gt;所得稅&lt;/li&gt;&#10;&lt;li&gt;復興稅&lt;/li&gt;&#10;&lt;li&gt;住民稅&lt;/li&gt;&#10;&lt;li&gt;消費稅&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;從社會保險（年金跟保險跟健康保險）開始算起比較容易理解，因為社會保險是用收入來計算的，稅是用所得（從收入扣掉必要的經費）來計算的。&lt;/p&gt;&#10;&lt;h3 id="厚生年金"&gt;厚生年金&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nenkin.go.jp/service/kounen/hokenryo/ryogaku/ryogakuhyo/20200825.html" target="_blank" rel="noopener noreferrer"&gt;稅率是18.30%&lt;/a&gt;&lt;/li&gt;&#10;&lt;li&gt;月收入是416,666屬於410,000那一檔&lt;/li&gt;&#10;&lt;li&gt;按照自己出一半，公司出一半的原則那麼一年的厚生年金是 410,000 * 0.183 * 0.5 * 12 = &lt;strong&gt;450,180&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="健康保險"&gt;健康保險&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;根據協會稅率會不同，以&lt;a href="https://www.its-kenpo.or.jp/hoken/jimu/hokenryou/jigyou.html" target="_blank" rel="noopener noreferrer"&gt;［ITS］関東ITソフトウェア健康保険組合&lt;/a&gt;為例，稅率是9.50%&lt;/li&gt;&#10;&lt;li&gt;按照自己出一半，公司出一半的原則: 5,000,000 * 0.095 * 0.5 = &lt;strong&gt;237,500&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="雇用保險"&gt;雇用保險&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;稅率0.60%&lt;/li&gt;&#10;&lt;li&gt;5,000,000 * 0.006 = 30,000&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="所得稅"&gt;所得稅&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1410.htm" target="_blank" rel="noopener noreferrer"&gt;給與所得控除&lt;/a&gt;: 5,000,000 * 0.2 + 440,000 = 1,440,000&#10;&lt;ul&gt;&#10;&lt;li&gt;可以理解為打工人的經費，只不過這個經費是被釘死的&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/1199.htm" target="_blank" rel="noopener noreferrer"&gt;基礎控除&lt;/a&gt;: 680,000&#10;&lt;ul&gt;&#10;&lt;li&gt;需要通過「合計所得金額」來算出。所得是 5,000,000 - 1,440,000 = 3,560,000&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;社會保險控除: 450,180 + 237,500 = 687,680&lt;/li&gt;&#10;&lt;li&gt;所得稅&#10;&lt;ul&gt;&#10;&lt;li&gt;課稅部分: 5,000,000 - 1,440,000 - 680,000 - 687,680 = 2,192,320&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.nta.go.jp/taxes/shiraberu/taxanswer/shotoku/2260.htm" target="_blank" rel="noopener noreferrer"&gt;稅率&lt;/a&gt; 10%外加97,500的控除&lt;/li&gt;&#10;&lt;li&gt;所得稅 2,192,320 * 0.1 - 97,500 = &lt;strong&gt;121,732&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="復興稅"&gt;復興稅&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;所得稅的2.1%: 121,732 * 0.021 = &lt;strong&gt;2,556&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="住民稅"&gt;住民稅&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;基礎控除縮水為: 430,000&lt;/li&gt;&#10;&lt;li&gt;其餘不變&lt;/li&gt;&#10;&lt;li&gt;住民稅&#10;&lt;ul&gt;&#10;&lt;li&gt;課稅部分: 5,000,000 - 1,440,000 - 430,000 - 687,680 = 2,442,320&lt;/li&gt;&#10;&lt;li&gt;稅率: 10%上下，這裡以10%計算&lt;/li&gt;&#10;&lt;li&gt;住民稅: 2,442,320 * 0.1 = &lt;strong&gt;244,232&lt;/strong&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;上面全部扣完，A還剩下5,000,000 - 450,180 - 237,500 - 30,000 - 121,732 - 2,556 - 244,232 = 3,913,800&#10;還需要計算消費稅&lt;/p&gt;</description></item><item><title>GKEでrollout restartする際に起きる503エラーを解消した</title><link>https://www.kinjo.tech/posts/20251112/</link><pubDate>Wed, 12 Nov 2025 08:33:24 +0000</pubDate><guid>https://www.kinjo.tech/posts/20251112/</guid><description>&lt;p&gt;前日、GKEでdeploymentに対してrollout restartを実行すると、10s前後の503エラーが発生する事象に気づいた。呼び出し側でエラー処理が実装されているため大きな問題ではなかったが、オンプレのクラスタでは同じ事象が発生しないため、原因を調査した。&lt;/p&gt;&#10;&lt;p&gt;環境&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;GKE: 1.34.1-gke.1829001&lt;/li&gt;&#10;&lt;li&gt;Networking: Gateway API&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="再現"&gt;再現&lt;/h2&gt;&#10;&lt;p&gt;以下のスクリプトを実行させながら、&lt;code&gt;kubectl rollout restart deployment/your-app&lt;/code&gt;を実行する。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;START&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s.%N&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;while&lt;/span&gt; true&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; curl -s https://your-app.example.com/&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;END&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date +%s.%N&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nv"&gt;ELAPSED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="nv"&gt;$END&lt;/span&gt;&lt;span class="s2"&gt; - &lt;/span&gt;&lt;span class="nv"&gt;$START&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; bc&lt;span class="k"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; -e &lt;span class="s2"&gt;&amp;#34;\nElapsed time: &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ELAPSED&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; sleep 0.1&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;done&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;rollout restartをするタイミングで、10s前後、503系のエラーが起きることを確認できる。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Elapsed time: 21.341049000s&#10;OK&#10;Elapsed time: 21.530590000s&#10;OK&#10;Elapsed time: 21.730951000s&#10;upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: Connection refused&#10;Elapsed time: 21.938160000s&#10;upstream connect error or disconnect/reset before headers. reset reason: remote connection failure, transport failure reason: delayed connect error: Connection refused&#10;Elapsed time: 22.135801000s&#10;upstream connect error or disconnect/reset before headers. reset reason: connection timeout&#10;Elapsed time: 26.838277000s&#10;upstream connect error or disconnect/reset before headers. reset reason: connection timeout&#10;Elapsed time: 31.532648000s&#10;OK&#10;Elapsed time: 31.740313000s&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="503-upstream-connect-error-or-disconnectreset-before-headers"&gt;&lt;code&gt;503 upstream connect error or disconnect/reset before headers&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;処理中のリクエストがあるにもかかわらず、backend serviceが削除されるとこのエラーが発生する。&#10;preStop hookを追加することで解消できた。&lt;/p&gt;</description></item><item><title>英文在日本Tech Conference的用武之地</title><link>https://www.kinjo.tech/posts/20250928/</link><pubDate>Sun, 28 Sep 2025 04:27:31 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250928/</guid><description>&lt;p&gt;因為中二病或是僅僅因為不好學，高中的時候英文不及格是家常便飯。大學後也被順理成章地分到級別最低的一檔英文班，曾被英文老師嘲諷「你們能坐在這裡，數學成績應該都不錯」。&lt;/p&gt;&#10;&lt;p&gt;10年前來日本留學後才猛然發現：英文不好學術研究就難以順利進行，參加國際學會也必須使用英文發表，另外想跟來自其他國家的留學生交流也只能講英文。因此我不得不在學日文的同時被動提高自己的英文水平。很多人說學日文後因為片假名的緣故英文會倒退，但從我的經驗來看自學日文反而讓我理解了學習語言的通用方法，即「記住最低限度的語法與單詞，在試錯中收集反饋，用反饋來理解下一步應該要學什麼」。&#10;完全沒有想到，在這個過程中當年不拿手的英文成了我的興趣之一，到今天為止每個禮拜都去同一間咖啡店參加英語會話並且已經持續了6年。&lt;/p&gt;&#10;&lt;p&gt;近幾年因為工作的緣故，經常有機會參加日本的Tech Conference，主辦方一般都會邀請世界級別的名人或著大佬來進行最開始的Keynote演講。&#10;但問題是參加的日本程序員多半不會英文或是不敢講英文，從 Google Cloud Certification 最開始只支持英文和日文（現在應該也是，懶得查了）就能看出日本程序員整體的水平。所以主辦方基本都會貼心地為參加者準備好同聲傳譯，每個座位都配備有接收器和耳機。&#10;根據我最近參加的三個不同的Conference的經驗，會英文的優勢其實不在於不需要戴那個同聲傳譯耳機，而是在結束後的「懇親会」可以跟演講嘉賓一對一聊天幾十分鐘，請教問題並交換聯繫方式，屢試不爽（n=3）。因為基本上英文不好又害羞的日本程序員不會跟你競爭。如果是在國際性的Conference，類似這樣的機會恐怕很難得，演講者多半沒有時間跟你一對一聊那麼久。&lt;/p&gt;&#10;&lt;p&gt;英文這個工具，總會在意想不到的地方派上用場。&#10;就像著名程序員原子能說的那樣，&lt;a href="https://www.youtube.com/watch?v=fHFEGfFoMZI" target="_blank" rel="noopener noreferrer"&gt;英語才是最好的計算機語言&lt;/a&gt;。&lt;/p&gt;</description></item><item><title>There are always people who think for you</title><link>https://www.kinjo.tech/posts/20250920/</link><pubDate>Sat, 20 Sep 2025 05:28:59 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250920/</guid><description>&lt;p&gt;所有忘記在元宇宙墳頭上香的阿貓阿狗們在AI時代又打滿雞血復活了。&lt;/p&gt;&#10;&lt;p&gt;「你不用這個AI新工具你就會跟不上時代然後被AI取代」&lt;/p&gt;&#10;&lt;p&gt;「學習新知識沒用因為AI學的比你更快」&lt;/p&gt;&#10;&lt;p&gt;「競爭對手已經開發出AI賦能的新功能了我們不抓緊就會被淘汰」&lt;/p&gt;&#10;&lt;p&gt;「你寫的CLI工具沒毛用我們應該讓開發者跟AI直接用自然語言交互」&lt;/p&gt;&#10;&lt;p&gt;「這個AI工具在歧視中國跟中國用戶」&lt;/p&gt;&#10;&lt;p&gt;從職場，推特，YouTube不斷受到這種論調的轟炸。互聯網泡沫時期我還沒有記事，身處於AI泡沫之中多少能想像當時的情境。&lt;/p&gt;&#10;&lt;p&gt;其實也不一定要身處於泡沫時期，衰退蕭條時期也是一樣，從十幾年前開始我所居住的日本地鐵裡比較常見的脫毛廣告喜歡用的一種利用恐懼或著從眾心理的營銷手段。&#10;諸如&lt;/p&gt;&#10;&lt;p&gt;「你不開始脫毛你就沒有機會接觸到優秀的異性」&lt;/p&gt;&#10;&lt;p&gt;「脫毛能變美」&lt;/p&gt;&#10;&lt;p&gt;「別人20歲都開始脫毛了就你沒有」&lt;/p&gt;&#10;&lt;p&gt;云云，他們的用語更加巧妙沒有我講的那麼直接。主要針對女性，最近可能市場飽和了，男性脫毛也多了起來。一開始我以為沒人會相信這種拙劣的廣告，但能持續這麼多年自然是有利可圖，你情我願。&lt;/p&gt;&#10;&lt;p&gt;甚至不一定要是脫毛無良廣告，日本的部分年輕人收入跑不過通脹生活越來越緊張，甚至買大米都要精打細算。房間裡的大象明明是吸年輕人血去填老人窟窿的這個無法持續的社保系統，本質原因是政府短視，決策失誤還有人口減少的趨勢無法避免。&#10;而多數人卻相信是外國勞動者帶來的問題，國家給外國人發錢搶佔了他們的資源。甚至讓某橘色政黨在7月的參議院選舉中投機成功。&lt;/p&gt;&#10;&lt;p&gt;總有人代替你思考，他們創造需求，製造焦慮，劃分敵我。&#10;科技飛速發展，但人類從「烏合之眾」出版的19世紀末開始（或著更早）並沒有進步。消費了過多的快餐信息，多巴胺分泌次數暴增，系統二已經開始退化。&#10;如何才能獨善其身？&lt;/p&gt;</description></item><item><title>我自己的炒股錯覺</title><link>https://www.kinjo.tech/posts/20250728/</link><pubDate>Mon, 28 Jul 2025 12:59:31 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250728/</guid><description>&lt;p&gt;東京飛往上海的飛機晚點至少2個小時。&#10;在候機的同時回看之前寫的一篇博文叫&lt;a href="../20240419"&gt;打工人炒股的錯覺&lt;/a&gt;，當時的我自信地得出了結論：「股黃金期貨外匯加密貨幣一律不碰」一年三個月後來看我自己也沒有履行這個結論，中間還是經不住誘惑投資了幾隻個股，甚至在4月川普宣布解放日關稅清單的時候出於恐懼賣掉了一部分持有很久的ETF。&lt;/p&gt;&#10;&lt;p&gt;跟很多人一樣我在金錢的利益誘惑與來自社交媒體的噪音輸入之下，並沒有經受住誘惑。甚至在抄底日股成功了兩次之後，開始覺得自己能跑贏標普500，然後開始每天花時間刷新各種證券APP有模有樣地分析圖表，可回頭看看作為玩家參與到這場需要一點點門檻的合法大型賭博，並不開心也完全談不上樂趣，但卻付出了不少時間。&lt;/p&gt;&#10;&lt;p&gt;真的有必要那麼貪婪跟著急嗎？錯過了行情無所謂天塌不下來，通勤地鐵或者旅行的飛機遲延一點無所謂不在乎那幾個小時，讓自己因為這些小事變得焦躁不安很不划算。&lt;/p&gt;</description></item><item><title>自宅Kubernetesクラスタをk3sからkubeadmに乗り換えた</title><link>https://www.kinjo.tech/posts/20250528/</link><pubDate>Wed, 28 May 2025 10:04:13 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250528/</guid><description>&lt;p&gt;2023年年末にk3sを使ってRaspberry Pi 4と10年位前に買った超古いノートPCで自宅Kubernetesクラスタに構築した。構築はめっちゃくちゃ楽だった（数分間で終わる）が、k8s標準のものではなく、addonsや新しいOSSを試す際に謎のバグが多発してdebugも大変だったので、改めてkubeadmを使って構築することにした。クラスタが崩壊したときの復旧を楽にするために、手順を残しておく。もし検索エンジや生成AIの参考リンクなどでこのページにたどり着いた方がいたら、何かの参考になれば嬉しい。&lt;/p&gt;&#10;&lt;p&gt;手順&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;container runtime (containerd)をインストール&lt;/li&gt;&#10;&lt;li&gt;kubeadm, kubelet, kubectlをインストール&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;kubeadm init&lt;/code&gt;でcontroller planeを構築&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;kubeadm join&lt;/code&gt;でnodeをクラスタに追加&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;当たり前のことだが、container runtimeとkubeadm, kubelet, kubectlはcontroller planeとworker nodeの両方にインストールする必要がある。&lt;/p&gt;&#10;&lt;h2 id="環境"&gt;環境&lt;/h2&gt;&#10;&lt;p&gt;Raspberry Pi 4&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;CPU: Cortex-A72, aarch64, 4 cores&lt;/li&gt;&#10;&lt;li&gt;Memory: 8GiB&lt;/li&gt;&#10;&lt;li&gt;OS: Debian GNU/Linux 12 (bookworm)&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Mini PC&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;CPU: Intel(R) N97, x86_64, 4 cores&lt;/li&gt;&#10;&lt;li&gt;Memory: 8GiB&lt;/li&gt;&#10;&lt;li&gt;OS: Ubuntu 24.04.2 LTS&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;古いノートPC&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;CPU: Intel(R) Pentium(R) 3805U @ 1.90GHz, 2 cores&lt;/li&gt;&#10;&lt;li&gt;Memory: 8GiB&lt;/li&gt;&#10;&lt;li&gt;OS: Ubuntu 22.04.5 LTS&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Raspberry Piをcontrol planeにして、残り2台をworkerにする。&lt;/p&gt;&#10;&lt;h2 id="container-runtime-containerdをインストール"&gt;container runtime (containerd)をインストール&lt;/h2&gt;&#10;&lt;p&gt;他のcontainer runtimeでも良いが、無難にcontainerdを使うことにする。今後はCRI-Oで遊んでみるかもしれないが、Docker Engineはたぶん使わない。&lt;/p&gt;</description></item><item><title>用極簡主義來健身</title><link>https://www.kinjo.tech/posts/20250511/</link><pubDate>Sun, 11 May 2025 10:05:35 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250511/</guid><description>&lt;p&gt;諸如健身，學習英語之類的活動，速成基本無效，需要長期堅持才能看到成果。但包括我在內多數人很容易半途而廢。&#10;經過數次失敗於總結教訓後，這次健身我持續了半年，每個禮拜去4到5次，健康狀況跟身材都有了明顯改善。我發現對抗半途而廢，「極簡主義」很實用。&lt;/p&gt;&#10;&lt;p&gt;我們要承認你我中的大多數人是沒有意志力的。所以走增強意志力的路線雖不能說完全不可能但難度不小，更理想的方法應該是不用強大的意志力也能長期堅持。這時候極簡主義中的一個思想就很有用：儘量「不做選擇」或者說「只做簡單的選擇」。&lt;/p&gt;&#10;&lt;p&gt;先來想一下去健身需要做的選擇&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;去什麼健身房&lt;/li&gt;&#10;&lt;li&gt;喝不喝蛋白粉&lt;/li&gt;&#10;&lt;li&gt;氧氣水需要嗎&lt;/li&gt;&#10;&lt;li&gt;需要帶什麼衣服去健身&lt;/li&gt;&#10;&lt;li&gt;什麼時候去健身&lt;/li&gt;&#10;&lt;li&gt;如何選擇器械，需不需要教練&lt;/li&gt;&#10;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;沒去之前看到這些選擇就覺得望而卻步了，即便是堅持了一段時間之後也會覺得麻煩慢慢就放棄了。&#10;仔細想想很多選擇其實是不用做的，除非想追求極致，但那是堅持下來之後的另一個階段這裡就不做討論了。&lt;/p&gt;&#10;&lt;h3 id="健身房選擇"&gt;健身房選擇&lt;/h3&gt;&#10;&lt;p&gt;在我看來就一條原則離家或者公司近，所有追加option都不要。那種又是全球連鎖，又提供最新器械的都沒用，最終去的只會是同一間。&lt;/p&gt;&#10;&lt;h3 id="蛋白粉選擇"&gt;蛋白粉選擇&lt;/h3&gt;&#10;&lt;p&gt;蛋白粉對於增肌來說肯定有幫助，但是從選擇開始，到購買，儲存，沖泡，洗杯子，晾乾。一次還好，每次去健身房後都要做這一整套動作就太過於麻煩了。而且不認真洗那個杯子真的會很臭。&#10;所以很簡單，不喝，或者稍微增加日常飲食的優質蛋白質攝入，偶爾去便利店或超市買散裝的就足夠了。&lt;/p&gt;&#10;&lt;h3 id="氧氣水選擇"&gt;氧氣水選擇&lt;/h3&gt;&#10;&lt;p&gt;氧氣水有種智商稅的感覺，我也懶得去查證了，沒那閒工夫喝。即使有用付出的成本也過高。&lt;/p&gt;&#10;&lt;h3 id="服裝選擇"&gt;服裝選擇&lt;/h3&gt;&#10;&lt;p&gt;為了防止受傷基礎的護具是必須的，但服裝其實沒那麼講究，緊身之外問題都不大。健身專用的服裝買來也會忘記帶，而且除去健身之外也毫無用處。&lt;/p&gt;&#10;&lt;h3 id="健身時間選擇"&gt;健身時間選擇&lt;/h3&gt;&#10;&lt;p&gt;什麼時候去其實無所謂，但儘量做到甩手就能去，或者減少去之前需要做的準備工作，時間固定的話比較能容易形成習慣。另外快步走到健身房就是有氧運動，當作熱身也行。這能有效縮短去健身房的時間，每次的時間縮短了堅持下去也就更容易。&lt;/p&gt;&#10;&lt;h3 id="器械菜單教練的選擇"&gt;器械，菜單，教練的選擇&lt;/h3&gt;&#10;&lt;p&gt;教練其實也不需要，網上的教程都說的很好只是沒辦法給你第一時間的反饋，習慣之後基本就能理解教程中所說的注意事項。菜單也越簡單越好，大多數部位練到了就行，訂製菜單AI很在行。&lt;/p&gt;&#10;&lt;p&gt;另外要儘量縮短健身時間，工作日擠出一兩個小時難度很大，但30-40分鐘應該還是可以的。&#10;比如最開始儘量不做自由重量只選擇機械式器材就行。自由重量雖然效果好，但找啞鈴片，放啞鈴片，收啞鈴片很麻煩也費時間，再者對於新手來說容易受傷。&#10;還有就是發現前面有人在用就立馬換項目，找其它可以代替的器械，功能類似的器械很多不用糾結於每次完全一模一樣，有的大哥大姐一上去二三十分鐘不下來，等他們練完太花時間。&lt;/p&gt;&#10;&lt;h2 id="總結"&gt;總結&lt;/h2&gt;&#10;&lt;p&gt;去離家最近的健身房，不喝蛋白粉氧氣水，基礎護具就行，縮短健身時間。即便意志力不強也比較容易能長期堅持，其實跟二八原理類似用20%的努力來取得80%的健身成果。&#10;切記不要想著用數字量化一切，今天比昨天好或者跟昨天一樣就足夠了。又不是考試也不是KPI考核，最關鍵的是能夠長期堅持，時間到了自然會看到效果。&#10;而且如果哪一天突然對健身或者運動本身產生興趣了再倒回去考慮之前被略過的細節就行。相信有了經驗之後會有完全不一樣的視角。&lt;/p&gt;&#10;&lt;p&gt;好像類似的方法運用到英文學習也是一樣的，有機會可以講講。&lt;/p&gt;</description></item><item><title>Phoenix Suns</title><link>https://www.kinjo.tech/posts/20250510/</link><pubDate>Sat, 10 May 2025 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250510/</guid><description>&lt;p&gt;從2005年開始看NBA，跟大多數人一樣最開始因為姚明的緣故自然而然成為了火箭球迷，姚明退役之後就基本沒有太關注最多也只是在季後賽偶爾看看新聞或者閒著沒事才會看比賽。&lt;/p&gt;&#10;&lt;p&gt;後來哈登來到了火箭之後火箭魂復燃，中間CP3又加入火箭。但不巧跟強盛的宇宙勇同處於一個時代，即便2018年完全搞了一套反勇士的豪華陣容，但CP3受傷以及火箭史詩一般的拉稀命中率導致G7失利。2019年捲土重來又幾乎以同樣的方式再次倒在勇士面前。當時邊寫論文邊看比賽，氣到想把電腦砸爛並發誓再也不看NBA。&lt;/p&gt;&#10;&lt;p&gt;但打臉總是很快到來，2021年CP3來到太陽，疫期間週末沒事做就開始關注這一支已經落寞很久的年輕球隊。這次還拉上了老婆（當時的女朋友）一起看比賽，兩人同時成為了太陽的球迷。本以為這支球隊距離奪冠的距離還很遠，但竟然還馬上就看到了希望，只是後來跟火箭時期的劇本很類似&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;21年 CP3, Booker, Ayton, Mikal, Crowder 總決賽失利，但陣容均衡充滿活力&lt;/li&gt;&#10;&lt;li&gt;22年 首發Crowder換成Johnson，例行賽第一然後倒在西區半決賽，還跟獨行俠上演的了另一場拉稀的G7，也就是The Game，現在聽到Dinwiddie這個名字我還是會頭皮發麻&lt;/li&gt;&#10;&lt;li&gt;23年 Mikal, Johnson換成KD，依然倒在西區半決賽，但跟後來的冠軍金塊打得有來有回，並且在太陽主場看到了Booker的爆發&lt;/li&gt;&#10;&lt;li&gt;24年 梭哈式交易CP3被換成Beal, 制服組丟掉未來追求現在。陣容完全被克制第一輪被灰狼橫掃，而且當時的Ant還沒有進化為完全體&lt;/li&gt;&#10;&lt;li&gt;25年 賽季初西部第一讓人看到希望，但經歷換中鋒KD受傷以及更衣室的一系列問題之後連play-in都沒的打&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;這5個賽季多半的比賽我都看了，最絕望的甚至還不是輸球，Beal手握霸王條款而且在菲尼克斯已經置業同意被交易的可能性很低，所以薪資空間的問題完全得不到解決。另一方面因為前兩年的賭博式交易選秀權早就被全被丟出去了，馬上宣布重建也不可能。看了出局後CEO的採訪，竟是場面話看不到一點對籃球的熱愛。&#10;現在比較現實的解法是就是交易KD跟Booker，特別是把Booker交易到季後賽因為貧攻剛剛輸給勇士的火箭。這樣對於當下的太陽，火箭以及Booker個人來說都是不錯的選擇。但夏天會發生什麼，沒人知道。&lt;/p&gt;&#10;&lt;p&gt;希望一切如願吧🚀&lt;/p&gt;</description></item><item><title>Meta Blog</title><link>https://www.kinjo.tech/posts/20250427/</link><pubDate>Sun, 27 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250427/</guid><description>&lt;p&gt;還沒有寫把博客遷移到這裡的原因，那就在地鐵裡開始動筆寫一篇博客的博客 Meta Blog&lt;/p&gt;&#10;&lt;p&gt;曾經用過的博客服務&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;QQ空間&lt;/li&gt;&#10;&lt;li&gt;新浪博客&lt;/li&gt;&#10;&lt;li&gt;Lofter&lt;/li&gt;&#10;&lt;li&gt;Qiita&lt;/li&gt;&#10;&lt;li&gt;Zenn&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;最開始接觸到博客服務是QQ空間，那時候Blog這個詞才剛剛被翻譯為博客或者部落格，已經忘了是2006年還是2007年。第一次擁有「個人網站」可以簡單的上傳文字和照片，小學生的我自然是很新奇。&lt;/p&gt;&#10;&lt;p&gt;開始寫真正意義上的博客是2009年上高中之前。在BBS認識的朋友很多人都有自己的博客於是跟風開了新浪博客，文字很幼稚但一直堅持寫到了大學一年級入學之後，還因為新浪博客認識了至今從未謀面的同濟大學的學姊。&lt;/p&gt;&#10;&lt;p&gt;大二之後把新浪博客遷移到了网易的Lofter因為UI看起來比其他的服務好看，當時幾乎所有的課都逃掉就算去了也是坐在最後一排，埋頭於自己的興趣（日語，吉他，長跑）。有時間就用文字紀錄下了自己的一些感想，幾乎兩個禮拜左右就能寫一篇。從最開始到大學畢業前關停，只有一位讀者，我也是他博客的唯一一位讀者，可以說是這一位讀者陪伴著我學會了吉他跟日語。&lt;/p&gt;&#10;&lt;p&gt;再次接觸到博客已經過了5, 6年，工作後想把自己的一些嘗試跟想法分享給他人，就開始在Qiita（差不多等價於中國的CSDN）跟Zenn上寫一些技術相關的文章。這也是第一次嘗試用日文寫博客，沒有想像中難跟Master時期寫的報告差不多，因為技術文章的用詞相對單一表達也不複雜。&lt;/p&gt;&#10;&lt;p&gt;平台帶來了便利（比如SEO），但相反也有諸多的限制，某些國家甚至還有言論審查。但無可厚非，就像在註冊帳號的時候沒人閱讀的超長協議書裡面寫的那樣，免費使用別人的服務的時候就默認自己已經成為了別人的商品。&lt;/p&gt;&#10;&lt;p&gt;所以去年年初開始最終決定self-hosting了現在的這個博客，並且同時關停了其它博客帳號。做的事情很簡單，把Markdown文件傳到GitHub, 然後通過static site generator &lt;code&gt;hugo&lt;/code&gt;生成網頁之後部署到Vercel上。&#10;我更需要一個&lt;strong&gt;相對自由&lt;/strong&gt;，&lt;strong&gt;好玩&lt;/strong&gt;， &lt;strong&gt;能夠自我滿足&lt;/strong&gt;的博客。不需要複雜的社交功能，不需要頻繁地查看或者回信，想看的人自然會看，想用什麼語言寫就用什麼語言，沒有KPI不想寫就完全不用寫，任何人都可以看。&lt;/p&gt;&#10;&lt;p&gt;一個牽強的比方：就像是爵士樂一樣。&lt;/p&gt;</description></item><item><title>生成AI時代のSRE【サポーターズCoLab】に登壇した</title><link>https://www.kinjo.tech/posts/20250411/</link><pubDate>Fri, 11 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250411/</guid><description>&lt;p&gt;4月3日、&lt;a href="https://supporterz-seminar.connpass.com/event/347921/" target="_blank" rel="noopener noreferrer"&gt;生成AI時代のSRE【サポーターズCoLab】&lt;/a&gt;に登壇しました。&lt;/p&gt;&#10;&lt;p&gt;資料を公開します。&lt;/p&gt;&#10;&lt;iframe class="speakerdeck-iframe" frameborder="0" src="https://speakerdeck.com/player/63ba57500d4745238a203104041cdb95?slide=1" title="SREが実現する開発者体験の革新" allowfullscreen="true" style="border: 0px; background: padding-box padding-box rgba(0, 0, 0, 0.1); margin: 0px; padding: 0px; border-radius: 6px; box-shadow: rgba(0, 0, 0, 0.2) 0px 5px 40px; width: 100%; height: auto; aspect-ratio: 560 / 315;" data-ratio="1.7777777777777777"&gt;&lt;/iframe&gt;</description></item><item><title>髮小</title><link>https://www.kinjo.tech/posts/20250407/</link><pubDate>Mon, 07 Apr 2025 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250407/</guid><description>&lt;p&gt;在社交媒體上，除了少數面基過的之外，網友大家最多也就是點讚之交，然後從隻言片語中構建著對彼此虛擬的印象。自然而然地就會傾向於臉譜化或者符號化對方的形象，比如左派右派、女權男權、身分認同、小粉紅、網右、精日。&#10;然而現實中人是更加複雜，多元跟矛盾的。觀點相近不是成為朋友的必要條件，當面對具體的人的時候去談論宏大而又空洞的主義意義並不大，我也是最近兩年，特別是30歲之後才理解的。&lt;/p&gt;&#10;&lt;p&gt;小學在英文補習班上認識的朋友（髮小）前段時間來日本旅遊，在我家住了一個禮拜。帶著第一次來的他上山下海看櫻花，走在東京街頭說著所有人都聽不懂的方言，有種時空錯亂的感覺。看到他的拍照、取景找點的水平遠遠超過我這種門外漢，讓我想起了2017年。&lt;/p&gt;&#10;&lt;p&gt;那年他剛剛畢業，因為對學校裡學專業課程相關的工作完全提不起興趣，更想從事cosplay攝影，當時急需要一筆錢去某大城市參加培訓班。但苦於沒有錢，剛剛交換留學回來的我想都沒多想就把剛剛從上海市拿到的留學獎學金大半借給了他。酒桌一別，我也沒有想到因為疫情等諸多因素，時隔7年才再次見面。現在他雖然沒有完全靠攝影為生，但週末業餘時間能接到項目賺點外快的實力還是有的。&lt;/p&gt;&#10;&lt;p&gt;晚上在我家客廳聊天。&#10;他說他堅決要買，房租房太難受了，一刻也等不了，旅行回去馬上就看房。&#10;我就跟他講現在經濟不好不適合上槓桿，最恐怖的是並不知道什麼時候是底，更別談觸底反彈。而且你們公司都裁員兩波了，謹慎點好。&#10;但他沒有要聽勸的意思，很篤定地相信這次下跌已經基本見底，再不買就又要錯過了。&lt;/p&gt;&#10;&lt;p&gt;他還說他堅決不會去東南亞，印度，香港，台灣等地，因為據說那邊歧視中國人而且很危險。&#10;我就跟他講，有機會還是要實地體驗一些才知道，就像你這次來日本一樣，不要妄下結論。國內簡體中文能接受到的訊息很受限並且被審查。當然第二句話沒有說的那麼明顯。&lt;/p&gt;&#10;&lt;p&gt;一起玩的幾天也跟著他領略到了小紅書的威力，按照小紅書的攻略走到的地方都擠滿了像是完成打卡任務的中國年輕遊客，熙熙攘攘。我為了跟他一起完成他搜索的攻略，平均每天走接近3萬步。&#10;只能跟他說第一次來心情完全可以理解，但沒必要把自己搞那麼累。小紅書上說的「必去比打卡地」是別人的想法，沒必要跟著他人的喜好去匆忙地對答案。去真正感興趣的一部分就行了，我們可以悠閒地去咖啡店或者酒吧。但他拿出了那一句難以反駁的標準答案「&lt;strong&gt;來都來了&lt;/strong&gt;」&lt;/p&gt;&#10;&lt;p&gt;沒有試圖完全說服他，因為我知道完全不可能，也沒有必要。&lt;/p&gt;&#10;&lt;p&gt;但以後如果他遇到切身的困難，我想我還是會像以前一樣拉他一把。跟2017年一樣。&lt;/p&gt;</description></item><item><title>旅途中遇到的人們</title><link>https://www.kinjo.tech/posts/20250127/</link><pubDate>Mon, 27 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250127/</guid><description>&lt;p&gt;在DeepSeek的威力之下，大即是正義模式正在崩潰瓦解，比起粗暴的資本堆疊也許這才是科學技術的進步。&lt;/p&gt;&#10;&lt;p&gt;看完新聞發完感慨之後，時隔多年我在農曆春節前夕登上了返回故鄉的飛機。其實已經準備好了電子書，所以就算是6, 7個小時的飛機也沒有那麼難熬。&#10;飛行途中跟鄰座的年輕人自然而然地攀談了起來。「去年大學畢業。旅遊簽證去東京參加修士入學考試。朋友們都已經開始工作或者入學。因為考試完全沒心情觀光旅行。沒有直達航班在陌生的城市轉機」所有的要素跟當年的我出奇地相似，讓我想起了因為考學的壓力大到蕁麻疹發作的2017年年初。&lt;/p&gt;&#10;&lt;p&gt;一個人的旅途中，總會遇到很多有意思的人和事，特別是在臥鋪列車或著青年旅館。&lt;/p&gt;&#10;&lt;p&gt;2007年剛上中學，在去廈門的列車上跟火車上下鋪的惠州大哥（沒記錯的話他叫希希）馬上就混熟了，之後跟他也剛剛認識的幾個大姐姐打狼人殺。具體內容早就忘記了只記得因爲我穿着一件黃白體恤於是他給我取了綽號叫大馬哈魚。這是我人生中第一次交到外地的朋友。&lt;/p&gt;&#10;&lt;p&gt;2012年上大學在去上海的臥鋪列車上遇到一個去浙江打工的大叔說鷹潭車站的雞腿好吃，要給自己老婆帶一個。聽聞我也感興趣之後深夜到鷹潭之後把我叫醒問我要不要買。那時候的臥鋪火車車站之間停的時間足夠跟站在月臺上的小販們買東西。遇到另一個大叔是來自紹興的生意人跟我強調了好多次有學歷才能掙大錢。&lt;/p&gt;&#10;&lt;p&gt;2013年從上海回程的列車上，遇到個大姐用中日雙語跟女兒講話。聽聞我在自學日語並且我們又是同鄉，馬上就開始講述她的傳奇人生，從沒有考上高中到去上海打工，到賭場輸掉30萬，到跟日企高管結婚，到日本婆婆不喜歡她，到她的女兒不想學日文，到她女兒用昂貴的化妝品給狗化妝。列車上的30個小時完全不足以讓我消化這巨大的信息量。&lt;/p&gt;&#10;&lt;p&gt;2014年還是在臥鋪列車上遇到了隔壁大學的同鄉學長，他家境優渥熱衷於做慈善。跟我講述了他組織的各種募捐活動以及自己對佛學的研究。&lt;/p&gt;&#10;&lt;p&gt;2015年西安青旅，跟老闆峰哥聊了起來，他說他嚮往自由，喜歡跟形形色色的人聊天，大學畢業後就離開家鄉在西安大理等地輾轉一直在開青旅。還遇到了一位退休後在世界各地旅行的天津阿姨，聽了她在世界各地旅遊的見聞，得知她的下一步計畫竟然是是去南極。她在爲此進行一系列的準備。&lt;/p&gt;&#10;&lt;p&gt;2016年在伊勢神宮青旅跟一個瑞士人還有日本的一家三口玩UNO，這個瑞士人僅僅學習日文幾個月卻流利到誇張學了三年的我自愧不如，甚至還跟我講比起外來語「オレンジ」來說他覺得「橙色(だいだいいろ)」這種說法更美。&lt;/p&gt;&#10;&lt;p&gt;2017年在上海住青旅，跟老闆白哥聊了起來，他大學畢業沒有就業一人隻身騎行了大半個中國，最後在上海徐匯區開了青旅以近乎難以維持的價格爲來上海求職的年輕人提供住宿。&lt;/p&gt;&#10;&lt;p&gt;雖然他們中的大多數我再也沒有見過，跟他們的小小交集，旅途也變得更有意思。就像「回憶愛瑪儂」的故事情節一樣，如果把時間拉長到無限長，「一瞬間」跟人類所能認知的「永遠」也並沒有區別。&lt;/p&gt;</description></item><item><title>KEDAを導入してAWS SQSキューとHTTPリクエストベースのスケーリングを実現する</title><link>https://www.kinjo.tech/posts/20250123/</link><pubDate>Thu, 23 Jan 2025 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20250123/</guid><description>&lt;p&gt;会社のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2025%2F01%2F23%2F100000"&#10; title="KEDAを導入してAWS SQSキューとHTTPリクエストベースのスケーリングを実現する"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2025/01/23/100000" target="_blank" rel="noopener noreferrer"&gt;KEDAを導入してAWS SQSキューとHTTPリクエストベースのスケーリングを実現する&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>Argo CDとArgo Workflowsにおける認証・認可の設定</title><link>https://www.kinjo.tech/posts/20241216/</link><pubDate>Mon, 16 Dec 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20241216/</guid><description>&lt;h2 id="背景"&gt;背景&lt;/h2&gt;&#10;&lt;p&gt;仕事ではArgoCDでGitOpsを行っており、Argo Workflowsでバッチ処理を実行するエンジンとして利用しています。&lt;/p&gt;&#10;&lt;p&gt;運用初期は利用者が多くなかったため、Argo CDのSSOはGitHub Team (グループ管理)とArgo CDに付随されている&lt;a href="https://argocd-operator.readthedocs.io/en/latest/usage/dex/" target="_blank" rel="noopener noreferrer"&gt;Dex&lt;/a&gt;を使用していました。 また、認可設定はシンプルに保ち、&lt;a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#basic-built-in-roles" target="_blank" rel="noopener noreferrer"&gt;Basic Built-in Roles&lt;/a&gt;を利用して、クラスタ管理者に&lt;code&gt;role:admin&lt;/code&gt;、利用者に&lt;code&gt;role:readonly&lt;/code&gt;権限を付与していました。一方、Argo WorkflowsではSSOを設定せず、デフォルトの&lt;a href="https://argo-workflows.readthedocs.io/en/release-3.6/argo-server-auth-mode/" target="_blank" rel="noopener noreferrer"&gt;Auth Mode&lt;/a&gt;である&lt;code&gt;server&lt;/code&gt;を使っていたため、認証が通れば誰でもバッチのログを閲覧できる状態でした。&lt;/p&gt;&#10;&lt;p&gt;しかし、プラットフォームの成長に伴い、機密性の高いログが増える見込みとなり、現在の運用は対応が難しくなってきました。&lt;/p&gt;&#10;&lt;p&gt;そこで、先日最小権限の原則（Principle of Least Privilege、必要最小限の権限のみを付与する運用方針）に従い、Argo CDとArgo Workflowsの権限設定を見直しました。 本記事では、公式ドキュメントに記載されていない落とし穴や、実践してみて明らかになったArgo CDとArgo Workflows権限のポイントについて解説します。 Argo CDやWorkflows権限設定の権限設定を進める際の参考になれば幸いです。&lt;/p&gt;&#10;&lt;h2 id="argo-cd"&gt;Argo CD&lt;/h2&gt;&#10;&lt;p&gt;先述の通り、今回の構成ではGitHub Teamでグループを管理し、OpenID Connect ProviderとしてDexを利用しています。この構成における認証・認可のフローは、GitHub Teams → Dex → Argo CD Applicationとなります。&lt;/p&gt;&#10;&lt;p&gt;Argo CDでは、グループごとにnamespaceのアクセス権限を設定することで、必要最小限の権限のみを付与できます。 ただし、Argo CDのドキュメント&lt;a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#rbac-configuration" target="_blank" rel="noopener noreferrer"&gt;RBAC Configuration&lt;/a&gt;の記載によると、namespaceごとに設定する場合、事前に&lt;a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/#application-in-any-namespaces" target="_blank" rel="noopener noreferrer"&gt;Application in Any Namespaces&lt;/a&gt;を有効化する必要があります。&lt;/p&gt;&#10;&lt;p&gt;デフォルトでは、Argo CDのApplicationはargocd namespaceに作成されるため、Applicationリソースを各namespaceに分割して配置する必要があります。この設定は比較的簡単な一方で、ApplicationSetのtemplateにおいて&lt;a href="https://github.com/argoproj/argo-cd/issues/11104" target="_blank" rel="noopener noreferrer"&gt;namespaceがサポートされていない&lt;/a&gt;ため、この方法を採用するのは現実的ではありませんでした。すでにApplicationSetによって複数のnamespaceにアプリケーションをデプロイしているため、ApplicationSetの廃止は容易ではありません。&lt;/p&gt;&#10;&lt;p&gt;その解決策として、namespaceごとにAppProjectを作成し、論理的なグループ化を行いました。AppProjectは、Argo CDにおいてアプリケーションをグループ化し、リソースやアクセス制限を設定するためのリソースです。以下は、その例です。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;apiVersion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;argoproj.io/v1alpha1&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;kind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;AppProject&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;samples&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;argocd&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;destinations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;namespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;samples&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;https://kubernetes.default.svc&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;sourceRepos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="s2"&gt;&amp;#34;https://github.com/org/repo.git&amp;#34;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このAppProjectに対応する権限は、Dex設定用のConfigMapに定義します。&lt;/p&gt;</description></item><item><title>EKSにおけるNamespaceごとにCloudWatchとEC2のコスト按分方法</title><link>https://www.kinjo.tech/posts/20240904/</link><pubDate>Wed, 04 Sep 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240904/</guid><description>&lt;p&gt;会社のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2024%2F09%2F04%2F110000"&#10; title="EKSにおけるNamespaceごとにCloudWatchとEC2のコスト按分方法"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2024/09/04/110000" target="_blank" rel="noopener noreferrer"&gt;EKSにおけるNamespaceごとにCloudWatchとEC2のコスト按分方法&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>GKEでArgo WorkflowsにWorkflow Archiveを導入する</title><link>https://www.kinjo.tech/posts/20240709/</link><pubDate>Tue, 09 Jul 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240709/</guid><description>&lt;p&gt;副業先のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fproduct.10x.co.jp%2Fentry%2F2024%2F07%2F09%2F083000"&#10; title="GKEでArgo WorkflowsにWorkflow Archiveを導入する"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://product.10x.co.jp/entry/2024/07/09/083000" target="_blank" rel="noopener noreferrer"&gt;GKEでArgo WorkflowsにWorkflow Archiveを導入する&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>blue/green戦略によるKubernetesクラスタ更新時にIstioの対応方法</title><link>https://www.kinjo.tech/posts/20240705/</link><pubDate>Fri, 05 Jul 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240705/</guid><description>&lt;p&gt;会社のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2024%2F07%2F05%2F110000"&#10; title="blue/green戦略によるKubernetesクラスタ更新時にIstioの対応方法"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2024/07/05/110000" target="_blank" rel="noopener noreferrer"&gt;blue/green戦略によるKubernetesクラスタ更新時にIstioの対応方法&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>ややこしいノートアプリを捨てよう！15行のMakefileとGitで自分のノートを管理する</title><link>https://www.kinjo.tech/posts/20240623/</link><pubDate>Sun, 23 Jun 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240623/</guid><description>&lt;h2 id="なぜ"&gt;なぜ&lt;/h2&gt;&#10;&lt;p&gt;最近のFancyなノートアプリが複雑になりすぎて個人利用には不要な機能が溢れています。&#10;その上、自分が書いたノートなのにローカルに保存されないため、サービス障害やインシデントが起きるとを閲覧できないリスクが高いです。&lt;/p&gt;&#10;&lt;p&gt;そのため、MakefileとGitだけで自分用簡易ノートを作りました。&lt;/p&gt;&#10;&lt;h2 id="詳細"&gt;詳細&lt;/h2&gt;&#10;&lt;p&gt;この15行のMakefileがすべてです。↓&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-makefile" data-lang="makefile"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt; &lt;span class="n"&gt;note&lt;/span&gt; &lt;span class="n"&gt;journal&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;pull&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;git checkout master &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git pull origin master&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;today&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;date +%Y%m%d&lt;span class="sb"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;year&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;date +%Y&lt;span class="sb"&gt;`&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;journal_path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;./journal/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;year&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;newname&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;note&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;@mkdir -p notes &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;# &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; notes/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;_&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.md &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; code notes/&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;_&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;.md&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nf"&gt;journal&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pull&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;@mkdir -p &lt;span class="s2"&gt;&amp;#34;./journal/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;year&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; ! -f &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;journal_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;# &lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;today&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt; &amp;gt;&amp;gt; &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;journal_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; code &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;journal_path&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ややこしいことはやっておらず、directory &lt;code&gt;journal/&lt;/code&gt;と&lt;code&gt;notes/&lt;/code&gt;にmarkdownファイルを作ってvscodeで開いているだけです。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;journal/&lt;/code&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;日記を保存するディレクトリ&lt;/li&gt;&#10;&lt;li&gt;年別に分けるために&lt;code&gt;journal/&lt;/code&gt;配下に更にサブディレクトリ&lt;code&gt;YYYY/&lt;/code&gt;を生成するように実装している&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;make journal&lt;/code&gt;を実行すると&lt;code&gt;journal/YYYY/YYYYMMDD.md&lt;/code&gt;が生成される&lt;/li&gt;&#10;&lt;li&gt;日付などは自動生成されるので自ら記入する必要なし&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;note/&lt;/code&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;その他のノードを保存するディレクトリ&lt;/li&gt;&#10;&lt;li&gt;こちらは年別にわけてない&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;make note name=&amp;quot;hogehoge&amp;quot;&lt;/code&gt;を実行すると&lt;code&gt;notes/YYYMMDD_hogehoge.md&lt;/code&gt;が生成される&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Markdownはコードブロックや画像挿入の機能が備えているため、ほとんど個人のユースケースに対応できます。また、ノードアプリの画像やURLを挿入する際に使う「Markdown方言」を覚える必要もなくなります。&lt;/p&gt;&#10;&lt;p&gt;検索に関しては、自分が普段使い慣れているIDEの検索機能を利用すればいいので、ほとんどのノードアプリより優れていると思います。&lt;/p&gt;&#10;&lt;p&gt;ノートをGitHubのprivate repositoryに上げておくと、GitHubにログインできれば他の端末からでも閲覧・編集できます。モバイルから編集しづらいという問題はもちろんありますが、そもそも外にいる際にスマホをいじる時間を減らして周りの景色を楽しんだほうがいいでしょう。&lt;/p&gt;&#10;&lt;h2 id="最後に"&gt;最後に&lt;/h2&gt;&#10;&lt;p&gt;シンプルなノードアプリを自作しましょう！&#10;ノートアプリを選んでカスタマイズする時間・お金があるのであれば、自分の趣味などに充てたほうがよいのではないでしょうか。&lt;/p&gt;&#10;&lt;p&gt;また、GitHub PagesとMarkdownで自作個人ナレッジマネジメントツールに興味のある方は筆者が2年前に書いた&lt;a href="../20220503"&gt;この記事&lt;/a&gt;も併せて読んでみてください。&lt;/p&gt;&#10;&lt;p&gt;ご参考になれば幸いです。&lt;/p&gt;</description></item><item><title>香港</title><link>https://www.kinjo.tech/posts/20240506/</link><pubDate>Mon, 06 May 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240506/</guid><description>&lt;p&gt;今天凌晨抵達東京，補覺到下午1點才起來本來想打開電腦做副業卻發現身體上的疲勞雖然恢復了但完全不想工作。趁着記憶還新鮮，寫一寫這幾天的直觀感受。&lt;/p&gt;&#10;&lt;h2 id="旅行本身"&gt;旅行本身&lt;/h2&gt;&#10;&lt;p&gt;去香港之前幫朋友買東西的時候，想了想覺得名煙，名酒，手錶，奢侈品都挺無聊的。為了這些東西去打工有點蠢。物慾在得到滿足的一瞬間就會帶來空虛，之後就需要更大更強烈的刺激來填補。&lt;/p&gt;&#10;&lt;p&gt;而旅行帶來的體驗卻完全不一樣，跟家人，朋友，或者自己一個人，從計劃到實行到中間因爲天氣等不可抗因素而必須做出的改變與妥協，在陌生的地方體驗幾天完全不用的生活方式，過後也能從回憶中得到滿足。&lt;/p&gt;&#10;&lt;h2 id="幾個小貼士"&gt;幾個小貼士&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;在東京的話可以考慮在新宿&lt;a href="http://www.interbank.co.jp/" target="_blank" rel="noopener noreferrer"&gt;外貨両替のインターバンク&lt;/a&gt;換港幣（或者其他外幣也一樣），沒有隱藏費用甚至可以比肩香港重慶大廈的匯率。但需要注意不要走錯地方，隔壁有一家沒有牌子的店匯率看上去好一點點但是會收8%的手續費。我因爲大意走錯被坑過一次&lt;/li&gt;&#10;&lt;li&gt;習慣了日本大車站便利的coin lockers之後會發現要在香港找個存行李的地方太難，而且價格也很貴，在中環三個箱子存了4個小時就花了260HKD&lt;/li&gt;&#10;&lt;li&gt;我乘坐紅眼航班沒有問題，但是女朋友顯然體力不支，下次能選的話還是不要安排那麼緊張的行程，至少去的時候可以選擇白天的航班&lt;/li&gt;&#10;&lt;li&gt;乘坐叮叮車（Tramways）的體驗很不錯，能近距離體驗港島的街景，甚至還能包車開Party&lt;/li&gt;&#10;&lt;li&gt;跟當地人交流雖然中文也沒有問題但是因爲我不會廣東話，用英文交流更加順暢&lt;/li&gt;&#10;&lt;li&gt;香港對寵物挺友好公共場合沒有太多限制，或者也有可能是能養寵物的人本身就很少，不用特別限制&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="5年之後"&gt;5年之後&lt;/h2&gt;&#10;&lt;p&gt;乘坐Tramways路過銅鑼灣，中環，金鐘等熟悉地名和街景讓我想起了2019年的那些熬夜的凌晨，連續幾天透過媒體看着前線用雨傘自製的燃燒瓶對抗裝甲車和水跑車的年輕學生們。&lt;strong&gt;5年&lt;/strong&gt;後的現在雞蛋不僅沒有戰勝城牆，而且城牆還被加高加固而且更加密不透風。一邊是賣房子準備跑路的英國加拿大的香港人，一邊是覺得自己靠着低稅收跟相對較低的房價撿了便宜的“新香港人”。&lt;/p&gt;&#10;&lt;p&gt;禮拜天中環HSBC樓下拿着&lt;a href="https://www.planto.hk/zh/blog/domestic-helper-hiring-costs/" target="_blank" rel="noopener noreferrer"&gt;微薄&lt;/a&gt;薪資的&lt;a href="https://theinitium.com/article/20160109-culture-column-thecity09" target="_blank" rel="noopener noreferrer"&gt;菲律賓女傭們&lt;/a&gt;席地而坐，年輕的女孩們在跳舞，唱卡拉OK，邊喝大瓶可樂邊打牌，買賣看起來像是從內地進來的衣物飾品。幾個肥頭大耳的保安保衛着寫字樓的“國界”。顯然她們好像並不屬於港府所喜歡的“新香港人”。而在那種環境下卻依然能夠保持樂觀與積極我很佩服。&lt;/p&gt;&#10;&lt;p&gt;說到我自己，2018年3月宣佈廢除任期的那條新聞讓我打消回中國的念頭，2019年的反送中則是讓我堅定了從法律上脫離中國的決心。但由於跟父母溝通不是很順暢很多時候又是我先斬後奏，趁着這次旅行的機會，在酒店跟快&lt;strong&gt;5年&lt;/strong&gt;沒見的父母深夜促膝長談。互相坦誠地交換了意見，父親說出了下面一句話&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;“我們可能接收不到沒有被管控過的信息，但是能夠理解你的決定。就像是從東德逃到西德，從北朝鮮逃到韓國，當年從內地逃到香港一樣。改變的只是時代背景，人們在做的事情沒有改變”&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;達成了此次旅行的目的，剛剛好目的地是5年後的香港&lt;/p&gt;</description></item><item><title>自作PCからUbuntu22.04開発環境構築してみた</title><link>https://www.kinjo.tech/posts/20220626/</link><pubDate>Sat, 20 Apr 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220626/</guid><description>&lt;p&gt;アップルをはじめとして、最近スペックがそこそこ良いPCがわけわからないくらい高くなり、誰でも使える個人用のコンピュータというPersnal Computerの趣旨と乖離しています。独断と偏見ですが、せっかく数十万円でPCを購入したのにもかかわらず、&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;パーツ交換不可なので、数年後は電子ゴミになり、自然環境に優しくない&lt;/li&gt;&#10;&lt;li&gt;不要なソフトウェア・サービスが多い（たとえば、2022年なのに、バージョン管理非対応のクラウドサービスiCloudが大きなシェアを占めているっていう皮肉）&lt;/li&gt;&#10;&lt;li&gt;なんとかStoreの通知がうるさい。利用するのにアカウント（個人情報）が必要&lt;/li&gt;&#10;&lt;li&gt;プライバシー侵害問題&lt;/li&gt;&#10;&lt;li&gt;&amp;hellip;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;&lt;a href="https://www.theguardian.com/world/2021/oct/15/apple-plan-scan-child-abuse-images-tears-heart-of-privacy" target="_blank" rel="noopener noreferrer"&gt;https://www.theguardian.com/world/2021/oct/15/apple-plan-scan-child-abuse-images-tears-heart-of-privacy&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;別にゲーマーではないですが、数十万払ったのに自由が奪われる感じがどうしても気に入らないので、自作PCからLinuxの開発環境を構築することにしました。&lt;/p&gt;&#10;&lt;h2 id="ハードウェア"&gt;ハードウェア&lt;/h2&gt;&#10;&lt;p&gt;秋葉原まで行く元気がなかったので、すべてのパーツはAmazonやメルカリから購入することにしました。また、PCパーツの価格変動が激しいのでkeepaのメール通知機能を活用すると簡単に低価格でお気に入りのパーツ狙えます。&#10;&lt;a href="https://keepa.com/" target="_blank" rel="noopener noreferrer"&gt;https://keepa.com/&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;最終的に以下のパーツを買いました。&lt;/p&gt;&#10;&lt;table&gt;&#10;&#9;&lt;thead&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Model&lt;/th&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;th&gt;Price&lt;/th&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/thead&gt;&#10;&#9;&lt;tbody&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Motherboard&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ASUS Intel B560M-A&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥10,563&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CPU&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Intel Core i5 10400&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥18,943&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;GPU&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;ASUS GTX 1650-4G DUAL&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥21,500&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Power&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;玄人志向 KRPW-BK 80PLUS Bronze 750W&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥5,535&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Memory&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;KLEVV DDR4 2666 PC4-21300 8GB*2&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥7,002&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Storage&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Crucial SSD 500GB MX500&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥5,980&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CPU Cooler&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;CPUクーラー 虎徹 Mark II&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥3,400&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Case&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;SAMA JAX-03W (second hand)&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥4,480&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;LAN cable&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;¥945&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&#9;&#9;&lt;tr&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;Total&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&#9;&#9;&lt;td&gt;&lt;strong&gt;¥78,348&lt;/strong&gt;&lt;/td&gt;&#10;&#9;&#9;&#9;&lt;/tr&gt;&#10;&#9;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;p&gt;あくまで筆者の例ですが、他のパーツに替えても全然問題がありません。パーツ間の互換性と電源容量だけ気をつけてください&lt;/p&gt;</description></item><item><title>打工人炒股的錯覺</title><link>https://www.kinjo.tech/posts/20240419/</link><pubDate>Fri, 19 Apr 2024 13:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240419/</guid><description>&lt;p&gt;趁着今天全球股市暴跌，來簡單聊聊打工人炒股的錯覺，以及我的策略。&lt;/p&gt;&#10;&lt;p&gt;打開推特好多人都在用中文聊炒股炒幣買房。最近跟公司同事去聚餐只要聊到炒股炒幣炒FX（拉槓桿炒外匯）或者買房，幾乎所有人都滔滔不絕，長篇大論發表觀點。&#10;而一個簡單的事實是，長期來看經濟大餅越確實是在做越大，但短期的股市漲跌取決於市場對於經濟的預期（不是實際情況，只是預期）。牛市之下，大家都能輕鬆靠著炒股短期賺到錢，但牛市永遠不可能長久，短期投資個股或對於大多數打工人來說不見得是個好的策略。&lt;/p&gt;&#10;&lt;p&gt;炒股好像會給我們打工人一種錯覺，會讓我們覺得我們有那麼一瞬間我們從勞方變成了資方，有機會不依靠勞動僅僅靠著決策就能在短期內賺進鈔票。不可否認確實也有人是這樣成功的，但跟書店琳琅滿目的成功學書籍一樣完全不具有任何普遍性和可再現性。而且投資跟投機其實本質上並無差別，都是某種形式上的賭博，靠著承擔一定的風險來獲利。再者，炒股炒幣花費時間精力，最終的營收不僅反應在最後的數字上，還需要考慮到時間成本，精力成本，甚至是心理成本。算上這些看不到的無形成本，炒股的收益率估計也就不剩多少了。&lt;/p&gt;&#10;&lt;p&gt;但是，作為打工人能不炒股嗎？&lt;/p&gt;&#10;&lt;p&gt;顯然完全不炒股也不可取，就連長年通縮的日本也進入了通脹時代，不炒股就只能等著資產縮水。另外，日本政府從幾年前開始學習英國的ISA推出了名為NISA的投資稅收減免政策，其實意思就是說按照現在的少子高齡化進展，以後養老金很大可能會不夠用，所以政府也想鼓勵民眾加入資本市場的遊戲。&lt;/p&gt;&#10;&lt;p&gt;我個人的觀點是，我們首先需要承認並考慮清楚我們的現狀&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;我們多數打工人是凡人不夠聰明&lt;/li&gt;&#10;&lt;li&gt;沒有一手信息源&lt;/li&gt;&#10;&lt;li&gt;運氣不可能一直好&lt;/li&gt;&#10;&lt;li&gt;分析公司財報大多數情況是浪費時間&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;基於以上現實，用免稅額度長期定投風險相對較低的S&amp;amp;P500等優質ETF就完全足夠了，個股黃金期貨外匯加密貨幣一律不碰，甚至不買房（跟&lt;em&gt;A Random Walk Down Wall Street&lt;/em&gt;書中的觀點類似）。把炒股看K線的時間用來多陪伴家人，或者打球，看書，寫碼，練琴等興趣愛好，甚至是刷推特都可能更加有意義。&lt;/p&gt;</description></item><item><title>Elementaryアップデートの試行錯誤</title><link>https://www.kinjo.tech/posts/20240328/</link><pubDate>Thu, 28 Mar 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240328/</guid><description>&lt;p&gt;副業先のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fproduct.10x.co.jp%2Fentry%2F2024%2F03%2F28%2F094606"&#10; title="Elementaryアップデートの試行錯誤"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://product.10x.co.jp/entry/2024/03/28/094606" target="_blank" rel="noopener noreferrer"&gt;Elementaryアップデートの試行錯誤&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>NodeにTaintsがついている場合、Datadog DaemonSetにTolerationsを忘れないでね！</title><link>https://www.kinjo.tech/posts/20240129/</link><pubDate>Mon, 15 Jan 2024 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20240129/</guid><description>&lt;h2 id="マネージド型k8sのdaemonset"&gt;マネージド型k8sのDaemonSet&lt;/h2&gt;&#10;&lt;p&gt;EKSなどのKubernetesマネージドサービスは、DaemonSetを介してNodeにkube-proxy, ebs csi, cniなどのPodを適切に配置してくれます。&lt;/p&gt;&#10;&lt;p&gt;k8sドキュメントに記載している&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/#taints-and-tolerations" target="_blank" rel="noopener noreferrer"&gt;自動的に追加されるTolerations&lt;/a&gt;以外で、ワイルドカードのTolerationsもデフォルトで入っています。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tolerations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Exists&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="eks管轄外のdaemonset"&gt;EKS管轄外のDaemonSet&lt;/h2&gt;&#10;&lt;p&gt;しかし、DatadogなどEKS外でデプロイされたDaemonSet Podsを入れる際に、TaintsがついているNodeにPodがスケジューリングされないため、注意が必要です。&#10;解決方法としては、ドキュメントに書かれているように&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/#taints-and-tolerations" target="_blank" rel="noopener noreferrer"&gt;https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/#taints-and-tolerations&lt;/a&gt;&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;You can add your own tolerations to the Pods of a DaemonSet as well, by defining these in the Pod template of the DaemonSet.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;DaemonSetのPodテンプレートにtolerationsを定義することです。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;tolerations&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;- &lt;span class="nt"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Exists&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;また、このようなワイルドカードのTolerationsを追加するのは便利かもしれないですが、（EKSの場合）Fargateにスケジューリングされてしまうので、Affinityもちゃんと書きましょう&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;affinity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nodeAffinity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;requiredDuringSchedulingIgnoredDuringExecution&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;nodeSelectorTerms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;matchExpressions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;eks.amazonaws.com/compute-type&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;NotIn &lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="l"&gt;fargat&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;余談ですが、同じくDaemonSet介してデプロイされるfluent bitでは、不必要なTolerationsによるバグが過去起きていました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/aws-samples/amazon-cloudwatch-container-insights/issues/61" target="_blank" rel="noopener noreferrer"&gt;https://github.com/aws-samples/amazon-cloudwatch-container-insights/issues/61&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Helmを使用せずにKustomizeでAirbyteをKubernetes上に構築する</title><link>https://www.kinjo.tech/posts/20231219/</link><pubDate>Tue, 19 Dec 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20231219/</guid><description>&lt;p&gt;会社のtech blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2023%2F12%2F19%2F110000"&#10; title="Helmを使用せずにKustomizeでAirbyteをKubernetes上に構築する"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2023/12/19/110000" target="_blank" rel="noopener noreferrer"&gt;Helmを使用せずにKustomizeでAirbyteをKubernetes上に構築する&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>潤日進階: 如何DIY歸化日籍🇯🇵</title><link>https://www.kinjo.tech/posts/cn2jp/readme/</link><pubDate>Wed, 29 Nov 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/cn2jp/readme/</guid><description>&lt;p&gt;本文可以轉載，但請附上這個博客的URL&lt;/p&gt;&#10;&lt;h2 id="寫這篇文章的理由"&gt;寫這篇文章的理由&lt;/h2&gt;&#10;&lt;p&gt;用日文谷歌搜索「日本国籍帰化」結果多半是&lt;a href="https://ja.wikipedia.org/wiki/%E8%A1%8C%E6%94%BF%E6%9B%B8%E5%A3%AB" target="_blank" rel="noopener noreferrer"&gt;行政書士&lt;/a&gt;們用於營業推廣的文章，可能因為利益相關，多數不是很詳細整理的也不是很系統，另外信息也相對較陳舊沒有一直更新。本文的目的是詳細描述DIY歸化日籍的整個過程，為後來人提供方便。&#10;花幾十萬日圓找行政書士也可以節約不少時間，但中國方面的資料依然還是需要自己獲取，大家可以看完這篇文章之後再權衡一下。&lt;/p&gt;&#10;&lt;p&gt;記得當時我DIY日本留學的時候也是在知乎（當年的知乎）上查到一個叫&lt;a href="https://www.zhihu.com/people/su-fei-17" target="_blank" rel="noopener noreferrer"&gt;蘇菲&lt;/a&gt;的前輩寫的文章，然後自己將文中的內容付諸實踐，最後沒有經過語言學校大學畢業後直接旅遊簽證考試然後入學修士。因爲我是小鎮做題家而且家庭條件也完全談不上好，那篇文章可以說是改變了我的人生軌跡。現在我想做的是把我微不足道的經驗分享給想要知道的人。&lt;/p&gt;&#10;&lt;h2 id="前提"&gt;前提&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;本文主要針對於來自中國且已經持有長期簽證在日本留學或者工作並且想要歸化的朋友。如果人還在中國的話，建議首先參考有名的&lt;a href="https://github.com/The-Run-Philosophy-Organization/run/tree/main/%E6%B6%A6%E5%AD%A6%E6%96%B9%E6%B3%95%E8%AE%BA/%E5%90%84%E5%9B%BD%E9%80%89%E6%8B%A9/%E4%BA%9A%E6%B4%B2%E5%92%8C%E5%A4%A7%E6%B4%8B%E6%B4%B2/%E6%97%A5%E6%9C%AC" target="_blank" rel="noopener noreferrer"&gt;潤學&lt;/a&gt;項目的其他文章再來閱讀此文&lt;/li&gt;&#10;&lt;li&gt;本文只談方法論，關於要不要「潤」以及日本適不適合潤等話題本文不做討論&lt;/li&gt;&#10;&lt;li&gt;本文內容全部基於筆者自身2022-23年（或者更早）的經驗，可能會過時或者跟你的情況有所偏差，如果有事實性錯誤歡迎提出issue, PR&lt;/li&gt;&#10;&lt;li&gt;也歡迎其他形式的PR&lt;/li&gt;&#10;&lt;li&gt;歸化所需要的材料因人而異，作爲補充整理到了&lt;a href="https://github.com/jckinjo/cn2jp/tree/master/cases/twitter" target="_blank" rel="noopener noreferrer"&gt;cases&lt;/a&gt;裏，請對比閱讀&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;hr&gt;&#10;&lt;h2 id="歸化的條件"&gt;歸化的條件&lt;/h2&gt;&#10;&lt;p&gt;日本的&lt;a href="https://www.moj.go.jp/MINJI/kokusekiho.html" target="_blank" rel="noopener noreferrer"&gt;國籍法第五條&lt;/a&gt;是這樣寫的，&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;居住条件（国籍法第５条１項１号）&#10;引き続き５年以上日本に住所を有すること&lt;/p&gt;&#10;&lt;p&gt;能力条件（国籍法第５条１項２号）&#10;２０歳以上で本国法によって行為能力を有すること&lt;/p&gt;&#10;&lt;p&gt;素行条件（国籍法第５条１項３号）&#10;素行が善良であること&lt;/p&gt;&#10;&lt;p&gt;生計条件（国籍法第５条１項４号）&#10;自己又は生計を一にする配偶者その他の親族の資産又は技能によって生計を営むことができること&lt;/p&gt;&#10;&lt;p&gt;重国籍防止条件（国籍法第５条１項５号）&#10;国籍を有せず、又は日本の国籍の取得によってその国籍を失うべきこと。&lt;/p&gt;&#10;&lt;p&gt;不法団体条件（国籍法第５条１項６号）&#10;日本国憲法又はその下に成立した政府を暴力で破壊することを企て、若しくは主張し、又はこれを企て、若しくは主張する政党その他の団体を結成し、若しくはこれに加入したことがないこと&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;挑選出其中的一部分稍微詳細說明一下。以下都是慣例沒有明文規定，或者說從官方的資料查不到相應的內容，但在申請的過程中可以瞭解到。&lt;/p&gt;&#10;&lt;p&gt;首先關於「居住條件」，說的是5年但按照一般經驗上來講，我申請過程中瞭解到其中的至少3年必須要工作並且正常納稅，也就是單單來日本留學念了5年書並不能申請。另外這個「引き続き」的定義也值得注意，跟申請用住類似，一年之內離開日本超過150天或者單次超過90天過去的話通常會從0開始計算。也就是有大半年或者有過長時間離開日本的經歷會被判定為將來不想在日本長居。&lt;/p&gt;&#10;&lt;p&gt;關於「素行條件」，就是說你不能偷稅漏稅（包括保險年金）或者過去有案底。&#10;。只要自己遵守規則準時繳納了稅金跟社保完全不是問題，如果就職於正規的公司的話每個月稅跟社保都會直接從工資扣除因此完全不需要擔心。過去就算有過輕微的交通事故，在申請的時候詳細說明情況問題應該也不大。&lt;/p&gt;&#10;&lt;p&gt;「生計條件」就是說你得有個穩定的工作能養活你自己跟你的家人，沒有硬性規定收入多少而且我個人覺得不是很嚴格。&lt;/p&gt;&#10;&lt;p&gt;「重国籍防止条件」這點就更好理解了，日本也不承認雙重國籍也就是說取得日籍之後必須放棄原來的國籍，這對持有中國國籍的人來說應該沒有任何影響，因為中國也不承認雙重國籍。準確來說在中國如果你不是鐮刀應該不可能擁有多本護照。&lt;/p&gt;&#10;&lt;p&gt;下面介紹幾個在申請過程中我所瞭解到的隱藏條件。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;日語水平法律沒有明確要求但至少要有小學低年級水平，面談的時候即便你有N1證書也還是會測試你的日語能力&lt;/li&gt;&#10;&lt;li&gt;如果你剛剛跳槽在新公司未滿一年，通常不會讓你申請，在他們看來頻繁換工作意味着不穩定。在一家公司至少買一年後才能夠申請&lt;/li&gt;&#10;&lt;li&gt;如果有同居人（這裏指的是還沒有結婚的男女朋友），同居人的身份證明跟納稅的材料也需要提交&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="時間線"&gt;時間線&lt;/h2&gt;&#10;&lt;p&gt;下面整理一下歸化的時間線&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;（滿足上述條件之前半年）第一次面談&lt;/li&gt;&#10;&lt;li&gt;（滿足條件之前兩個月）第二次面談&lt;/li&gt;&#10;&lt;li&gt;（滿足條件的當月）第三次面談&lt;/li&gt;&#10;&lt;li&gt;（滿足條件的當月）提交申請&lt;/li&gt;&#10;&lt;li&gt;（申請三個月後）法務局面試&lt;/li&gt;&#10;&lt;li&gt;（面試三個月後）法務局來電&lt;/li&gt;&#10;&lt;li&gt;（兩週後）歸化結果通知&lt;/li&gt;&#10;&lt;li&gt;（一週後）放棄中國國籍拿到歸化相關的證明書&lt;/li&gt;&#10;&lt;li&gt;（一週後）辦理戶籍相關的手續&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;整個過程前後接近1年半，需要有點耐心。還有就是上述時間線是我綜合幾個朋友的平均結果，樣本數很少應該會有不小的偏差，僅供參考。&#10;歸化的通過率很高，走完這個漫長的過程基本上就成功了。換句話來說只要法務局讓你提交申請，成功率就已經在90%以上了。數據來自&lt;a href="https://www.moj.go.jp/MINJI/toukei_t_minj03.html" target="_blank" rel="noopener noreferrer"&gt;日本政府官方的統計&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;乍看很難，如果把歸化看作一個項目，分階段完成的話其實難度也不大。整個過程中你還會發現日本各個行政部門之間儘管做事方式落後缺少互相交流且效率低下，但卻都很很認真能把整件事情事無鉅細地全部辦完，還挺有意思的，扯太遠了，回到主題。&lt;/p&gt;&#10;&lt;h2 id="需要打交道的實體"&gt;需要打交道的實體&lt;/h2&gt;&#10;&lt;p&gt;歸化涉及到的材料很多，並且日本跟中國的材料都需要。並且如果有配偶小孩，或者有工資以外的收入則需要的材料會更多。獲取這些材料需要跟以下實體打交道。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;中國&#10;&lt;ul&gt;&#10;&lt;li&gt;中國戶籍所在地的公證處&lt;/li&gt;&#10;&lt;li&gt;在老家的父母&lt;/li&gt;&#10;&lt;li&gt;中國駐日大使館領事部&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;日本&#10;&lt;ul&gt;&#10;&lt;li&gt;法務局&lt;/li&gt;&#10;&lt;li&gt;区・市役所&lt;/li&gt;&#10;&lt;li&gt;警察局&lt;/li&gt;&#10;&lt;li&gt;年金事務所&lt;/li&gt;&#10;&lt;li&gt;就職的公司&lt;/li&gt;&#10;&lt;li&gt;大學&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;整個過程至少需要去6次法務局，2次中國大使館領事部，2次居住地的区・市役所，1次中國國內的公證處。別的因人而異，具體需要什麼材料以及如何獲取會在後面的詳細步驟中說明。請行政書士可以減少你去法務局的次數，有沒有價值這裏不做判斷。&lt;/p&gt;</description></item><item><title>自作プロキシサーバで海外のサービスを利用しよう</title><link>https://www.kinjo.tech/posts/20231017/</link><pubDate>Tue, 17 Oct 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20231017/</guid><description>&lt;p&gt;&lt;strong&gt;本記事に書かれていることを実践する際は自己責任でお願いします。不都合などが生じた場合、責任を負いかねます。&lt;/strong&gt;&lt;/p&gt;&#10;&lt;h2 id="背景"&gt;背景&lt;/h2&gt;&#10;&lt;p&gt;日本のインターネット環境は比較的自由ですが、海外のサービスを利用際には大人の諸事情によってサイトがリダイレクトされて利用できない場合があります。&#10;制限されるのはあまり気持ちよく感じないのとWeb閲覧時自分のプライバシーを保護するため、&#10;先日自作プロキシサーバを自作しました。&lt;/p&gt;&#10;&lt;p&gt;本記事では主にHowに焦点を当てて紹介します。関連技術のWhatとWhyについては深く言及しないので、公式ドキュメントまたは他の解説記事をご参照ください。&lt;/p&gt;&#10;&lt;p&gt;雑なアーキテクチャ図&#10;&lt;img src="https://www.kinjo.tech/posts/20231017-1_hu_15dfc8f2aefbe8ba.png"&#10; srcset="https://www.kinjo.tech/posts/20231017-1_hu_36d840edccd3610a.png 480w, https://www.kinjo.tech/posts/20231017-1_hu_15dfc8f2aefbe8ba.png 571w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="571" height="231"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;h2 id="利用技術サービス"&gt;利用技術・サービス&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;a href="https://aws.amazon.com/jp/lightsail/" target="_blank" rel="noopener noreferrer"&gt;AWS Lightsail&lt;/a&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;仮想プライベートサーバ&lt;/li&gt;&#10;&lt;li&gt;3ヶ月無料&lt;/li&gt;&#10;&lt;li&gt;$3.5/月&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://muumuu-domain.com/" target="_blank" rel="noopener noreferrer"&gt;ムームードメイン&lt;/a&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;3000~4000円/年（申請するドメインによる）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/ginuerzh/gost/blob/master/README_en.md" target="_blank" rel="noopener noreferrer"&gt;gost&lt;/a&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;OSS&lt;/li&gt;&#10;&lt;li&gt;Webサービスを装うために使う&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://www.cloudflare.com/" target="_blank" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;gostを使うのに必要&lt;/li&gt;&#10;&lt;li&gt;無料プランで十分&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;&lt;a href="https://github.com/FelisCatus/SwitchyOmega" target="_blank" rel="noopener noreferrer"&gt;SwitchyOmega&lt;/a&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;OSS&lt;/li&gt;&#10;&lt;li&gt;クライアント側の設定&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;あくまでも一例で最適解ではないです。&lt;/p&gt;&#10;&lt;h2 id="事前準備"&gt;事前準備&lt;/h2&gt;&#10;&lt;h3 id="vps仮想プライベートサーバを契約"&gt;VPS（仮想プライベートサーバ）を契約&lt;/h3&gt;&#10;&lt;p&gt;今回は、AWSが提供しているLightsailという軽量仮想プライベートサーバのサービスを利用しますが、EC2やHerokuなど他のサービスを利用する場合もまったく問題ありません。リージョンは日本以外（例えばUS）に設定しておきます。AWSのCLIを利用する場合、AWS IAMなどを設定する必要がありますが、今回は基本的にいじらなくても良いです。&#10;Lightsailでサーバを立ち上げたら、AWS CloudShellからサーバにアクセスできます。ローカルに慣れている方はキーをダウンロードして、&lt;code&gt;ssh&lt;/code&gt;でアクセスしても構いません。&lt;/p&gt;&#10;&lt;p&gt;念のため、&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;curl ipinfo.io&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;を実行して、設定しているリージョンの住所と一致しているか確認します。&#10;また、HTTPSを利用するため、&lt;code&gt;Networking -&amp;gt; IPv4 Firewall&lt;/code&gt;からHTTPSを追加しておきます。&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://www.kinjo.tech/posts/20231017-7_hu_105869ab5ff4d356.png"&#10; srcset="https://www.kinjo.tech/posts/20231017-7_hu_9f3f904ff83ba367.png 480w, https://www.kinjo.tech/posts/20231017-7_hu_105869ab5ff4d356.png 821w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="821" height="334"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;</description></item><item><title>GCS composeで32以上のオブジェクトをまとめる方法</title><link>https://www.kinjo.tech/posts/20230817/</link><pubDate>Thu, 17 Aug 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230817/</guid><description>&lt;p&gt;先日の記事にて&lt;a href="../20230816"&gt;パーティション化されたCSVオブジェクトをCloudSQLにimportする方法&lt;/a&gt;を紹介しました。&lt;/p&gt;&#10;&lt;p&gt;SDKを利用して32より多いオブジェクトをまとめる場合、GCPのコミュニティのチュートリアルのコードをそのまま使っていました。（2023.8.10にアーカイブされ済み）&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/GoogleCloudPlatform/community/blob/master/archived/cloud-storage-infinite-compose/index.md" target="_blank" rel="noopener noreferrer"&gt;https://github.com/GoogleCloudPlatform/community/blob/master/archived/cloud-storage-infinite-compose/index.md&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;文章最後に書いてあるように&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;This code is offered for demonstration purposes only, and should not be considered production-ready. Applying it to your production workloads is up to you!&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;コードはデモ用途のため、そのまま本番環境で使うのはで推奨しないです。先日この文を見逃して自分の環境にデプロイし、平常運転1ヶ月後、パーティション化されたCSVオブジェクトの数が増えたらバグが出ました。&lt;/p&gt;&#10;&lt;h2 id="事象"&gt;事象&lt;/h2&gt;&#10;&lt;p&gt;起きていた事象は、&lt;code&gt;compose&lt;/code&gt;によって作られた中間オブジェクトが消されず残り続け、最終的に数TBのとてつもなく大きいオブジェクトが作成されてしまいました。一つのオブジェクトは5TiBまでというGCSの上限を超えてしまうため、処理が失敗しました。&lt;/p&gt;&#10;&lt;h2 id="どこがバグ"&gt;どこがバグ&lt;/h2&gt;&#10;&lt;p&gt;問題は関数&lt;code&gt;compose_and_cleanup&lt;/code&gt;から呼び出している関数&lt;code&gt;delete_objects_concurrent&lt;/code&gt;にあります。オブジェクトをまとめた後、毎回中間オブジェクトを削除していますが、そのdelete処理自体が非同期処理ですべての処理が終了するのを待たずに次の&lt;code&gt;blob.compose&lt;/code&gt;の実行が始まります。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;The delete_objects_concurrent function is very simple, using fire-and-forget delete tasks in an executor. A more robust implementation might check the futures from the submitted tasks.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;チュートリアルの中にちゃんと書いてあります。（もっとロバスト性のある実装は、submit済みのタスクのfuturesをチェックするとのこと）&lt;/p&gt;&#10;&lt;p&gt;まとめようとするオブジェクトの数が少ない場合はほとんど問題がないですが、1000オブジェクトあたりを超えるとdeleteがcompose処理より遅くなるため、&lt;code&gt;next_chunk&lt;/code&gt;が永遠に存在しているままwhileループから脱出できない状態になります。&lt;/p&gt;&#10;&lt;p&gt;delete処理自体は特に制限ないようですが、数百のオブジェクトを削除する場合時間かかるとドキュメントに記載されています。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/storage/docs/deleting-objects#delete-objects-in-bulk" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/storage/docs/deleting-objects#delete-objects-in-bulk&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="解決方法"&gt;解決方法&lt;/h2&gt;&#10;&lt;p&gt;最初に試したことは、記事に書いてあるようにsubmit済みのタスクのfuturesをwait処理でチェックします。つまりすべてのdelete処理が終わるまでに、compose処理を行いません。&#10;（python並行処理の詳細は公式ドキュメントまたは他の記事をご覧ください）&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ALL_COMPLETED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;delete_objects_concurrent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;Delete Cloud Storage objects concurrently.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; Args:&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; blobs (List[storage.Blob]): The objects to delete.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; executor (Executor): An executor to schedule the deletions in.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; client (storage.Client): Cloud Storage client to use.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Deleting slice &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blob&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ALL_COMPLETED&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Deleted &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blobs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; objects&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;しかし大量なオブジェクトを1つずつ削除していること自体は変わらないので、試してみたら処理が途中でtimeoutになりました。&#10;&lt;a href="https://cloud.google.com/storage/docs/batch" target="_blank" rel="noopener noreferrer"&gt;batch&lt;/a&gt;を利用して削除処理をバッチにまとめる方法もありますが、ロジックをシンプルにしたいので、他の方法を考えました。&lt;/p&gt;</description></item><item><title>パーティション化されたCSVファイルをCloudSQLにimportする方法</title><link>https://www.kinjo.tech/posts/20230816/</link><pubDate>Wed, 16 Aug 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230816/</guid><description>&lt;h2 id="問題"&gt;問題&lt;/h2&gt;&#10;&lt;p&gt;パーティション化されたCSVファイルをCloudSQLにimportする場面は時々あると思います。&#10;残念ながらCloudSQLはBigQueryのようにwildcardsによるimportを対応していません。需要はあるようですが↓&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://issuetracker.google.com/issues/132058570?pli=1" target="_blank" rel="noopener noreferrer"&gt;https://issuetracker.google.com/issues/132058570?pli=1&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;ファイルごとにimportするとオーバーヘッドが毎回発生するため、速度的に実用性があまりないと思います。一方、importはオペレーションの1種なので、並列処理はできません。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/sql/docs/troubleshooting#import-export" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/sql/docs/troubleshooting#import-export&lt;/a&gt;&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;HTTP Error 409: Operation failed because another operation was already in progress.&#10;There is already a pending operation for your instance. Only one operation is allowed at a time. Try your request after the current operation is complete.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;なので、ファイルを結合してimportするのはより現実的な解決策だと思います。&lt;/p&gt;&#10;&lt;h2 id="gsutil-compose"&gt;&lt;code&gt;gsutil compose&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;gsutil compose&lt;/code&gt;を利用すると、GCSにある複数のファイルを結合できます。&#10;cliのみならず、SDK(&lt;code&gt;google.cloud.storage.Blob.compose&lt;/code&gt;)も同じ機能が提供されています。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/storage/docs/composing-objects#create-composite-client-libraries" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/storage/docs/composing-objects#create-composite-client-libraries&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/storage/docs/gsutil/commands/compose" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/storage/docs/gsutil/commands/compose&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;ただし、結合できるファイルは最大32個という制約があります。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;There is a limit (currently 32) to the number of components that can be composed in a single operation.&lt;/p&gt;&#10;&lt;/blockquote&gt;</description></item><item><title>AirflowからDataformにdata_interval_endなどのcontext変数を渡す方法</title><link>https://www.kinjo.tech/posts/20230524/</link><pubDate>Wed, 24 May 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230524/</guid><description>&lt;p&gt;先日GCPのDataformがGAリリースされました。&#10;せっかくなので、まずAirflowにある既存ワークフローの一部をDataformで書き換えようと思いました。&lt;/p&gt;&#10;&lt;h2 id="airflowからdataformをトリッガーする"&gt;AirflowからDataformをトリッガーする&lt;/h2&gt;&#10;&lt;p&gt;ドキュメントを調べると、AirflowからDataformをトリッガーするoperatorはすでに存在しています。&#10;&lt;a href="https://cloud.google.com/dataform/docs/schedule-executions-composer#create_an_airflow_dag_that_schedules_workflow_invocations" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/dataform/docs/schedule-executions-composer#create_an_airflow_dag_that_schedules_workflow_invocations&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;簡単にまとめると&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;DataformCreateCompilationResultOperator&lt;/code&gt;: sqlxをsqlにコンパイルする&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;DataformCreateWorkflowInvocationOperator&lt;/code&gt;: sqlを実行する&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;しかし、どのようにAirflowからDataformへ変数を渡すかについてはドキュメントに記載されていません。&lt;/p&gt;&#10;&lt;h2 id="dataformに変数を渡す"&gt;Dataformに変数を渡す&lt;/h2&gt;&#10;&lt;p&gt;まず、Dataformの設定ファイルdataform.jsonに変数varsを追加しておきましょう。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultSchema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;dataform&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;assertionSchema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;dataform_assertions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;warehouse&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;bigquery&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultDatabase&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;project-stg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultLocation&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;asia-northeast1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;vars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;bq_suffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_stg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;execution_date&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;2023-05-24&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;DataformCreateCompilationResultOperatorのソースを調べてみたところ、compilation_resultという引数があることを発見しました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/apache/airflow/blob/739e6b5d775412f987a3ff5fb71c51fbb7051a89/airflow/providers/google/cloud/operators/dataform.py#LL73C29-L73C46" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/blob/739e6b5d775412f987a3ff5fb71c51fbb7051a89/airflow/providers/google/cloud/operators/dataform.py#LL73C29-L73C46&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;compilation_resultの中身を確認するため、APIの詳細を調べました。&#10;&lt;a href="https://cloud.google.com/dataform/reference/rest/v1beta1/CodeCompilationConfig" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/dataform/reference/rest/v1beta1/CodeCompilationConfig&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;CodeCompilationConfig&lt;/code&gt;内に&lt;code&gt;vars&lt;/code&gt;という変数を指定できるようです。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-json" data-lang="json"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultDatabase&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultSchema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;defaultLocation&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;assertionSchema&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;vars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;string:&lt;/span&gt; &lt;span class="err"&gt;string,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="err"&gt;...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;databaseSuffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;schemaSuffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nt"&gt;&amp;#34;tablePrefix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;string&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;BigQueryのsuffixを&lt;code&gt;code_compilation_config&lt;/code&gt;の&lt;code&gt;vars&lt;/code&gt;へ渡してみたら問題なく実行できました。ちなみに、Dataform側からは&lt;code&gt;dataform.projectConfig.vars.bq_suffix&lt;/code&gt;で変数を呼び出せます。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;DataformCreateCompilationResultOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;create_compilation_result&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repository_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;compilation_result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;git_commitish&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GIT_COMMITISH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;code_compilation_config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;vars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;bq_suffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;_stg&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="dataformにcontext変数を渡す"&gt;Dataformにcontext変数を渡す&lt;/h2&gt;&#10;&lt;p&gt;増分処理する際によく&lt;code&gt;data_interval_end&lt;/code&gt;などの&lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/templates-ref.html#variables" target="_blank" rel="noopener noreferrer"&gt;context変数&lt;/a&gt;を利用して当日の差分だけ取り入れます。&#10;しかし、&lt;code&gt;DataformCreateCompilationResultOperator&lt;/code&gt;では&lt;code&gt;template_fields&lt;/code&gt;が実装されていないため、直接&lt;code&gt;{{ data_interval_end }}&lt;/code&gt;のようなjinjaテンプレートを渡すことはできません。&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/tutorial/taskflow.html" target="_blank" rel="noopener noreferrer"&gt;TaskFlow&lt;/a&gt;で&lt;code&gt;DataformCreateCompilationResultOperator&lt;/code&gt;をラッピングすれば前述の問題を解決できます&lt;/strong&gt;。&lt;code&gt;data_interval_end&lt;/code&gt;は&lt;code&gt;context&lt;/code&gt;から取得します。ポイントとしては&lt;code&gt;DataformCreateCompilationResultOperator&lt;/code&gt;を返す際に&lt;code&gt;execute()&lt;/code&gt;を呼び出す必要があります。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;airflow.decorators&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@task&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_compilation_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;execute_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;data_interval_end&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_timezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Asia/Tokyo&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;%Y-%m-&lt;/span&gt;&lt;span class="si"&gt;%d&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DataformCreateCompilationResultOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;create_compilation_result&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REGION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;repository_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;REPOSITORY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;compilation_result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;git_commitish&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;GIT_COMMITISH&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;code_compilation_config&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;vars&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;execute_date&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;execute_date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;bq_suffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bq_suffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最終的なDAGは以下のようになります。&lt;/p&gt;</description></item><item><title>Apache Airflowのコミッターになった話</title><link>https://www.kinjo.tech/posts/20230511/</link><pubDate>Thu, 11 May 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230511/</guid><description>&lt;h2 id="google-providersのバグを見つけた"&gt;Google Providersのバグを見つけた&lt;/h2&gt;&#10;&lt;p&gt;先日DAGを開発中にGoogle Providers (&lt;code&gt;apache-airflow-providers-google==8.9.0&lt;/code&gt;)の&lt;code&gt;CloudDataTransferServiceJobStatusSensor&lt;/code&gt;を使用したところ、&#10;&lt;code&gt;project_id&lt;/code&gt;はオプション引数であるにも関わらず、省略するとエラーが発生するというバグに遭遇しました。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2023&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;03&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;09&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;02&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;taskinstance&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;py&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1774&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="n"&gt;ERROR&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="n"&gt;failed&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Traceback&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;most&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/airflow/.local/lib/python3.8/site-packages/airflow/sensors/base.py&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;236&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;poke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/airflow/.local/lib/python3.8/site-packages/airflow/providers/google/cloud/sensors/cloud_storage_transfer_service.py&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;91&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;poke&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;operations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;list_transfer_operations&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/airflow/.local/lib/python3.8/site-packages/airflow/providers/google/cloud/hooks/cloud_storage_transfer_service.py&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;list_transfer_operations&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;request_filter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inject_project_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request_filter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FILTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FILTER_PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;File&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;/home/airflow/.local/lib/python3.8/site-packages/airflow/providers/google/cloud/hooks/cloud_storage_transfer_service.py&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="mi"&gt;459&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_inject_project_id&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;AirflowException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;airflow&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AirflowException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;The&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="n"&gt;must&lt;/span&gt; &lt;span class="n"&gt;be&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt; &lt;span class="n"&gt;either&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="n"&gt;project_id&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="nb"&gt;filter&lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt; &lt;span class="n"&gt;parameter&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;project_id&lt;/span&gt; &lt;span class="n"&gt;extra&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Google&lt;/span&gt; &lt;span class="n"&gt;Cloud&lt;/span&gt; &lt;span class="n"&gt;connection&lt;/span&gt; &lt;span class="n"&gt;definition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt; &lt;span class="n"&gt;Both&lt;/span&gt; &lt;span class="n"&gt;are&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="err"&gt;!&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;修正自体はそれほど困難に見えなかったため、Airflowにissueを報告するよりも、自分で直接修正に取り組むことにしました。&lt;/p&gt;&#10;&lt;h2 id="contributor手順を読んで環境構築する"&gt;Contributor手順を読んで環境構築する&lt;/h2&gt;&#10;&lt;p&gt;むやみにコーディングするより、まずCONTRIBUTINGを読んだほうが良いと思い、下記のドキュメンを見つけました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/apache/airflow/blob/main/CONTRIBUTING.rst" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/blob/main/CONTRIBUTING.rst&lt;/a&gt;&#10;けっこう長いので、前半をさらさらと読んで&lt;code&gt;Contribution Workflow&lt;/code&gt;を参照しながら、ローカルの開発環境を問題なく構築しました。躓きそうなところ基本的にドキュメントにまとめてもらっています。&lt;/p&gt;&#10;&lt;h2 id="開発"&gt;開発&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/apache/airflow/pull/30035/files#diff-2118fb849310fd85b9768e6732ab2dfa60ed75c751b5b9d0e176bcd1f950b6bbR75-R109" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/pull/30035/files#diff-2118fb849310fd85b9768e6732ab2dfa60ed75c751b5b9d0e176bcd1f950b6bbR75-R109&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;まず他のところを真似して&lt;code&gt;project_id&lt;/code&gt;を指定しない場合の単体テスクを書きます。何も実装していないので、もちろんテストはコケます。その後、&lt;code&gt;CloudDataTransferServiceJobStatusSensor&lt;/code&gt;の実装を下記のように&lt;code&gt;project_id&lt;/code&gt;を明示的に指定しない場合、&lt;code&gt;hook.project_id&lt;/code&gt;から取得できるように変更します。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;- request_filter={&amp;#34;project_id&amp;#34;: self.project_id, &amp;#34;job_names&amp;#34;: [self.job_name]}&#10;+ request_filter={&amp;#34;project_id&amp;#34;: self.project_id or hook.project_id, &amp;#34;job_names&amp;#34;: [self.job_name]}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;これで終わり！PRを投げてPRを待ちます。&#10;一週間もかからずApache Software Foundationメンバーの方からApproveをもらいました。&#10;&lt;img src="https://www.kinjo.tech/posts/20230511-1_hu_93d1a3d6d5f81bc4.png"&#10; srcset="https://www.kinjo.tech/posts/20230511-1_hu_2426e5b4220ffcf9.png 480w, https://www.kinjo.tech/posts/20230511-1_hu_93d1a3d6d5f81bc4.png 733w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="733" height="497"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;h2 id="受け入れテスト"&gt;受け入れテスト&lt;/h2&gt;&#10;&lt;p&gt;2週間後「&lt;a href="https://pypi.org/project/apache-airflow-providers-google/8.12.0rc1/" target="_blank" rel="noopener noreferrer"&gt;apache-airflow-providers-google 8.12.0rc1&lt;/a&gt;をリリースされたので、リリースのテストをお願いします」の連絡がissueから来ました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/apache/airflow/issues/30427" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/issues/30427&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;8.12.0rc1&lt;/code&gt;をインストールし実際に&lt;code&gt;CloudDataTransferServiceJobStatusSensor&lt;/code&gt;の動作を検証してみたら特に問題なかったので、うまく動いたよと返信しました。&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://www.kinjo.tech/posts/20230511-2_hu_48646f6654b777a3.png"&#10; srcset="https://www.kinjo.tech/posts/20230511-2_hu_b2e5f433a4b5ea66.png 480w, https://www.kinjo.tech/posts/20230511-2_hu_48646f6654b777a3.png 733w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="733" height="182"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;</description></item><item><title>Slack BoltとCloud Functionsでデータ基盤アクセス承認システムを開発</title><link>https://www.kinjo.tech/posts/20230420/</link><pubDate>Thu, 20 Apr 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230420/</guid><description>&lt;p&gt;会社のTech Blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2023%2F04%2F20%2F110000"&#10; title="Slack BoltとCloud Functionsでデータ基盤アクセス承認システムを開発"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2023/04/20/110000" target="_blank" rel="noopener noreferrer"&gt;Slack BoltとCloud Functionsでデータ基盤アクセス承認システムを開発&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>Airflowの単体テストを書きましょう</title><link>https://www.kinjo.tech/posts/20230406/</link><pubDate>Thu, 06 Apr 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230406/</guid><description>&lt;p&gt;データ基盤は下流の分析・可視化・モデリングの「基盤」となるので、品質の担保は言うまでもなく重要ですね。品質を確保するには、ワークフローの監視・検証、ワークフローのテスト、そして加工用クエリのテストがいずれも欠かせません。この記事では、ワークフロー（Airflow）の単体テスト方法について紹介します。また、ワークフローの監視・検証に関しては、過去の記事も合わせてご覧いただけると幸いです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/08/18/110000" target="_blank" rel="noopener noreferrer"&gt;ワークフローの監視&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="../20221101"&gt;ワークフローの検証&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="dagの単体テスト"&gt;DAGの単体テスト&lt;/h2&gt;&#10;&lt;p&gt;まずは、DAGの単体テストについて説明します。厳密に言えば、DAGの実行ではなく、DAGが正確に構築されたかどうかのテストを行います。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#unit-tests" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow/stable/best-practices.html#unit-tests&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;Airflowの公式ベストプラクティスでは簡潔に紹介されていますが、具体例を挙げてさらに詳しく説明しましょう。&lt;/p&gt;&#10;&lt;h3 id="importのテスト"&gt;importのテスト&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;importが正常にできることを確認する（importが失敗するとWeb UIからも確認できるが、単体テストする時点で確認するともっと便利）&lt;/li&gt;&#10;&lt;li&gt;import時間を制限する。（冗長なDAGがあると解析するのに時間がかかるので、import時間を制限することで、事前に冗長なDAGを発見できる）&lt;/li&gt;&#10;&lt;li&gt;最低でも1つのタスクが含まれていることを確認する。&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;unittest&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;airflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestImportDags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;IMPORT_TIMEOUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;120&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@classmethod&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUpClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag_stats&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_import_dags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_errors&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DAG import failures. Errors: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;import_errors&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_import_dags_time&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;total_seconds&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertLess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IMPORT_TIMEOUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_dags_have_at_least_one_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dags&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not exsit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertGreater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hasn&amp;#39;t any tasks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="dagの設定"&gt;DAGの設定&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;DAGタイムアウト設定が必ず行われていることを確認する&lt;/li&gt;&#10;&lt;li&gt;失敗時のアラート通知用のcallback関数が必ず行われていることを確認する&lt;/li&gt;&#10;&lt;li&gt;DAGにタグが付与されていることを確認する（タグはDAGの分類や検索に役立つ）&lt;/li&gt;&#10;&lt;li&gt;catchupを無効になっているかを確認する（場合によって有効でも構わない）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestDagsSetting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@classmethod&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUpClass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DagBag&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_dag_has_necessary_setting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagbag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dags&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertIsNotNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dagrun_timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hasn&amp;#39;t dagrun timeout&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertIsNotNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;on_failure_callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hasn&amp;#39;t on_failure_callback&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertIsNotNone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; hasn&amp;#39;t tags&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;catchup of DAG &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; was not False&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これらのテストを通すことで、DAGの基本的な構成が適切であることが確認でき、DAGの運用上の問題を未然に防ぐことができます。&lt;/p&gt;</description></item><item><title>AirflowからAirbyteをトリッガーする際にハマるポイント</title><link>https://www.kinjo.tech/posts/20230306/</link><pubDate>Mon, 06 Mar 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230306/</guid><description>&lt;p&gt;&lt;a href="https://docs.airbyte.com/operator-guides/using-the-airflow-airbyte-operator/" target="_blank" rel="noopener noreferrer"&gt;https://docs.airbyte.com/operator-guides/using-the-airflow-airbyte-operator/&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;AirflowからAirbyte Operatorを利用するための設定について、Airbyte公式の記事は既にわかりやすくまとめています。実際に試してみて、少しハマったところがあったので、その知見を共有したいと思います。&lt;/p&gt;&#10;&lt;h2 id="1-airflowを230以上にアップグレードする必要がある"&gt;1. Airflowを&lt;code&gt;2.3.0&lt;/code&gt;以上にアップグレードする必要がある&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;apache-airflow-providers-airbyte[http]&lt;/code&gt;を利用するのにAirflowを2.3.0以上に上げないといけません。(&lt;code&gt;apache-airflow-providers-airbyte[http]&lt;/code&gt;を&lt;code&gt;docker-composer.yml&lt;/code&gt;の&lt;code&gt;_PIP_ADDITIONAL_REQUIREMENTS&lt;/code&gt;に追加することも忘れずに)&#10;Cloud Composerなどを利用している場合、GUIからアップグレード可能です。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow-providers-airbyte/stable/index.html" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow-providers-airbyte/stable/index.html&lt;/a&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;x-airflow-common&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;&amp;amp;airflow-common&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;apache/airflow:2.3.4-python3.8&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cp"&gt;&amp;amp;airflow-common-env&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;PYTHONPATH&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;/opt/airflow/dags&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AIRFLOW__CORE__EXECUTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;CeleryExecutor&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AIRFLOW__CORE__SQL_ALCHEMY_CONN&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;postgresql+psycopg2://airflow:password@postgres/airflow&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AIRFLOW__CELERY__RESULT_BACKEND&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;db+postgresql://airflow:password@postgres/airflow&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AIRFLOW__CELERY__BROKER_URL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;redis://:@redis:6379/0&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;AIRFLOW__API__AUTH_BACKEND&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;airflow.api.auth.backend.basic_auth&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;# 追加&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;_PIP_ADDITIONAL_REQUIREMENTS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;apache-airflow-providers-airbyte[http]==3.2.0&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="2-airflowの古いバージョンから234上げるとdocker-composeがバグる"&gt;2. Airflowの古いバージョンから&lt;code&gt;2.3.4&lt;/code&gt;上げるとdocker-composeがバグる&lt;/h2&gt;&#10;&lt;p&gt;airflow 2.2.xでは問題なく環境構築できていましたが、イメージを&lt;code&gt;apache/airflow:2.3.4-python3.8&lt;/code&gt;に変更して&lt;code&gt;docker compose up airflow-init&lt;/code&gt;を実行したら怒られます。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;You are running pip as root. Please use &amp;#39;airflow&amp;#39; user to run pip!&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;Airflowの古い&lt;code&gt;docker-composer.yml&lt;/code&gt;のバグのようなので、&#10;&lt;a href="https://github.com/apache/airflow/pull/23517/files" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/pull/23517/files&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;services -&amp;gt; airflow-init -&amp;gt; environment&lt;/code&gt;に&lt;code&gt;_PIP_ADDITIONAL_REQUIREMENTS: ''&lt;/code&gt;を追加すれば解決できます。&lt;/p&gt;</description></item><item><title>Cloud Composerでmax_active_tasks_per_dagのデフォルト値が機能していない問題</title><link>https://www.kinjo.tech/posts/20230208/</link><pubDate>Wed, 08 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230208/</guid><description>&lt;h2 id="問題"&gt;問題&lt;/h2&gt;&#10;&lt;p&gt;先日Cloud Composerの環境を↓にバージョンアップしました。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Cloud Composer 2.0.32&#10;Airflow 2.2.5&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;core.max_active_tasks_per_dag&lt;/code&gt;という一つのDAG内同時に処理できるタスクの上限を設定するパラメータがデフォルト値16のままになっているのにも関わらず、実行するタスクの上限が明らかに16を超えています。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#max-active-tasks-per-dag" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#max-active-tasks-per-dag&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;ローカルにある&lt;code&gt;Airflow 2.2.5&lt;/code&gt;環境では何の異常もなく、ComposerのAirflow Configurationを確認したところ、なぜか&lt;code&gt;core.dag_concurrency&lt;/code&gt;が100に設定されています。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;[core]&#10;dags_folder = /home/airflow/gcs/dags&#10;plugins_folder = /home/airflow/gcs/plugins&#10;executor = CeleryExecutor&#10;dags_are_paused_at_creation = True&#10;load_examples = False&#10;donot_pickle = True&#10;dagbag_import_timeout = 300&#10;default_task_retries = 2&#10;killed_task_cleanup_time = 3570&#10;parallelism = 0&#10;non_pooled_task_slot_count = 100000&#10;dag_concurrency = 100&#10;....&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;code&gt;core.dag_concurrency&lt;/code&gt;の役割は&lt;code&gt;core.max_active_tasks_per_dag&lt;/code&gt;と同じく、一つのDAG内同時に処理できるタスクの上限を設定しています。Airflow 2.2.0からはすでにDeprecatedになったはずなのに、なぜか残り続いています。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#dag-concurrency-deprecated" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow/stable/configurations-ref.html#dag-concurrency-deprecated&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="試み"&gt;試み&lt;/h2&gt;&#10;&lt;p&gt;手動で削除しようと思ったですけど、バージョンを上げたのでCloud Composer -&amp;gt; AIRFLOW CONFIGURATION OVERRIDESに&lt;code&gt;core.dag_concurrency&lt;/code&gt;というパラメータすら存在しませんでした。&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://www.kinjo.tech/posts/20230208-1_hu_e23c4e8a90e5edd0.png"&#10; srcset="https://www.kinjo.tech/posts/20230208-1_hu_6caf5f7b48598a84.png 480w, https://www.kinjo.tech/posts/20230208-1_hu_e23c4e8a90e5edd0.png 777w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="777" height="245"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;</description></item><item><title>Cloud SQLにある大量なテーブルをBigQueryに入れる話</title><link>https://www.kinjo.tech/posts/20230202/</link><pubDate>Thu, 02 Feb 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230202/</guid><description>&lt;h2 id="経緯"&gt;経緯&lt;/h2&gt;&#10;&lt;p&gt;昨年度から大規模なデータ分析基盤の構築に携わっています。最近Cloud SQLにある6つのDBの数百個のテーブルを日次洗い替えでBigQueryにあるデータ基盤に入れるタスクを取り組んでいます。&#10;Cloud SQLとBigQuery両方ともGCPのサービスのため、federated queriesを利用すると簡単にできそうに見えますが、&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/bigquery/docs/federated-queries-intro" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/federated-queries-intro&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;実際に行ってみると、以下の3つの課題を気づきました。&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;BigQuery側でスキーマ情報を含めたテーブルを一々作成するのは現実的ではない&lt;/li&gt;&#10;&lt;li&gt;プロダクトの進化とともにテーブル・カラムが頻繁に作成・変更されるため、BigQuery側でも対応しないといけない&lt;/li&gt;&#10;&lt;li&gt;Cloud SQLにあるテーブルの定義をそのまま取ってきてもBigQueryではMySQLとPostgreSQLの一部の型が対応されていない&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/bigquery/docs/tables#sql_1" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/tables#sql_1&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;少し苦労していましたが、幸い解決方法を見つけました。&#10;今後躓く方もいるかもしれないので、知見を共有したいと思います。&lt;/p&gt;&#10;&lt;h2 id="bigquery側でスキーマ情報を含めたテーブルを一々作成するのは現実的ではない"&gt;BigQuery側でスキーマ情報を含めたテーブルを一々作成するのは現実的ではない&lt;/h2&gt;&#10;&lt;p&gt;BigQueryはクエリの結果によってテーブルを作成できるので、事前にテーブルを作っておく必要がありません。&#10;&lt;a href="https://cloud.google.com/bigquery/docs/tables#sql_1" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/tables#sql_1&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;大量なテーブルを一々作成するのは現実的ではない課題の解決法としてはDBのメタ情報（descriptionを含めて）をそのまま生かしてテーブル作成用クエリを生成し、テーブルを作成します。&lt;/p&gt;&#10;&lt;p&gt;例えばPostgreSQLの場合、まずはテーブルのメタ情報&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;obj_description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;relid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_statio_all_tables&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{YOUR_SCHEMA}&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;とカラムのメタ情報を取得します。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;column_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pgd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_statio_all_tables&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;INNER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pg_catalog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pg_description&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pgd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pgd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objoid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relid&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;RIGHT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;JOIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;information_schema&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pgd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objsubid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ordinal_position&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;schemaname&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;and&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;table_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;relname&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_schema&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{YOUR_SCHEMA}&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;上記のメタ情報をfor文で回してテーブル作成用のクエリテンプレートの&lt;code&gt;$columns&lt;/code&gt; &lt;code&gt;$pg_schema&lt;/code&gt; &lt;code&gt;$table_name&lt;/code&gt;に入れたら、テーブル作成用のクエリが生成されます。Airflowとかで定期的にクエリを実行すれば問題解決です。&lt;/p&gt;</description></item><item><title>M1 MacでDocker DesktopからRancher Desktopに移行</title><link>https://www.kinjo.tech/posts/20230119/</link><pubDate>Thu, 19 Jan 2023 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20230119/</guid><description>&lt;p&gt;&lt;a href="https://www.docker.com/pricing/october-2022-pricing-change-faq/" target="_blank" rel="noopener noreferrer"&gt;https://www.docker.com/pricing/october-2022-pricing-change-faq/&lt;/a&gt;&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;The list price of the Docker Business subscription will go up by $3, to $24 per user per month&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;2022年10月のお知らせですが、Docker Desktop Business subscriptionがなんと8倍値上げ！！&#10;コスト面の理由でRancher Desktopに移行することになりました&lt;/p&gt;&#10;&lt;p&gt;移行する際に、Rancher Desktopの2つバグを見つけました。これから躓く人もいると思うので、一旦バグ内容と解決法を共有します。&lt;/p&gt;&#10;&lt;h2 id="バグ1volumesをマウントする際にchownからpermission-deniedエラーが出る"&gt;バグ1：volumesをマウントする際に&lt;code&gt;chown&lt;/code&gt;から&lt;code&gt;permission denied&lt;/code&gt;エラーが出る&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/rancher-sandbox/rancher-desktop/issues/1209" target="_blank" rel="noopener noreferrer"&gt;https://github.com/rancher-sandbox/rancher-desktop/issues/1209&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;issue自体はまだ解決されていない(2023年1月)ですが、&#10;&lt;code&gt;~/Library/Application\ Support/rancher-desktop/lima/_config/override.yaml&lt;/code&gt;に下記の設定を追加すれば回避できます。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;mountType: 9p&#10;mounts:&#10; - location: &amp;#34;~&amp;#34;&#10; 9p:&#10; securityModel: mapped-xattr&#10; cache: &amp;#34;mmap&amp;#34;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="バグ2m1-macはmonterey-124以上に上げないと割り当てられるメモリは最大3gbになる"&gt;バグ2：M1 MacはMonterey 12.4以上に上げないと、割り当てられるメモリは最大3GBになる&lt;/h2&gt;&#10;&lt;p&gt;Rancher DesktopのGUIからメモリを32GBに設定したにもかかわらず、&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://www.kinjo.tech/posts/20230119-1_hu_8a68f19fadad713f.png"&#10; srcset="https://www.kinjo.tech/posts/20230119-1_hu_69df59a3e870f501.png 480w, https://www.kinjo.tech/posts/20230119-1_hu_9577f496345c5ca3.png 960w, https://www.kinjo.tech/posts/20230119-1_hu_8a68f19fadad713f.png 1400w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="1400" height="933"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;docker info&lt;/code&gt;で確認すると、CPUは設定通りですが、メモリは2.9GiBしか割り当てられていませんでした。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt; Architecture: aarch64&#10; CPUs: 6&#10; Total Memory: 2.909GiB&#10; Name: lima-rancher-desktop&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;&lt;a href="https://github.com/rancher-sandbox/rancher-desktop/issues/2855" target="_blank" rel="noopener noreferrer"&gt;https://github.com/rancher-sandbox/rancher-desktop/issues/2855&lt;/a&gt;&lt;/p&gt;</description></item><item><title>dbtでBigQuery上に構築したデータ基盤のメタデータ管理</title><link>https://www.kinjo.tech/posts/20221211/</link><pubDate>Sun, 11 Dec 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221211/</guid><description>&lt;p&gt;データ基盤におけるETL/ELT開発のT(Transform)を担うツール&lt;a href="https://www.getdbt.com/" target="_blank" rel="noopener noreferrer"&gt;dbt&lt;/a&gt;は最近注目を浴びています。dbtでデータモデリングする方法既に多く紹介されたので、この記事では手を動かしながらdbtでBigQuery上に構築したデータ基盤のメタデータを管理する方法を紹介します。&lt;/p&gt;&#10;&lt;h2 id="環境構築"&gt;環境構築&lt;/h2&gt;&#10;&lt;p&gt;dbt公式は&lt;code&gt;Homebrew&lt;/code&gt;を推していますが、ローカル環境が汚染されるのをなるべく避けたいので、Dockerで環境構築します。&#10;dbtのプロジェクトとプロファイルの設定ファイルを用意しておかないと、公式のドキュメントそのまま実行したらコケます。しかし設定ファイルの生成は環境を構築する必要があるので無限ループになっています。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://docs.getdbt.com/docs/get-started/docker-install" target="_blank" rel="noopener noreferrer"&gt;https://docs.getdbt.com/docs/get-started/docker-install&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;そのため、公式のサンプルプロジェクトをforkし、事前にローカル環境で生成した設定ファイルを追加しました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/jckinjo/dbt-metadata-management" target="_blank" rel="noopener noreferrer"&gt;https://github.com/jckinjo/dbt-metadata-management&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="profilesymlを編集"&gt;&lt;code&gt;profiles.yml&lt;/code&gt;を編集&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;{YOUR_DATASET_NAME}&lt;/code&gt;と&lt;code&gt;{YOUR_PROJECT_ID}&lt;/code&gt;を置き換えます。&#10;複数のデータセットのメタデータも作成可能ですが、一旦任意のデータセット名を指定する必要があります。dbtの問題点でもありますが、後ほど説明します。&lt;/p&gt;&#10;&lt;h3 id="dockerイメージをプル"&gt;Dockerイメージをプル&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;docker pull ghcr.io/dbt-labs/dbt-bigquery:1.2.0&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="コンテナを立ち上げる"&gt;コンテナを立ち上げる&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;git clone https://github.com/jckinjo/dbt-metadata-management&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;cd dbt-metadata-management&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="gcloud認証"&gt;&lt;code&gt;gcloud&lt;/code&gt;認証&lt;/h3&gt;&#10;&lt;p&gt;認証済みの場合、このステップは不要です。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;gcloud auth login --no-launch-browser&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;gcloud auth application-default login --no-launch-browser&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="コンテナを立ち上げる-1"&gt;コンテナを立ち上げる&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;docker run --rm \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --network=host \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --platform linux/amd64 \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --mount type=bind,source=`PWD`,target=/usr/app \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --mount type=bind,source=`PWD`/profiles.yml,target=/root/.dbt/profiles.yml \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --mount type=bind,source=$HOME/.config/gcloud/application_default_credentials.json,target=/root/.config/gcloud/application_default_credentials.json \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ghcr.io/dbt-labs/dbt-bigquery:1.2.0 \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ls&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;データモデルのリストが表示されたら、環境構築が無事終了です。&lt;/p&gt;&#10;&lt;h2 id="ドキュメントを生成する"&gt;ドキュメントを生成する&lt;/h2&gt;&#10;&lt;h3 id="docs-generate"&gt;&lt;code&gt;docs generate&lt;/code&gt;&lt;/h3&gt;&#10;&lt;p&gt;以下のコマンドを実行すれば、&lt;code&gt;models/&lt;/code&gt;配下のクエリとメタデータ（yamlファイル）を参照して、&lt;code&gt;target/&lt;/code&gt;配下にメタデータのドキュメントが生成されます。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;docker run --rm \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --network=host \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --platform linux/amd64 \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --mount type=bind,source=`PWD`,target=/usr/app \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --mount type=bind,source=`PWD`/profiles.yml,target=/root/.dbt/profiles.yml \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; --mount type=bind,source=$HOME/.config/gcloud/application_default_credentials.json,target=/root/.config/gcloud/application_default_credentials.json \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; ghcr.io/dbt-labs/dbt-bigquery:1.2.0 \&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt; docs generate&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;さらに&lt;code&gt;dbt docs serve&lt;/code&gt;を実行すると、localhostからメタデータのドキュメントを確認できます↓。schema &lt;code&gt;dbt_test&lt;/code&gt;と&lt;code&gt;dbt_test_2&lt;/code&gt;はBigQueryのデータセットに該当します。&lt;/p&gt;</description></item><item><title>PythonデコレータのSyntactic Sugarなぜ便利かを理解した</title><link>https://www.kinjo.tech/posts/20221123/</link><pubDate>Wed, 23 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221123/</guid><description>&lt;p&gt;Pythonデコレータを利用する場合、&lt;code&gt;@decorator&lt;/code&gt;というSyntactic Sugarを関数やメソッドの先頭に付けるのが一般的ですが、なぜ便利なのかいまいち理解できていないので、調べてみました。&lt;/p&gt;&#10;&lt;p&gt;デコレータの詳細は公式ドキュメントあるいは他の方がすでに紹介されているので、本記事では割愛します。ちなみにおすすめの記事はこちらです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://rednafi.github.io/digressions/python/2020/05/13/python-decorators" target="_blank" rel="noopener noreferrer"&gt;https://rednafi.github.io/digressions/python/2020/05/13/python-decorators&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;まず適当に文字列の両側に&lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;を追加してくれる簡単なデコレータを書きましょう。&lt;code&gt;num_b&lt;/code&gt;は片方に追加する&lt;code&gt;&amp;lt;b&amp;gt;&lt;/code&gt;の数を表しており、デフォルトは1となっています。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;functools&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wraps&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Emphasis&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nd"&gt;@wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;wrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;b&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;num_b&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ret&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;&amp;lt;b&amp;gt;&amp;#34;&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;num_b&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrap&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="デコレータ引数なし関数引数なしの場合"&gt;デコレータ引数なし・関数引数なしの場合&lt;/h2&gt;&#10;&lt;p&gt;最初は一番簡単なパターンで見ていきましょう。デコレータ&lt;code&gt;ep.add_b(hello)&lt;/code&gt;の返り値は関数なので、一回コール&lt;code&gt;()&lt;/code&gt;すればSyntactic Sugarと同じことができるので、むしろSyntactic Sugarを使わないほうがわかりすそうですね。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Emphasis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Hello, There&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@ep.add_b&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello_with_sugar&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Hello, There&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;)())&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hello_with_sugar&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;&amp;lt;b&amp;gt;Hello, There&amp;lt;b&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;&amp;lt;b&amp;gt;Hello, There&amp;lt;b&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="デコレータ引数あり関数引数ありの場合"&gt;デコレータ引数あり・関数引数ありの場合&lt;/h2&gt;&#10;&lt;p&gt;しかしデコレータと関数両方引数がある場合は&lt;code&gt;ep.add_b(num_b=2)(hello)(&amp;quot;Taro&amp;quot;)&lt;/code&gt;を書かないといけません。言葉で説明すると、&lt;code&gt;hello&lt;/code&gt;関数を引数としてデコレータ&lt;code&gt;ep.add_b(num_b=2)&lt;/code&gt;に入れて返された関数に引数&lt;code&gt;&amp;quot;taro&amp;quot;&lt;/code&gt;を渡してコールするという意味です。書けなくはないですが、ややこしくなるので、Syntactic Sugarのほうが断然簡単ですね&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;ep&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Emphasis&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@ep.add_b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello_with_sugar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ep&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_b&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_b&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Taro&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hello_with_sugar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Taro&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;pre tabindex="0"&gt;&lt;code class="language-concole" data-lang="concole"&gt;&amp;lt;b&amp;gt;&amp;lt;b&amp;gt;Hello, Taro&amp;lt;b&amp;gt;&amp;lt;b&amp;gt;&#10;&amp;lt;b&amp;gt;&amp;lt;b&amp;gt;Hello, Taro&amp;lt;b&amp;gt;&amp;lt;b&amp;gt;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="おわりに"&gt;おわりに&lt;/h2&gt;&#10;&lt;p&gt;Syntactic Sugarのおかげで、デコレータと関数両方引数がある場合でも簡単にデコレータを使えるようになりました。ただ個人的にデコレータを初めて触る方はまずあえてSyntactic Sugarを使わず試してみたら、デコレータの理解が捗るのではないかと思います。&#10;ご参考になれば幸いです。&lt;/p&gt;</description></item><item><title>アラートを出す際にAirflowのContextから誤ったtask idが取得されてしまうバグの対処法</title><link>https://www.kinjo.tech/posts/20221121/</link><pubDate>Mon, 21 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221121/</guid><description>&lt;p&gt;先日投稿した&lt;a href="../20221117"&gt;記事&lt;/a&gt;はAirflow DAGの&lt;code&gt;on_failure_callback&lt;/code&gt;と&lt;code&gt;dagrun_timeout&lt;/code&gt;を組み合わせることでDAGの遅延を監視する方法を紹介しました。&lt;/p&gt;&#10;&lt;h2 id="contextから誤ったtask-idが取得されてしまう"&gt;Contextから誤ったtask idが取得されてしまう&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;context&lt;/code&gt;から&lt;code&gt;dag_run&lt;/code&gt;の情報を取得してチャットツールやメールにアラートを出すのは一般的です。Slackにアラートを出す際の例ですが、&lt;code&gt;dag_id&lt;/code&gt;, &lt;code&gt;run_id&lt;/code&gt;, &lt;code&gt;task_id&lt;/code&gt;, &lt;code&gt;reason&lt;/code&gt;, &lt;code&gt;log_url&lt;/code&gt;を取得して、webhookでSlackの特定なチャンネルに投稿し、&lt;code&gt;log_url&lt;/code&gt;をクリックするだけですぐローカルあるいはクラウド環境（例えばCloud Composer）で失敗したtaskのログを確認できるので、アラート解消の効率化に繋がります。&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://www.kinjo.tech/posts/20221121-1_hu_870ef7edc64873b9.png"&#10; srcset="https://www.kinjo.tech/posts/20221121-1_hu_6ecbd44c9f2e3bdb.png 480w, https://www.kinjo.tech/posts/20221121-1_hu_870ef7edc64873b9.png 521w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="521" height="171"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;ソースは以下となります。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;slack_sdk.webhook&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;WebhookClient&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;airflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Variable&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;textwrap&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;notify_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebhookClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;slack_webhook_access_token&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;log_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;task_instance&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log_url&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&amp;#34;&amp;#34;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt; :x: Task has failed.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;*Workflow*: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;workflow&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;*DAG*: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;task_instance&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;*Run ID* &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dag_run&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;*Task*: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;task_instance&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;*Reason*: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;reason&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;log_url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;| *Log URL*&amp;gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="s2"&gt;&#9;&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;webhook&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;alert&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;section&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mrkdwn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;しかし、数回検証してみた結果、実行が失敗したタスク&lt;code&gt;task_id&lt;/code&gt;ではなく、誤った&lt;code&gt;task_id&lt;/code&gt;が取得されてしまう事象がしばしば発生します。Airflowの既知バグで、現時点（2022.11.21）はまだ修正されていないです。&lt;/p&gt;</description></item><item><title>データ基盤におけるGoogleグループ・IAMによるアクセス制御</title><link>https://www.kinjo.tech/posts/20221118/</link><pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221118/</guid><description>&lt;p&gt;会社のTech Blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2022%2F11%2F18%2F110000"&#10; title="データ基盤におけるGoogleグループ・IAMによるアクセス制御"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/11/18/110000" target="_blank" rel="noopener noreferrer"&gt;データ基盤におけるGoogleグループ・IAMによるアクセス制御&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>Airflowのon_failure_callbackとdagrun_timeoutを組み合わせることでDAGの遅延を監視する</title><link>https://www.kinjo.tech/posts/20221117/</link><pubDate>Thu, 17 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221117/</guid><description>&lt;p&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/08/18/110000" target="_blank" rel="noopener noreferrer"&gt;https://buildersbox.corp-sansan.com/entry/2022/08/18/110000&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;この記事では新しくDAGを作成してデータの転送処理が遅れているかを監視する方法が紹介されました。監視と実行用のDAGを分離することでスッキリにはなりますが、DAGが増えることによって管理の手間が生じます（特にDAGが大量にある場合）。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/69312630/airflow-triggering-the-on-failure-callback-when-the-dagrun-timeout-is-exceed" target="_blank" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/69312630/airflow-triggering-the-on-failure-callback-when-the-dagrun-timeout-is-exceed&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;を参考にして&#10;DAGの引数&lt;code&gt;on_failure_callback&lt;/code&gt;と&lt;code&gt;dagrun_timeout&lt;/code&gt;を組み合わせることでDAGの遅延を監視する方法を試してみました。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;airflow&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;airflow.models&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TaskInstance&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;airflow.operators.bash&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_on_dag_run_fail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;***DAG failed!! do something***&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;The DAG failed because: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;reason&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;default_args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;owner&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;mi_empresa&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;DAG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dag_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;failure_callback_example&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;start_date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2021&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;schedule_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;default_args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;catchup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;on_failure_callback&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_on_dag_run_fail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dagrun_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;dag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;delayed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;delayed&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;echo &amp;#34;waiting..&amp;#34;;sleep 60; echo &amp;#34;Done!!&amp;#34;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;will_fail&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BashOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;will_fail&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bash_command&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;exit 1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;delayed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;will_fail&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;実行時間が45秒超えると、&lt;code&gt;_on_dag_run_fail&lt;/code&gt;が実行され失敗したDAGのコンテキスト情報がプリントされます。誰かの役に立つかもしれないので、試しながら気づいた2つのポイントを共有します&lt;/p&gt;&#10;&lt;h3 id="1-on_failure_callbackとdagrun_timeoutをdefault_argsに入れちゃったら動作しない"&gt;1. &lt;code&gt;on_failure_callback&lt;/code&gt;と&lt;code&gt;dagrun_timeout&lt;/code&gt;をdefault_argsに入れちゃったら動作しない&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;default_args&lt;/code&gt;はDAG作成時に実行されるため、DAGのメタデータを入れるのが一般的&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;with DAG(...)&lt;/code&gt;に記載されている引数はDAG実行時に反映されるため、&lt;code&gt;on_failure_callback&lt;/code&gt;と&lt;code&gt;dagrun_timeout&lt;/code&gt;に入れないといけない&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h3 id="2-contextgetreasonを利用するとエラーの種類を判断可能"&gt;2. &lt;code&gt;context.get('reason')&lt;/code&gt;を利用すると、エラーの種類を判断可能&lt;/h3&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;task_failure&lt;/code&gt; タスク実行失敗によるエラー&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;timed_out&lt;/code&gt;　遅延によるエラー&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;なので、エラーによって異なる処理する場面では活用できそうですね。&lt;/p&gt;</description></item><item><title>Telegramで多言語ニュースチャットボットを作った話</title><link>https://www.kinjo.tech/posts/20221103/</link><pubDate>Thu, 03 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221103/</guid><description>&lt;p&gt;先日&lt;a href="../20220418/"&gt;多言語Webニュースアプリ&lt;/a&gt;を作りました。&#10;通勤電車で当日のニュースをチェックしながら、外国語を勉強できるので、まあまあ使いやすかったです。&lt;/p&gt;&#10;&lt;p&gt;実はこのWebを開発する1年ほど前に、一度Telegram botで多言語ニュースチャットボットを開発していました。今日はそれについて紹介したいと思います。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/jckinjo/multitrue-bot" target="_blank" rel="noopener noreferrer"&gt;https://github.com/jckinjo/multitrue-bot&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="事前準備"&gt;事前準備&lt;/h2&gt;&#10;&lt;h3 id="telegram-bot"&gt;Telegram bot&lt;/h3&gt;&#10;&lt;p&gt;Telegramは日本ではあまり人気がないですが、Telegramのアカウントを作るだけでAPIキーを取得できるので、ざくっとボット作りたい場合は使い勝手がけっこういいです。また、良さげのSDKもあります↓。そういう意味ではSlackも同じですが、Telegramは感覚的にLineに近いので手軽さで勝っていると思います。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples" target="_blank" rel="noopener noreferrer"&gt;https://github.com/python-telegram-bot/python-telegram-bot/tree/master/examples&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="ニュースの取得"&gt;ニュースの取得&lt;/h3&gt;&#10;&lt;p&gt;ニュースの取得方法は先日開発したWebアプリと同じく&lt;a href="https://newsapi.org/" target="_blank" rel="noopener noreferrer"&gt;NewsAPI&lt;/a&gt;利用して取得しています。なぜNewsAPIを採用したかについては&lt;a href="../20220418/"&gt;半年前の記事&lt;/a&gt;で詳細を説明したので、割愛します。&lt;/p&gt;&#10;&lt;h2 id="実装"&gt;実装&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://core.telegram.org/" target="_blank" rel="noopener noreferrer"&gt;https://core.telegram.org/&lt;/a&gt;&#10;に参照してボットの初期設定が完了したら、実装に入ります。&#10;フローは以下となります。&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;&lt;code&gt;/start&lt;/code&gt;でニュースボットを起動&lt;/li&gt;&#10;&lt;li&gt;国・地域を選ぶ&lt;/li&gt;&#10;&lt;li&gt;ニュースのジャンルを選ぶ&lt;/li&gt;&#10;&lt;li&gt;終了あるいは2.に戻る&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;h3 id="全体フロー"&gt;全体フロー&lt;/h3&gt;&#10;&lt;p&gt;SDKが提供してくれた下記3つのhandlerを利用しています。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;ConversationHandler&lt;/code&gt;: 先ほど設計したフローに基づいてhandlerを定義する。&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;CommandHandler&lt;/code&gt;: コマンドによって発火される。今回は「入り口 (entry point)」として使う　&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;CallbackQueryHandler&lt;/code&gt;: 他のhandlerの返り値によって発火される&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;updater&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Updater&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEY_PATH&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;keys.json&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;r&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;))[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;telegram_key&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;use_context&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;updater&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dispatcher&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;country_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;^us|jp|cn|tw|kr|gb$&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;headlines_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;^us|jp|cn|tw|kr|gb business|entertainment|general|health|science|sports|technology|$&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;conv_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ConversationHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;entry_points&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;states&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;CATEGORY&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CallbackQueryHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;select_category&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country_pattern&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;HEADLINES&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CallbackQueryHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_news&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headlines_pattern&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;START OVER OR NOT&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CallbackQueryHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start_over&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;^start over$&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;CallbackQueryHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;^end$&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;fallbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CommandHandler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;start&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;dispatcher&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conv_handler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;updater&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_polling&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;updater&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;idle&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="メニュー"&gt;メニュー&lt;/h3&gt;&#10;&lt;p&gt;続いてはcallback関数を見ていきましょう。&lt;code&gt;start&lt;/code&gt;関数はwelcomeメッセージをユーザに送って、ユーザに国・地域を選んでもらいます。その後、&lt;code&gt;select_category&lt;/code&gt;関数でニュースのジャンルを選んでもらいます。&lt;/p&gt;&#10;&lt;p&gt;引数&lt;code&gt;context&lt;/code&gt;と&lt;code&gt;update&lt;/code&gt;&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;context.user_data&lt;/code&gt;: ボットを利用してユーザの情報を取得する&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;context.bot&lt;/code&gt;: ボット自体を操作する（今回はメッセージを送るだけ）のに使う&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;update&lt;/code&gt;: メッセージを更新するのに使う&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_user&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;User &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt; started the conversation.&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;vars&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;welcome_message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;Hello, &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;This is JC News bot🗞️🤖&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;You can get Top News Headlines for a Country and a Category from here. &lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;keyborad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇺🇸&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;us&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇯🇵&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;jp&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇹🇼&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;tw&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇰🇷&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;kr&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇬🇧&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;gb&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🇨🇳&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;cn&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;reply_markup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InlineKeyboardMarkup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyborad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;send_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chat_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effective_chat&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;welcome_message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reply_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Please Choose a Country🤖&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply_markup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reply_markup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;CATEGORY&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;select_category&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;User data from context &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Chat data from context &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chat_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Bot data from context &lt;/span&gt;&lt;span class="si"&gt;{}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bot_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callback_query&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;keyborad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;👩🏼‍💻Technology&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; technology&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🧑‍💼Business&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; business&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;👨🏻‍🎤Entertainment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; entertainment&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;👩🏻‍⚕️Health&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; health&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;👨🏿‍🔬Science&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; science&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🏋🏼‍♂️Sports&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; sports&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;InlineKeyboardButton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;🌎General&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34; general&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;reply_markup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;InlineKeyboardMarkup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyborad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;edit_message_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;Please Choose a Category🤖&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reply_markup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reply_markup&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;HEADLINES&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="ニュースを取得"&gt;ニュースを取得&lt;/h3&gt;&#10;&lt;p&gt;最後は最もコアとなるニュースを取得するロジックです。&lt;code&gt;country&lt;/code&gt;と&lt;code&gt;category&lt;/code&gt;によってNewAPIから最新ホットラインニュースを取得しフォーマットした後、ユーザにメッセージを送ります。&#10;具体的な詳細は説明しませんが、&lt;code&gt;NewsAPICollector&lt;/code&gt;というクラスでNewAPIをラッピングしています。&lt;/p&gt;</description></item><item><title>GitHub Issueだけで自分のルーティングを管理し、そして草を生やす</title><link>https://www.kinjo.tech/posts/20221102/</link><pubDate>Wed, 02 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221102/</guid><description>&lt;p&gt;先日シェルスクリプトで&lt;a href="../20220503"&gt;個人ナレッジマネジメントツール&lt;/a&gt;を作った話しを投稿して、予想以上に需要がありました。&lt;/p&gt;&#10;&lt;h2 id="ルーティングをgithub-posterに生成"&gt;ルーティングをGitHub Posterに生成&lt;/h2&gt;&#10;&lt;p&gt;似たような発想でルーティング管理アプリを使わずに、GitHubのcontributionsのように自分のルーティング（例えば読書、ランニング、LeetCode、外国語の勉強）を管理できると面白くない？と思いながら、GitHub上で検索したらyihong0618さんが開発したGitHubPosterを発見しました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/yihong0618/GitHubPoster" target="_blank" rel="noopener noreferrer"&gt;https://github.com/yihong0618/GitHubPoster&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;GitHub Isssue、Duolingo、Twitter、Kindleなど20個以上のAPIで履歴を取得し、GitHub svg poster（aka. 皆さんが大好きなGitHubの草）を生成します。&lt;/p&gt;&#10;&lt;h2 id="実際使ってみよう"&gt;実際使ってみよう&lt;/h2&gt;&#10;&lt;p&gt;ローダーは20個以上あり、とりあえずissueで今年年始以来の読書ルーティングの草を生やしてみました。&lt;/p&gt;&#10;&lt;h3 id="issueを書く"&gt;Issueを書く&lt;/h3&gt;&#10;&lt;p&gt;issueフォーマットは↓に従う必要でがあります。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;{整数}&#10;&#10;{内容}&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;今年は1月から、日課をこなした日に当該Issueに↓のコメント追加していました。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;2&#10;&#10;「データ指向アプリケーションデザイン」&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="環境構築"&gt;環境構築&lt;/h3&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;pip install -U &amp;#39;github_poster[all]&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="実行"&gt;実行&lt;/h3&gt;&#10;&lt;p&gt;GitHubをトークンを取得し、下記コマンドを実行するだけです。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;github_poster issue --issue_number ${issue_number} --repo_name ${repo_name} --token ${github_token}&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;また、オプション&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;--special-color1&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;--special-color2&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;---stand-with-ukraine&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;などによって色を指定することも可能です。（ウクライナをサポートする配色もあるようですね）&lt;/p&gt;&#10;&lt;h3 id="結果"&gt;結果&lt;/h3&gt;&#10;&lt;p&gt;最終的に生成されたGitHub Poster（&lt;code&gt;.svg&lt;/code&gt;ファイル）はこんな感じです。単位はtimes（回数）になっているが、hours（時間）が正しいです。&#10;&lt;img src="https://www.kinjo.tech/posts/20221102-1_hu_2ddf9bfd2353086e.png"&#10; srcset="https://www.kinjo.tech/posts/20221102-1_hu_d454c1631450f35b.png 480w, https://www.kinjo.tech/posts/20221102-1_hu_b225008412b68916.png 960w, https://www.kinjo.tech/posts/20221102-1_hu_2ddf9bfd2353086e.png 1400w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="1400" height="534"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;確認してみると、&#10;今年今まで336時間読書していて、週の真ん中あまり本を読んでいないと気づきました。&lt;/p&gt;&#10;&lt;h2 id="使った感想"&gt;使った感想&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;余計なモバイルアプリを使わずに、ルーティング管理できるのはミニマムリスト的には最高&lt;/li&gt;&#10;&lt;li&gt;生成されたファイルは&lt;code&gt;.svg&lt;/code&gt;なので、自分のサイトや他のところに取り入れるのも簡単そう&lt;/li&gt;&#10;&lt;li&gt;新しいローダーの開発に貢献してみたい&lt;/li&gt;&#10;&lt;/ul&gt;</description></item><item><title>Airflowで構築したワークフローを検証する</title><link>https://www.kinjo.tech/posts/20221101/</link><pubDate>Tue, 01 Nov 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221101/</guid><description>&lt;h2 id="データ基盤の監視"&gt;データ基盤の監視&lt;/h2&gt;&#10;&lt;p&gt;データ基盤は下流の分析・可視化・モデリングの「基盤」となるので、監視は言うまでもなく品質を担保するため重要な存在です。データ基盤監視の考え方についてこの2つの記事が紹介しています。&#10;&lt;a href="https://tech-blog.monotaro.com/entry/2021/08/24/100000" target="_blank" rel="noopener noreferrer"&gt;https://tech-blog.monotaro.com/entry/2021/08/24/100000&lt;/a&gt;&#10;&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/08/18/110000" target="_blank" rel="noopener noreferrer"&gt;https://buildersbox.corp-sansan.com/entry/2022/08/18/110000&lt;/a&gt;&#10;同じくSQLによるデータ基盤を監視しており、最も大きな違いは自作ツールかAirflowで検証することだけです。本文はAirflowで構築したワークフローの検証についてもう少し紹介したいと思います。&lt;/p&gt;&#10;&lt;p&gt;まず、&lt;a href="https://www.amazon.co.jp/-/en/James-Densmore/dp/1492087831" target="_blank" rel="noopener noreferrer"&gt;Data Pipelines Pocket Reference&lt;/a&gt;ではデータ基盤検証の原則が紹介されました。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Validate Early, Validate Often&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;要はできるだけ早く、できるだけ頻繁に検証するとのことです。ELTあるいはETL処理においては、Extract, Load, Transformそれぞれのステップが終了した直後に監視するのは最も理想的だと思います。&lt;/p&gt;&#10;&lt;p&gt;Transformはデータセットのコンテキストを把握しておかないと検証できないため、データセットごとに対応していく必要があります。ExtractとLoadはnon-contextで汎用的な検証ができるため、データ基盤構築の序盤からやっておいた方が安心だと思います。&lt;/p&gt;&#10;&lt;h2 id="extractの検証"&gt;Extractの検証&lt;/h2&gt;&#10;&lt;p&gt;ストレージサービス（例えばGCS）をデータレイクにする場合、データソースからデータレイクにデータがちゃんとレプリケートされたかを検証するためにAirflowの&lt;code&gt;airflow.providers.google.cloud.sensors.gcs&lt;/code&gt;を利用すると簡単にできます。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow-providers-google/stable/_api/airflow/providers/google/cloud/sensors/gcs/index.html" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow-providers-google/stable/_api/airflow/providers/google/cloud/sensors/gcs/index.html&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;一つのファイルをチェックする場合はほとんどないと思うので、&lt;code&gt;GCSObjectExistenceSensor&lt;/code&gt;より&lt;code&gt;GCSObjectsWithPrefixExistenceSensor&lt;/code&gt;の方がもっと実用的でしょう。下記のタスクをExtractと次の処理の間に挟むと、障害の早期発見が期待できます。なお、Extract時点で既に障害が起きている場合はほとんどデータソース側の処理が失敗しているので、アプリケーション側と連携して作業する必要があります。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;check_extract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GCSObjectsWithPrefixExistenceSensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;check_extract&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{YOUR_BUCKET}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="si"&gt;{YOUR_PREFIX}&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="loadの検証"&gt;Loadの検証&lt;/h2&gt;&#10;&lt;p&gt;ELTとETL処理、Loadするタイミングは異なりますが、検証の方法（データがデータウェアハウスあるいはデータマートにロードされたか）は同じです。よく使われているデータウェアハウスサービスBigQueryだと、Airflowの&lt;code&gt;airflow.contrib.operators.bigquery_check_operator.BigQueryCheckOperator&lt;/code&gt;を利用できます。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow/1.10.10/_api/airflow/contrib/operators/bigquery_check_operator/index.html#airflow.contrib.operators.bigquery_check_operator.BigQueryCheckOperator" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow/1.10.10/_api/airflow/contrib/operators/bigquery_check_operator/index.html#airflow.contrib.operators.bigquery_check_operator.BigQueryCheckOperator&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;下記のタスクをLoad処理の後ろに追加すれば良いです。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;check_load&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BigQueryCheckOperator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;check_load&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;YOUR_SQL&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;use_legacy_sql&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;bq_suffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Variable&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;bq_suffix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;dataset&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;setting&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;table&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;setting&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;table_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;asia-northeast1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;SQLは検証用のクエリとなっており、BigQueryのメタデータテーブルがよく用いられます。例えば&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/08/18/110000" target="_blank" rel="noopener noreferrer"&gt;この記事&lt;/a&gt;が紹介されたクエリでテーブルが空になっているかを確認できます。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;SELECT&#10; total_rows&#10;FROM&#10; ${dataset_id}.INFORMATION_SCHEMA.PARTITIONS&#10;WHERE&#10; table_name = &amp;#39;${table_name}&amp;#39;&#10;ORDER BY&#10; last_modified_time DESC&#10;LIMIT 1;&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;GCPについて簡単に説明しましたが、AWSとAzureも似たようなことはできるはずです。&#10;皆さんのワークフロー設計にご参考になれば幸いです。&lt;/p&gt;</description></item><item><title>自作PythonラッパーでGoogleグループのメンバーシップをより便利に管理する</title><link>https://www.kinjo.tech/posts/20221002/</link><pubDate>Sun, 02 Oct 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20221002/</guid><description>&lt;h2 id="googleグループのメンバーシップを管理する方法"&gt;Googleグループのメンバーシップを管理する方法&lt;/h2&gt;&#10;&lt;p&gt;Googleグループを管理するのは&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;Workspaceのadmin SDKを利用する&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;a href="https://developers.google.com/admin-sdk/directory/v1/guides/manage-groups" target="_blank" rel="noopener noreferrer"&gt;https://developers.google.com/admin-sdk/directory/v1/guides/manage-groups&lt;/a&gt;&lt;/p&gt;&#10;&lt;ol start="2"&gt;&#10;&lt;li&gt;CLI &lt;code&gt;gcloud&lt;/code&gt;を利用する&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/sdk/gcloud/reference/identity/groups/memberships" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/sdk/gcloud/reference/identity/groups/memberships&lt;/a&gt;&lt;/p&gt;&#10;&lt;ol start="3"&gt;&#10;&lt;li&gt;Cloud Identity APIを叩く&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/identity/docs/how-to/setup" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/identity/docs/how-to/setup&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;3つの方法があります&#10;CLI &lt;code&gt;gcloud&lt;/code&gt;はCloud Identity APIを叩いているので、方法2と方法3は実質同じです。&lt;/p&gt;&#10;&lt;p&gt;方法1との違いはなんでしょう？&#10;ドキュメント読んで実際に試したところ、大きな違いは&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;WorkspaceはGCPで作ったサービスアカウントをメンバーとして追加できない（それはそう）&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;gcloud&lt;/code&gt;あるいはCloud Identity APIだと有効期限付きでメンバー追加することが可能&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;p&gt;Googleグループによってセンシティブな情報のアクセス制御する際に、有効期限付きでメンバー追加する機能を利用すると、セキュリティを担保できるので、使い勝手がかなり良いと思います。&lt;/p&gt;&#10;&lt;p&gt;シェルスクリプトを書いてGoogleグループ管理を自動化する場合であれば、ローカル環境やリモート環境（EC2やCloud Runなどを含めて）問わず、&lt;code&gt;gcloud&lt;/code&gt;は断然便利です。下記のコマンドで一発aliceさんを1時間有効期限付きでグループ&lt;code&gt;group_name@email.com&lt;/code&gt;に追加できます。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;gcloud identity groups memberships add --group-email=&amp;#34;group_name@email.com&amp;#34; --member-email=&amp;#34;alice@email.com&amp;#34; --expiration=&amp;#39;1h&amp;#39;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一方、アプリケーションなどに組み込みたい場合は、Cloud Identity APIを叩くことになります。&#10;現時点（2022.10.1）Cloud IdentityはBigqueyなどのサービスのような公式SDKが存在しなく、APIのドキュメントも若干わかりづらいので、この度より便利にアプリケーションからGoogleグループを管理するために、Pythonラッパーを作ってみました。簡単に修正すれば他言語でも利用できる（はず）です。&lt;/p&gt;&#10;&lt;h2 id="pythonラッパー"&gt;Pythonラッパー&lt;/h2&gt;&#10;&lt;h3 id="認証"&gt;認証　&lt;/h3&gt;&#10;&lt;p&gt;Cloud Identity APIを叩くのに、言うまでもなく認証が必要です。&#10;キーによる認証は公式のドキュメントにあるため、コピペすればOKです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/identity/docs/how-to/setup" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/identity/docs/how-to/setup&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;アプリケーションをCloud Runなどにデプロイしている場合、キーを使わずにサービスアカウントとして認証する方法もあります。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;httplib2&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Http&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;oauth2client.client&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;GoogleCredentials&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;googleapiclient.discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_service&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;credentials&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;GoogleCredentials&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_application_default&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;service_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;cloudidentity&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;api_version&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;v1&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;api_version&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;credentials&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Http&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;cache_discovery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="group-idとmembership-id"&gt;Group IDとMembership ID&lt;/h3&gt;&#10;&lt;p&gt;実際にメンバーシップを取得するAPIを叩いてみたらわかると思います。&lt;code&gt;memberships_id&lt;/code&gt;と&lt;code&gt;group_id&lt;/code&gt;によってメンバーシップの&lt;code&gt;name&lt;/code&gt;が決められます。後ほどグループからメンバーを削除する際に、ここの&lt;code&gt;name&lt;/code&gt;が必須です。&lt;/p&gt;</description></item><item><title>AirflowのBigQueryInsertJobOperatorのオプションについて</title><link>https://www.kinjo.tech/posts/20220830/</link><pubDate>Tue, 30 Aug 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220830/</guid><description>&lt;p&gt;先日「&lt;a href="../20220721"&gt;AirflowでGCS外部パーティションテーブルをBigQueryに取り込む方法&lt;/a&gt;」についての記事を書きましたが、&lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt;にフォーマットのオプションを追加する際に、実際の挙動は公式ドキュメントの記述と若干違うので、追記しておきます。&lt;/p&gt;&#10;&lt;h3 id="なぜ気づいたのか"&gt;なぜ気づいたのか&lt;/h3&gt;&#10;&lt;p&gt;外部テーブルを取り組む際に、閉じるカッコが見つからないというエラーが出て、&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Error detected while parsing row starting at position: 4204. Error: Missing close double quote (&amp;#34;) character.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;調査してみた結果、改行記号が含まれているのが原因のようでした。stack overflowによると&lt;code&gt;allowQuotedNewlines&lt;/code&gt;といったCSVのオプションを追加すると解決できるらしいです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://stackoverflow.com/questions/65782572/how-to-avoid-missing-close-double-quote-character-error-in-google-bigquer" target="_blank" rel="noopener noreferrer"&gt;https://stackoverflow.com/questions/65782572/how-to-avoid-missing-close-double-quote-character-error-in-google-bigquer&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://qiita.com/sawanoboly/items/410b3346518e569da581" target="_blank" rel="noopener noreferrer"&gt;https://qiita.com/sawanoboly/items/410b3346518e569da581&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;ただし、configurationに&lt;code&gt;csvOptions&lt;/code&gt;を追加しても解決されず（同じエラーが出た）、困っていました。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;load&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;destinationTable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;projectId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;datasetId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DATASET_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;tableId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;sourceUris&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;gs://my-bucket/biostats/*.csv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;autodetect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;sourceFormat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;CSV&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;hivePartitioningOptions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;AUTO&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;sourceUriPrefix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gs://my-bucket/biostats/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;csvOptions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;allowQuotedNewlines&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="解決法"&gt;解決法&lt;/h3&gt;&#10;&lt;p&gt;CLI &lt;code&gt;bq load&lt;/code&gt;を利用す際にどうなっているかを確認したら、どうやら&lt;code&gt;csvOptions&lt;/code&gt;を明示的に書く必要がなく&lt;code&gt;--allow_quoted_newlines&lt;/code&gt;だけ追加すとでうまくいきました。Airflowの&lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt;で同じことをすると、問題なくloadできました。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;configuration&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;load&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;destinationTable&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;projectId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;datasetId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;DATASET_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;tableId&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;sourceUris&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;gs://my-bucket/biostats/*.csv&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;autodetect&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;sourceFormat&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;CSV&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;hivePartitioningOptions&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;mode&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;AUTO&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9; &lt;span class="s2"&gt;&amp;#34;sourceUriPrefix&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;gs://my-bucket/biostats/&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="c1"&gt;# csvOptionsの記述は不要 &lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#9;&lt;span class="s2"&gt;&amp;#34;allowQuotedNewlines&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Cloud ComposerのDAGでデータ基盤の転送パイプラインを監視</title><link>https://www.kinjo.tech/posts/20220818/</link><pubDate>Thu, 18 Aug 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220818/</guid><description>&lt;p&gt;会社のTech Blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2022%2F08%2F18%2F110000"&#10; title="Cloud ComposerのDAGでデータ基盤の転送パイプラインを監視"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/08/18/110000" target="_blank" rel="noopener noreferrer"&gt;Cloud ComposerのDAGでデータ基盤の転送パイプラインを監視&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>AirflowでGCS外部パーティションテーブルをBigQueryに取り込む方法</title><link>https://www.kinjo.tech/posts/20220721/</link><pubDate>Thu, 21 Jul 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220721/</guid><description>&lt;h2 id="gcs外部パーティションテーブルをbigqueryに取り込む"&gt;GCS外部パーティションテーブルをBigQueryに取り込む&lt;/h2&gt;&#10;&lt;p&gt;BigQueryでデータレイクを構築する際に、GCS、Google Drive Data、Bigtableから外部テーブルを取り組む場面は少なくないと思います。下記のような外部テーブルがパーティションで分けられている場合(Hive partitioned dataと呼ばれている)は少しややこしくなりますが、&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-console" data-lang="console"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;gs://myBucket/myTable/dt=2019-10-31/lang=en/foo&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="go"&gt;gs://myBucket/myTable/dt=2018-10-31/lang=fr/bar&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;公式ドキュメントは取り組み方をわかりやすくまとめてくれています。&lt;code&gt;bq&lt;/code&gt;というcliでコマンド2行で解決できます。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/bigquery/docs/hive-partitioned-queries-gcs#partition_schema_detection_modes" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/hive-partitioned-queries-gcs#partition_schema_detection_modes&lt;/a&gt;&lt;/p&gt;&#10;&lt;ol&gt;&#10;&lt;li&gt;テーブル定義を作成&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bq mkdef &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --autodetect &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --source_format&lt;span class="o"&gt;=&lt;/span&gt;CSV &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --hive_partitioning_mode&lt;span class="o"&gt;=&lt;/span&gt;AUTO &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; --hive_partitioning_source_uri_prefix&lt;span class="o"&gt;=&lt;/span&gt;gs://your-bucket/your-table/ &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;gs://you-bucket/you-table/*.csv&amp;#34;&lt;/span&gt; &amp;gt; /tmp/MyTableDefFile&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol start="2"&gt;&#10;&lt;li&gt;テーブルを作成&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;bq mk --external_table_definition&lt;span class="o"&gt;=&lt;/span&gt;/tmp/MyTableDefFile &lt;span class="se"&gt;\&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; my_dataset.my_table&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="airflowで取り込むとどうなる"&gt;Airflowで取り込むとどうなる？&lt;/h2&gt;&#10;&lt;p&gt;まず、簡単に背景を補足すると現在筆者が構築しているデータ基盤はCloud Composer (Airflow)でワークフローを管理しており、AirflowでGCSから外部テーブルを取り込むといった需要があります。パーティションではない普通のテーブルであれば、&lt;code&gt;BigQueryCreateExternalTableOperator&lt;/code&gt;利用すると一発で解決できます。&#10;しかし、パーティションのある場合はAirflowから事前用意された関数がなく（2022年7月時点）、他の方法を考えないといけないです。&lt;/p&gt;&#10;&lt;p&gt;GCP公式ドキュメントもConosole, bq, API, Java 4つの方法しか書いておらず、PythonのSDKはまだ実装されていないようです。&#10;&lt;a href="https://cloud.google.com/bigquery/docs/hive-partitioned-queries-gcs#creating_an_external_table_for_hive_partitioned_data" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/hive-partitioned-queries-gcs#creating_an_external_table_for_hive_partitioned_data&lt;/a&gt;&lt;/p&gt;&#10;&lt;h2 id="bigqueryinsertjoboperatorを利用"&gt;&lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt;を利用&lt;/h2&gt;&#10;&lt;p&gt;いろいろ調査した結果、筆者と同じことに困っている人がいたようです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/apache/airflow/issues/13626" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/issues/13626&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;その下に&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Have you tried to use BigQueryInsertJobOperator? for example, see: #13598&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;「&lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt;を使ってみたらどう？」というコメントがありました。&#10;さらにリンク先のPR（なぜかクローズされた）に行くと、&lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt;の例をまとめてもらっています。どうやら専用のSDKがない場合&lt;code&gt;BigQueryInsertJobOperator&lt;/code&gt;を使うのは普通らしいです。引数もやや雑で、そのままBigQueryのREST APIに叩くのと近い感じです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://airflow.apache.org/docs/apache-airflow-providers-google/stable/_api/airflow/providers/google/cloud/operators/bigquery/index.html#airflow.providers.google.cloud.operators.bigquery.BigQueryInsertJobOperator" target="_blank" rel="noopener noreferrer"&gt;https://airflow.apache.org/docs/apache-airflow-providers-google/stable/_api/airflow/providers/google/cloud/operators/bigquery/index.html#airflow.providers.google.cloud.operators.bigquery.BigQueryInsertJobOperator&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/apache/airflow/pull/13598/files" target="_blank" rel="noopener noreferrer"&gt;https://github.com/apache/airflow/pull/13598/files&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;しかしながら、上記のPRは外部パーティションテーブルを取り組む例がなかったため、BigQuery APIのドキュメントを調べて、&lt;code&gt;hivePartitioningOptions&lt;/code&gt;というオプションを見つけました。これを追加すると、もしかしてうまくいくのではないかと思い、早速検証してみました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#hivepartitioningoptions" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/bigquery/docs/reference/rest/v2/tables#hivepartitioningoptions&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="データを用意"&gt;データを用意&lt;/h3&gt;&#10;&lt;p&gt;&lt;a href="https://people.math.sc.edu/Burkardt/datasets/csv/csv.html" target="_blank" rel="noopener noreferrer"&gt;https://people.math.sc.edu/Burkardt/datasets/csv/csv.html&lt;/a&gt;&#10;から適当にcsvファイルをダウロードします。&lt;/p&gt;</description></item><item><title>git switch, git restore, git grep, git blameを使ってみよう</title><link>https://www.kinjo.tech/posts/20220710/</link><pubDate>Sun, 10 Jul 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220710/</guid><description>&lt;p&gt;&lt;a href="https://git-scm.com/" target="_blank" rel="noopener noreferrer"&gt;https://git-scm.com/&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;毎日息を吸うようにgitを使っているけど、gitのドキュメント見たことないなと思って実際に読んでみました。あまり使われていないが、便利そうなgitコマンドを見つけたので、共有します。&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;git switch&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git restore&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git grep&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git blame&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git log -p &amp;lt;filename&amp;gt;&lt;/code&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git commit --amend --no-edit&lt;/code&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="git-switch"&gt;&lt;code&gt;git switch&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;ブランチを切り替えるコマンドです。&lt;code&gt;git checkout&lt;/code&gt;でできるのに、なぜわざわざ&lt;code&gt;git switch&lt;/code&gt;を使うのかって思っていましたが、この記事を読んで意図を理解しました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://www.banterly.net/2021/07/31/new-in-git-switch-and-restore/" target="_blank" rel="noopener noreferrer"&gt;https://www.banterly.net/2021/07/31/new-in-git-switch-and-restore/&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;簡単に説明すると&lt;code&gt;git checkout&lt;/code&gt;は&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;code&gt;git checkout &amp;lt;branch_name&amp;gt;&lt;/code&gt; ブランチ・コミットを切り替える&lt;/li&gt;&#10;&lt;li&gt;&lt;code&gt;git checkout -- &amp;lt;file_name&amp;gt;&lt;/code&gt; ファイルの変更を廃棄する（直近のcommitに戻す）&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;2つの機能を持っているので、少し紛らわしいですね。数年前初めてgitを勉強した頃、けっこうハマっていた記憶があります。&#10;&lt;code&gt;version 2.23&lt;/code&gt;にブランチ・コミットを切り替える機能のみ持っている機能&lt;code&gt;git switch&lt;/code&gt;がリリースされました。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# git checkout your_branchと同じ&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git switch your_branch&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ただ、新しくブランチを切る際のオプションは&lt;code&gt;-b&lt;/code&gt;ではなく、&lt;code&gt;-c&lt;/code&gt;になっています。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# git checkout -b your_branchと同じ&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git switch -c new_branch&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="git-restore"&gt;&lt;code&gt;git restore&lt;/code&gt;&lt;/h2&gt;&#10;&lt;p&gt;&lt;code&gt;git restore&lt;/code&gt;は、ファイルの変更を廃棄して、直近のcommitに戻すという&lt;code&gt;git checkout&lt;/code&gt;の残り半分の機能を持っています。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# git checkout -- file.txtと同じ&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;git restore -- file.txt&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt;をやめて&lt;code&gt;git switch&lt;/code&gt;と&lt;code&gt;git restore&lt;/code&gt;を使ったほうがわかりやすいと思います。特に初心者の方はぜひ試してみてください。&lt;/p&gt;</description></item><item><title>Cloud FunctionsにおけるSlack APIの3秒レスポンス問題の対処法</title><link>https://www.kinjo.tech/posts/20220612/</link><pubDate>Sun, 12 Jun 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220612/</guid><description>&lt;p&gt;Slack APIはUXのため著名な3秒レスポンスルールを設けています。初期設定のようなものではなく、自ら伸ばすことはできません。（厳しい）&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://api.slack.com/interactivity/slash-commands" target="_blank" rel="noopener noreferrer"&gt;https://api.slack.com/interactivity/slash-commands&lt;/a&gt;&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;If you need to respond outside of the 3 second window provided by the request responses above, you still have plenty of options for keeping the workflow alive.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Slack Appsを開発したことのある人にとって最初は少し戸惑うでしょうか。（筆者はそうでした。）&#10;最近はCloud Functionsとslack boltで社内の承認アプリを開発していて、試行錯誤した経験を共有したいと思います。&lt;/p&gt;&#10;&lt;h2 id="経緯"&gt;経緯&lt;/h2&gt;&#10;&lt;p&gt;実装する予定のアプリの最初のステップとして、ショートカットでCloud Functionsを発火させてmodalを開きます。&lt;code&gt;slack bolt&lt;/code&gt;のドキュメントを読んで、以下の実装をおこないました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://slack.dev/bolt-python/concepts" target="_blank" rel="noopener noreferrer"&gt;https://slack.dev/bolt-python/concepts&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;（一部のコード）&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@app.shortcut&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;/hogehoge&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;open_application_modal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;WebClient&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;views_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;trigger_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;trigger_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;modal&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# View identifier&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;callback_id&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;application_form_view&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;plain_text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;APP Title&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;submit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;type&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;plain_text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;text&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Submit&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s2"&gt;&amp;#34;blocks&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;create_blocks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# ....&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;しかし、最初ショートカットをクリックしてmodalが出てこないが、数回試してみたら出てくるという謎な事象が起きていました。modal出てからのステップ（申請フォーマットの提出、Google APIを叩く処理）は正常に動作します。&lt;/p&gt;&#10;&lt;p&gt;Cloud Functionsのログを調べてみたら、&lt;code&gt;expired_trigger_id&lt;/code&gt;が出ているので、トリガーの有効期限が切れて、3秒レスポンスルールを違反しているようです。&lt;/p&gt;</description></item><item><title>Cloud Functions 2nd Genにおけるsecret keyの管理</title><link>https://www.kinjo.tech/posts/20220601/</link><pubDate>Wed, 01 Jun 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220601/</guid><description>&lt;p&gt;GCPのFaaS(Functions-as-a-Service)Cloud Functionsは最近第二世代がリリースされ、（まだbeta期間中ですが）インフラ周りがけっこう強化されました。Cloud StorageとBigQueryにある大規模データを処理するために、HTTPトリガーのアップタイムが60分までに、イベントトリガーも10分まで伸びました。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/functions/docs/2nd-gen/overview" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/functions/docs/2nd-gen/overview&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;筆者はこの数週間ちょうどCloud Functionsを利用して承認システムを開発しています。Cloud Functions 2nd Genにおけるsecret keyの管理について少し困っていたので、調べたものを共有します。&lt;/p&gt;&#10;&lt;h2 id="何が問題か"&gt;何が問題か&lt;/h2&gt;&#10;&lt;p&gt;secret keyなどを明文でCloud Functionsの環境変数に保存するは流石に危ないので、他の方法が必要です。&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;we recommend that you review the best practices for secret management. Note that there is no Cloud Functions-specific integration with Cloud KMS.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;Cloud Functions公式ドキュメントによると、secret keyを管理するベストプラクティスはSecret Managementを利用することです。また、Cloud Functions特有のCloud KMSは存在しません。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://cloud.google.com/functions/docs/configuring/env-var#managing_secrets" target="_blank" rel="noopener noreferrer"&gt;https://cloud.google.com/functions/docs/configuring/env-var#managing_secrets&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;1st genの場合はデプロイする際に&lt;code&gt;-set-secrets&lt;/code&gt;というオプションでSecret Management上保存しているキーを追加すれば良いですが、2nd genで同じようなことをやると、&lt;code&gt;-set-secrets&lt;/code&gt;はまだ実装されていない（2022年5月）ので、怒られます。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;ERROR: (gcloud.beta.functions.deploy) --set-secrets is not yet supported in Cloud Functions V2.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="cloud-kmsを使う"&gt;Cloud KMSを使う&lt;/h2&gt;&#10;&lt;p&gt;Secret Managementはまだ存在しない時代（と言っても3年前）に書かれた記事を読んで、参考になりました。&lt;/p&gt;&#10;&lt;p&gt;考え方としては、&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Cloud KMSで認証情報(文字列)を暗号化する&lt;/li&gt;&#10;&lt;li&gt;暗号化した認証情報をCloud Functionsの環境変数に登録する&lt;/li&gt;&#10;&lt;li&gt;Cloud Functionsの関数内で暗号化された環境変数を復号化する&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;&lt;a href="https://www.apps-gcp.com/kms-with-functions/" target="_blank" rel="noopener noreferrer"&gt;https://www.apps-gcp.com/kms-with-functions/&lt;/a&gt;&lt;/p&gt;</description></item><item><title>TerraformでCloud Storageのバケット名を変更する際の罠</title><link>https://www.kinjo.tech/posts/20220527/</link><pubDate>Fri, 27 May 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220527/</guid><description>&lt;p&gt;最近よくデータ基盤構築の仕事をしているため、TerraformでGCPをいじったりしています。今回はCloud Storageのバケット名を変更する際に罠にはまりやすいところを紹介します。もちろん、Cloud Storageだけでなく、他のサービスやAWSなどにも活用できそうな箇所があるではないかと思います。&lt;/p&gt;&#10;&lt;p&gt;環境&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;Terraform v1.1.2&#10;on darwin_arm64&#10;provider registry.terraform.io/hashicorp/google v4.15.0&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;他の環境は正しく動作するかは未検証です。&lt;/p&gt;&#10;&lt;h2 id="経緯"&gt;経緯&lt;/h2&gt;&#10;&lt;p&gt;下記のバケットを作って検証環境に反映した後、PRを出しました。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-tf" data-lang="tf"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;google_storage_bucket&amp;#34;&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;my_bucket&amp;#34;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;my-bucket&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;storage_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gcs_storage_class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;coldline&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;project&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;project_id&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gcs_location&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;force_destroy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;uniform_bucket_level_access&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;retention_policy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;is_locked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;retention_period&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;lifecycle_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;condition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;age&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;action&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;Delete&amp;#34;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;チームメンバーのレビュー受けて、バケット名は&lt;code&gt;my-bucket&lt;/code&gt;より&lt;code&gt;my-bucket-hoge&lt;/code&gt;の方が良いと言われたので、&lt;code&gt;my-bucket&lt;/code&gt;を&lt;code&gt;my-bucket-hoge&lt;/code&gt;変更し&lt;code&gt;terraform apply&lt;/code&gt;したらエラーになりました。もちろんのことですが、最初は&lt;code&gt;force_destroy = false&lt;/code&gt;に設定していて、バケット内オブジェクトが入っているので、強制変更はできません。&lt;/p&gt;&#10;&lt;pre tabindex="0"&gt;&lt;code&gt;force_destroy - (Optional, Default: false) When deleting a bucket, this boolean option will delete all contained objects. If you try to delete a bucket that contains objects, Terraform will fail that run.&#10;&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;解決方法を二つ見つけました。&lt;/p&gt;</description></item><item><title>データ基盤におけるGitHub Actionsを使ったTerraformとCloud ComposerのCI/CD</title><link>https://www.kinjo.tech/posts/20220516/</link><pubDate>Mon, 16 May 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220516/</guid><description>&lt;p&gt;会社のTech Blogに記事を書きました&lt;/p&gt;&#10;&lt;iframe&#10; class="hatena-blog-card"&#10; src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fbuildersbox.corp-sansan.com%2Fentry%2F2022%2F05%2F16%2F110000"&#10; title="データ基盤におけるGitHub Actionsを使ったTerraformとCloud ComposerのCI/CD"&#10; frameborder="0"&#10; scrolling="no"&#10; loading="lazy"&#10;&gt;&lt;/iframe&gt;&#10;&lt;noscript&gt;&lt;a href="https://buildersbox.corp-sansan.com/entry/2022/05/16/110000" target="_blank" rel="noopener noreferrer"&gt;データ基盤におけるGitHub Actionsを使ったTerraformとCloud ComposerのCI/CD&lt;/a&gt;&lt;/noscript&gt;</description></item><item><title>ワードクラウドを自作ニュースアプリに追加してみた</title><link>https://www.kinjo.tech/posts/20220514/</link><pubDate>Sat, 14 May 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220514/</guid><description>&lt;h2 id="はじめに"&gt;はじめに&lt;/h2&gt;&#10;&lt;p&gt;この間Node.jsで&lt;a href="../20220418/"&gt;多言語Webニュースアプリ&lt;/a&gt;作ってみました&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://www.multitrue.news" target="_blank" rel="noopener noreferrer"&gt;https://www.multitrue.news&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;ニュースのタイトルと概要だけではつまらないので、単語の出現頻度によって直近一週間のニュースからワードクラウド作って、一目で世の中の出来事を確認できたら面白そうじゃないかと思いながら、ワードクラウドをニュースアプリに追加してみました。&lt;/p&gt;&#10;&lt;h2 id="詳細"&gt;詳細&lt;/h2&gt;&#10;&lt;p&gt;ソースはこちらから確認できます。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/jckinjo/multitrue" target="_blank" rel="noopener noreferrer"&gt;https://github.com/jckinjo/multitrue&lt;/a&gt;&lt;/p&gt;&#10;&lt;h3 id="下準備"&gt;下準備&lt;/h3&gt;&#10;&lt;p&gt;日本語と中国語などは英語と異なり、単語と単語の間スペースがないので、形態素解析が必要です。簡略化するために、今回は英語のワードクラウドのみを作ることにしました。人称代名詞や助動詞のような&lt;code&gt;Stop Words&lt;/code&gt;をワードクラウドに出しても意味がないので、NLTKの英語&lt;code&gt;Stop Words&lt;/code&gt;辞書を利用します。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://gist.github.com/sebleier/554280" target="_blank" rel="noopener noreferrer"&gt;https://gist.github.com/sebleier/554280&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;また、単語の出現頻度を集計するヘルプ関数を作成します。いい感じライブラリもありますが、できるだけdependencyを減らしたいので、自分で実装することにしました。ただ、全ての単語を最終的に大文字に変換します。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;src/utils/countWordsFrequency.js&lt;/code&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stopwords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;./stopwords-en.json&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;// English stopwords via https://gist.github.com/sebleier/554280&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;countWordsFrequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;// remove punctuation and split by space&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;terms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/[a-zA-Z]+/g&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;terms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;stopwords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stopwords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toUpperCase&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}))&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;};&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exports&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;countWordsFrequency&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="ニュース記事の単語出現頻度を計算"&gt;ニュース記事の単語出現頻度を計算&lt;/h3&gt;&#10;&lt;p&gt;続いて、英語ニュースを収集するcontrollerに単語頻度を計算するロジックを組み込みます。今回利用しているNewsAPIはニュースの本文を取得できないため、タイトルと概要から単語出現頻度を計算し、ニュース・メタ情報と一緒にレスポンスに追加します。（収集期間の&lt;code&gt;view-config.json&lt;/code&gt;から日単位で設定できます）今考えるとやはり単語出現頻度の計算とニュースの取得を分けたほうが良いかもしれません。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;src/controllers/viewsController.js&lt;/code&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-javascript" data-lang="javascript"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nx"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getHeadlinesUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;catchAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;news&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;News&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;general&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us&amp;#39;&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-publishedAt&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;viewConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;articlesTitleDesc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kr"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;News&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;general&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;country&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;publishedAt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;$gt&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;viewConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wordscloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dateRangeDay&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;title description&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;wordsFrequency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;calcWordFrequncyInArticles&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;articlesTitleDesc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;index&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;countryMeta&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;flag&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;🇺🇸&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Top Stories&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;},&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;news&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;wordsFrequency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;});&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="計算結果をフロントに渡す"&gt;計算結果をフロントに渡す&lt;/h3&gt;&#10;&lt;p&gt;&lt;code&gt;echarts-wordcloud&lt;/code&gt;というライブラリでワードクラウドを描きます。&#10;&lt;a href="https://github.com/ecomfe/echarts-wordcloud" target="_blank" rel="noopener noreferrer"&gt;https://github.com/ecomfe/echarts-wordcloud&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Herokuにデプロイする際のTips</title><link>https://www.kinjo.tech/posts/20220504/</link><pubDate>Wed, 04 May 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220504/</guid><description>&lt;p&gt;2022年11月28日をもってHerokuフリープランが終了しました&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://blog.heroku.com/new-low-cost-plans" target="_blank" rel="noopener noreferrer"&gt;https://blog.heroku.com/new-low-cost-plans&lt;/a&gt;&lt;/p&gt;&#10;&lt;hr&gt;&#10;&lt;p&gt;個人開発する際にコスト面と手軽さを考えるとAWSにデプロイするのはあまりふさわしくないでしょう。Herokuだともっと手軽くデプロイできるし無料枠もあるので、個人で開発したおもちゃをデプロイするのはちょうどいい感じだと思います。この間筆者が開発した&lt;a href="../20220418/"&gt;多言語Webニュースアプリ&lt;/a&gt;も&lt;code&gt;Heroku&lt;/code&gt;にデプロイしています。&lt;/p&gt;&#10;&lt;p&gt;本記事では、Herokuにデプロイする際のTipsをいくつかを紹介したいと思います。少しでもお役に立てれば幸いです。&lt;/p&gt;&#10;&lt;h1 id="tip-1-github-actionsで自動デプロイ"&gt;Tip 1: GitHub Actionsで自動デプロイ&lt;/h1&gt;&#10;&lt;p&gt;&lt;a href="https://devcenter.heroku.com/articles/git" target="_blank" rel="noopener noreferrer"&gt;https://devcenter.heroku.com/articles/git&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;コマンドラインでいちいち打つのは面倒くさいので、自動デプロイしたほうが断然便利ですね。もちろんHerokuのGUIからも自動デプロイできますが、せっかくソースコードもGitHubで管理しているので、GitHub Actions を使ったほう便利だと思います。&lt;/p&gt;&#10;&lt;p&gt;&lt;code&gt;heroku-deploy&lt;/code&gt;というアクションを利用すると、Herokuのメールアドレスと&lt;code&gt;heroku_api_key&lt;/code&gt;さえわかれば、GitHub Actionsから自動デプロイできます。ちなみに、&lt;code&gt;heroku_api_key&lt;/code&gt;は&lt;a href="https://dashboard.heroku.com/account" target="_blank" rel="noopener noreferrer"&gt;こちら&lt;/a&gt;から取得できます。&#10;GitHub Actionsの設定についてですが、他の記事を見たかぎり&lt;code&gt;on: [push, pull_request]&lt;/code&gt;に設定している方もいました。本番にデプロイするという意味合いもあるので、個人的には&lt;code&gt;release&lt;/code&gt;（リリース作成するたびに、パイプラインが発火される）に設定したほうがいいではないかと思います。ミスってpushしちゃったのも防げます。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;release&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;types&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;published&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;Deploy to PROD&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;build&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;ubuntu-latest&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;actions/checkout@v2&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;- &lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;akhileshns/heroku-deploy@v3.12.12&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;heroku_api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{secrets.HEROKU_API_KEY}}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;heroku_app_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;your_app_name&amp;#39;&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;heroku_email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l"&gt;${{secrets.HEROKU_EMAIL}}&lt;/span&gt;&lt;span class="w"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="tip-2-無料枠を使いこなす"&gt;Tip 2: 無料枠を使いこなす&lt;/h1&gt;&#10;&lt;p&gt;現時点(2022年5月)Herokuのプランはこうなっています。&#10;&lt;img src="https://www.kinjo.tech/posts/20220504-4_hu_da90c91b4ea1d779.png"&#10; srcset="https://www.kinjo.tech/posts/20220504-4_hu_797dd5cf7d17d4b.png 480w, https://www.kinjo.tech/posts/20220504-4_hu_8f95310505b3df5e.png 960w, https://www.kinjo.tech/posts/20220504-4_hu_da90c91b4ea1d779.png 1022w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="1022" height="999"&#10; alt=""&#10; loading="eager" decoding="async" fetchpriority="high"&gt;&lt;/p&gt;</description></item><item><title>Creating a Personal Knowledge Management Tool with Just a 15-Line Shell Script</title><link>https://www.kinjo.tech/posts/20220503/</link><pubDate>Tue, 03 May 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220503/</guid><description>&lt;h2 id="introduction"&gt;Introduction&lt;/h2&gt;&#10;&lt;p&gt;When reading books or articles, you may want to systematically manage the notes you&amp;rsquo;ve written. Currently, there are many excellent knowledge management tools available.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Confluence&lt;/li&gt;&#10;&lt;li&gt;Notion&lt;/li&gt;&#10;&lt;li&gt;HackMD&lt;/li&gt;&#10;&lt;li&gt;Boost Note&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;There are countless tools when you also include mind mapping tools. I am a minimalist and has the following preferences:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Manage personal notes like code on Github&lt;/li&gt;&#10;&lt;li&gt;View notes in a mind map format from the web&lt;/li&gt;&#10;&lt;li&gt;Use a free tool instead of a subscription-based service&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Thererfore, I came up with the idea of creating a custom personal knowledge management tool. As a result, I only used a 15-line shell script, GitHub Actions, and the Markdown mind map conversion tool Markmap to create it.&lt;/p&gt;</description></item><item><title>M1 Mac (macOS Monterey 12.2.1)でpyenv/Python開発環境構築</title><link>https://www.kinjo.tech/posts/20220422/</link><pubDate>Fri, 22 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220422/</guid><description>&lt;h1 id="はじめに"&gt;はじめに&lt;/h1&gt;&#10;&lt;p&gt;先日M1のMBPを入手したので、早速Pythonの開発環境を構築しました。&#10;Intel Macと比べて大きく違うので、昔メモったIntel Macの環境構築手順が使えなくなり、新しく環境構築の記事を書こうと思いました。&#10;少しでもお役に立てれば幸いです。&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://zenn.dev/osuzuki/articles/380be0f682d72d" target="_blank" rel="noopener noreferrer"&gt;この記事&lt;/a&gt;では&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;Homebrewは使わないで！（動くけど難易度高い）&#10;pyenvは使わないで！（動かない=2021年2月時点）&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;と記述していましたが、おそらく2021年2月時点ではHomebrewやpyenvなどはまだM1 Macに対応していない（？）と考えられます。現在(2022年4月)では、少し設定は必要ですが、問題なく動作できるようになりました。&lt;/p&gt;&#10;&lt;h1 id="homebrew"&gt;Homebrew&lt;/h1&gt;&#10;&lt;p&gt;まずはHomebrewをインストールします&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/bin/bash -c &lt;span class="s2"&gt;&amp;#34;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;M1 MacではHomebrewによってインストールされたパッケージは&lt;code&gt;/usr/local/bin/hoge&lt;/code&gt;ではなく、&lt;code&gt;/opt/homebrew/bin/hoge&lt;/code&gt;保存されるので、brewをインストールした後は環境変数を設定する必要があります。&lt;/p&gt;&#10;&lt;p&gt;以下を&lt;code&gt;.bashrc&lt;/code&gt;か&lt;code&gt;.zshrc&lt;/code&gt;に追加して、ターミナルを再起動すると、&lt;code&gt;brew install&lt;/code&gt;は問題なく動作します。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/homebrew/bin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;export&lt;/span&gt; &lt;span class="nv"&gt;PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/opt/homebrew/sbin:&lt;span class="nv"&gt;$PATH&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;fish&lt;/code&gt;ユーザはこれを実行してください。&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nb"&gt;set&lt;/span&gt; PATH /opt/homebrew/bin /opt/homebrew/sbin &lt;span class="nv"&gt;$PATH&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="pyenv"&gt;pyenv&lt;/h1&gt;&#10;&lt;p&gt;&lt;code&gt;pyenv&lt;/code&gt;と&lt;code&gt;virtualenv&lt;/code&gt;をインストールします&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install pyenv&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;brew install pyenv-virtualenv&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ここまでは問題ないはずです。&lt;/p&gt;&#10;&lt;p&gt;続いてバージョンしてPythonをインストールします。&#10;たとえば &lt;code&gt;Python 3.8.12&lt;/code&gt;&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pyenv install 3.8.12&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;しかし、うまくいきません&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-shell" data-lang="shell"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;pyenv install 3.8.12&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python-build: use openssl@1.1 from homebrew&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python-build: use readline from homebrew&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Installing Python-3.8.12...&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python-build: use readline from homebrew&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;python-build: use zlib from xcode sdk&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;BUILD FAILED &lt;span class="o"&gt;(&lt;/span&gt;OS X 12.2.1 using python-build 20180424&lt;span class="o"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Inspect or clean up the working tree at /var/folders/f1/3g092d_11zl8xnf67bp0n79r0000gp/T/python-build.20220318172446.23811&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Results logged to /var/folders/f1/3g092d_11zl8xnf67bp0n79r0000gp/T/python-build.20220318172446.23811.log&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Last &lt;span class="m"&gt;10&lt;/span&gt; log lines:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checking &lt;span class="k"&gt;for&lt;/span&gt; --with-cxx-main&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;compiler&amp;gt;... no&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checking &lt;span class="k"&gt;for&lt;/span&gt; clang++... no&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configure:&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; By default, distutils will build C++ extension modules with &lt;span class="s2"&gt;&amp;#34;clang++&amp;#34;&lt;/span&gt;.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; If this is not intended, &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; CXX on the configure &lt;span class="nb"&gt;command&lt;/span&gt; line.&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;checking &lt;span class="k"&gt;for&lt;/span&gt; the platform triplet based on compiler characteristics... darwin&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;configure: error: internal configure error &lt;span class="k"&gt;for&lt;/span&gt; the platform triplet, please file a bug report&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;make: *** No targets specified and no makefile found. Stop.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;いろいろ調べた結果、&#10;&lt;a href="https://issueantenna.com/repo/pyenv/pyenv/issues/2284" target="_blank" rel="noopener noreferrer"&gt;https://issueantenna.com/repo/pyenv/pyenv/issues/2284&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Web開発ど素人がNode.jsで多言語Webニュースアプリ作ってみた</title><link>https://www.kinjo.tech/posts/20220418/</link><pubDate>Mon, 18 Apr 2022 00:00:00 +0000</pubDate><guid>https://www.kinjo.tech/posts/20220418/</guid><description>&lt;h1 id="目的"&gt;目的&lt;/h1&gt;&#10;&lt;p&gt;筆者自身はトリリンガル（自称）のため、簡単に日本語、英語、中国語などの複数の言語のホットニュースを読めるサービスががあると便利だなとずっと思っていました。「Google Newsで良くない？」って言われそうなところですが、Google Newsはやや使いづらいと感じました。&#10;&lt;img src="https://www.kinjo.tech/posts/20220418-5_hu_794e1fb8c42d97bd.png"&#10; srcset="https://www.kinjo.tech/posts/20220418-5_hu_d0a4c77aa4101a10.png 480w, https://www.kinjo.tech/posts/20220418-5_hu_c64ad696f721c1ff.png 960w, https://www.kinjo.tech/posts/20220418-5_hu_794e1fb8c42d97bd.png 1038w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="1038" height="846"&#10; alt=""&#10; loading="eager" decoding="async" fetchpriority="high"&gt;&lt;/p&gt;&#10;&lt;p&gt;例えば、&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;言語を切り替えるのに「言語地域→候補から選ぶ→更新」3回クリックする必要がある&lt;/li&gt;&#10;&lt;li&gt;本当にヘッドライトしか閲覧したくないのに、「おすすめ」や「ピックアップ」などがうるさい&lt;/li&gt;&#10;&lt;li&gt;モバイルのweb版が見づらい・アプリをダウンロードしたくない&lt;/li&gt;&#10;&lt;li&gt;芸能ニュースにまったく興味がないため、ニュースの表示をカスタマイズしたい&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;また、やってみないと（何かを残さないと）学んだ技術をすぐ忘れるのはもったいないのも考えて、多言語ニュースWebアプリを自作するという発想に至りました。&lt;/p&gt;&#10;&lt;h2 id="使った技術"&gt;使った技術&lt;/h2&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;バックエンド&#10;&lt;ul&gt;&#10;&lt;li&gt;NodeJS&lt;/li&gt;&#10;&lt;li&gt;Express&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;フロントエンド&#10;&lt;ul&gt;&#10;&lt;li&gt;インタラクションがほぼないため、フレームワークは使っておらず、DOMをいじっただけ&lt;/li&gt;&#10;&lt;li&gt;CSSは&lt;a href="https://startbootstrap.com/" target="_blank" rel="noopener noreferrer"&gt;Start Bootstrap&lt;/a&gt;の無料テンプレートを使用&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;DB&#10;&lt;ul&gt;&#10;&lt;li&gt;MongoDB&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;li&gt;インフラ&#10;&lt;ul&gt;&#10;&lt;li&gt;Heroku&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;h2 id="コスト"&gt;コスト&lt;/h2&gt;&#10;&lt;p&gt;毎月7ドルだけです！！（Heroku Hobbyの料金）&lt;/p&gt;&#10;&lt;h2 id="成果物"&gt;成果物&lt;/h2&gt;&#10;&lt;p&gt;&lt;a href="https://www.multitrue.news" target="_blank" rel="noopener noreferrer"&gt;https://www.multitrue.news&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;a href="https://github.com/jckinjo/multitrue" target="_blank" rel="noopener noreferrer"&gt;https://github.com/jckinjo/multitrue&lt;/a&gt;&lt;/p&gt;&#10;&lt;p&gt;&lt;img src="https://www.kinjo.tech/posts/20220418-1_hu_15262c0ef60164e1.png"&#10; srcset="https://www.kinjo.tech/posts/20220418-1_hu_1e0f20a613eae1dd.png 480w, https://www.kinjo.tech/posts/20220418-1_hu_7247097b7fbf4a7a.png 960w, https://www.kinjo.tech/posts/20220418-1_hu_15262c0ef60164e1.png 1012w"&#10; sizes="(max-width: 767px) calc(100vw - 28px), (max-width: 815px) calc(100vw - 48px), 720px"&#10; width="1012" height="1396"&#10; alt=""&#10; loading="lazy" decoding="async"&gt;&lt;/p&gt;&#10;&lt;p&gt;それでは、詳細を解説していきます。&lt;/p&gt;&#10;&lt;h1 id="下準備"&gt;下準備&lt;/h1&gt;&#10;&lt;h2 id="ニュースはどこから収集するか"&gt;ニュースはどこから収集するか&lt;/h2&gt;&#10;&lt;p&gt;最初はニュース収集するかについてけっこう時間を費やしました。ニュース関連のAPIがかなり多いですが、&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;ある程度の無料枠がある&lt;/li&gt;&#10;&lt;li&gt;多言語のニュースが簡単に取れる&lt;/li&gt;&#10;&lt;li&gt;使いやすさ&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;3つの観点から考えて、&lt;a href="https://newsapi.org/" target="_blank" rel="noopener noreferrer"&gt;NewsAPI&lt;/a&gt;と&lt;a href="https://newsdata.io/" target="_blank" rel="noopener noreferrer"&gt;News data&lt;/a&gt;の2つのAPIに絞りました。ただ、後々&lt;a href="https://newsdata.io/" target="_blank" rel="noopener noreferrer"&gt;News data&lt;/a&gt;は言語を指定しても他の言語のニュースが混ざっていることがあると気づいたので（例えば、言語を日本語と指定したにも関わらず、日本関連の英語ニュースが出てくる）、&lt;a href="https://newsapi.org/" target="_blank" rel="noopener noreferrer"&gt;NewsAPI&lt;/a&gt;一択となりました。ちなみに、&lt;a href="https://blog.api.rakuten.net/ja/api-tutorial-news-api-jp/" target="_blank" rel="noopener noreferrer"&gt;こちら&lt;/a&gt;の記事はNewAPIについて詳しく説明しています。また、RSSなど他の人が作ったニュースAPIを使わないという方法もあるようですが、今回は試していなかったです。&lt;/p&gt;</description></item></channel></rss>