TVerはDroidKaigi 2024に協賛します

こんにちは、TVerAndroidエンジニアをしている石井です。
株式会社TVerはDroidKaigi 2024のサポーターとして協賛することになりました。

DroidKaigiとは

DroidKaigiはエンジニアが主役のAndroidカンファレンスです。 今年で10年を迎えるDroidKaigiは、Android技術情報の共有とコミュニケーションを目的に、2024年9月11日(水) - 13日(金)の3日間開催します。(HPより引用)

オフライン会場: ベルサール渋谷ガーデン

TVerAndroid

TVerは昨年Androidエンジニアが2名入社し、昨年9月頃に完全内製化が完了しました。
品質や生産性向上のための取り組みとして、現在TVeriOS/Androidアプリではリアーキテクチャに取り組んでいます。 AtomicDesignを採用した独自のデザインシステムの構築や、JetpackComposeによるUIの構築、Androidアプリアーキテクチャガイドをベースとしたリアーキテクチャの実施などを中心にモダンな技術スタックに移行を進めております。

またTVerはモバイルアプリ以外に、AndroidTVやFireTVなどのTVアプリ開発にも力を入れています。 その中でDroidKaigiを通じてAndroid技術の今後の発展を祈り、些細ながらも協賛させていただくことになりました。

TVerで働くAndroidエンジニア募集中!

TVerアプリをより使いやすく便利にしていくための様々な機能開発を行っていくためにもまだまだAndroidエンジニアが必要な状況です。 TVerのミッション「#テレビを開放して、もっとワクワクする未来を」に共感いただけるテレビの未来を支えるエンジニアの方をお待ちしております!

herp.careers

iOSDC Japan 2024に参加してきました!

みなさんこんにちは、TVerでEngineering Managerをしている高橋 (@ukitaka) です。

8/22-8/24で開催されたiOSDCに参加してきましたので、 少々遅くなりましたが #iwillblog しておこうかなと思います!

久しぶりのiOSDCオフライン参加

前夜祭参加組で記念撮影

個人的な話にはなってしまうのですが、iOSDCオフライン参加するのはかなり久しぶりで 2018年に登壇して以来6年ぶりでした。

当時の発表資料 speakerdeck.com

もはや界隈から忘れ去られているかもなとドキドキしながら会場入りしたんですが、いろんな方々にお声がけいただいただけではなく、がんばってスポンサーしたりパンフレット書いたりした甲斐あってか「いまTVerにいらっしゃるんですよね!?」と所属まで知っていただけていたのでとても安心しましたし、嬉しかったです。

スポンサーブースの様子

iOSDCは相変わらず文化祭のようなワイワイ感があって最高ですね・・スポンサーブースも各社気合い入っていて本当にお祭りみたいでした。来年も開催されるのであれば、次こそはTVerブースを出そうと心に誓いました。

Forkwellさん、ノベルティのうちわもブースもユニークで最高でした

マネフォさんのブース。 TVerは2024年8月時点では iOS14.0+ なんですが、もはや少数派ですね、、

スポンサーセッションの様子

Day1 の14:30~ はスポンサーセッションで弊社iOSエンジニアの小森が登壇し、リアーキテクチャ戦略についてお話しさせていただきました。

iOSエンジニア小森によるスポンサーセッションの発表

speakerdeck.com

発表聞いていただいた方はわかったかもしれないですが、実はTVer iOSアプリのリアーキテクチャはまだ始まったばかりで「これからやっていくぞ!」という発表でした。来年もiOSDCが開催されるのであれば、このリアーキテクチャがうまくいったのか?うまくいかなかったとしたら何が起こったのか?みたいなところをぜひお話しできればと考えています!

最後に

iOSDC Japan 2024、本当にめちゃくちゃ楽しかったです。運営の方々、スポンサーセッションを聞いてくれた方々、話しかけてくれたみなさん本当にありがとうございました。

そして発表スライドにもあった通り、iOSチームの状況は以下の通りなので一緒にTVer iOSアプリを作っていってくれる方々のご応募をお待ちしております!!

 

このスライド、「圧がすごい」というXのポストを見かけました

herp.careers

Backend Enabling Team ができました in TVer

はじめに

こんにちは。TVerでバックエンドエンジニアをやっている伊藤(@kanataxa)です。

TVerをより多くの方に利用していただくために、バックエンドチームでは機能開発と並行して開発サイクルの高速化や品質向上にも取り組んでいます。

その中で2024/7に組織変更が行われ、「開発サイクルにフォーカスする」ことを目的としてEnabling Teamが立ち上げられました。 今回はそのEnabling Teamについてです。

TVerのバックエンドチームの現状と合わせて、これから何をしていくのかを書いていきたいと思います。

TVerのバックエンドチームの現状

バックエンドチームはTVerサービスの内製化に向けてできたチームです。2022/4にリニューアルが行われたので、まだできて数年の歴史の浅いチームです。 またサービス規模に対して当初は数名ととても小さなチームでした。

このような状況のため、日々の開発の中で犠牲にしなければならなかったものも多く、属人化を許容し、いくつかの要件もサービス特性的に致命でない部分に関しては割り切らないといけないこともありました。

しかしながら、この2年でチームは拡大し十数名のエンジニアが所属することとなり、ようやくいくつかの開発を複数人で行えるようになってきました。 そしてサービスの成長やあり方の時間的変化に伴い徐々に組織的に開発を行うことが求められ、これらの諦めてきた部分が機能開発において大きな障壁になりつつあります。 暗黙的な何かが大きくなってしまい、新規メンバーの初速が出にくくスケールが難しい状態になっているのです。

サービスを成長させていくためにもまだまだチーム拡大を行いたいという想いもあり、認知負荷の軽減や、よりデファクトスタンダートに則った開発サイクルが強く求められるようになってきました。

なぜEnabling Teamを立ち上げるのか

TVerのバックエンドチームの現状 でも述べたとおり、より多くの機能開発をはやく行うには抜本的に開発サイクルを見直す必要が出てきました。 その課題にフォーカスを当てることのできる組織体系にしようとした結果、Enabling Teamが立ち上げられました。

将来的にはよりTeam Topologiesにあるような組織を目指したいと考えていますが、現時点ではより限定的な課題に対して「バックエンドエンジニア×リードタイム」の最適化を目指すためのチームです。

つまり、機能開発チームがメンバーの能力になるべく依存しない状態で、より多くのサービス課題に注力できるように「当たり前を整備していく」ためのチームです。

TVerにおけるEnabling Team(2024/7時点)

まだまだ数名の小さいチームであるため、役割を限定的にしつつ中長期的なゴールを定めています。 そのゴールを達成するために、今期はなにをやる/やらないのかということを半期に1回見直すこととしています。

2024/7時点での取り組みを以下でいくつか紹介します。*1

目の前に積まれた課題を消化する

バックエンドチーム全体の課題として、リリース時にオミットしたものが多く存在しています。 チーム全体の開発サイクルを最適化するには、これらの課題の多くを消化する必要があります。*2

今は高度な技術の導入・検証より、当たり前の世界を作るために手を動かすことを重視しています。

ドキュメント駆動の開発の推進

「なぜ」の部分が残っていないと意思決定を把握することが途端に難しくなります。 そのためEnabling TeamではADRによるドキュメント駆動での開発を行うこととし、機能開発チームに対してはDesign Docの導入推進を行い、テンプレート作成等の支援を行っております。

またドキュメントのメンテナンスコストをそのまま開発コストに転換できるような、OpenAPIやDDLに対するスキーマ駆動開発の導入も進めています。

余談ですが、TVerのバリューのうち1つ「我々は仲間である」と関連させて、「未来の仲間への投資」としています。

開発サイクルの計測の準備

目の前の課題もいつかは終わりが来ます。その後を見据えて「開発」に対してどのような計測ができるのか準備を進めています。

3つほど紹介させていただきましたが、他にも監視通知やアプリケーションログ・エラーの標準化、GitHub移行など開発の足回りの整備を進めています。 またSREと連携してトイルの撲滅や、New Relicをより活用するための実装支援や仕組み作りも今後行っていく予定です。

おわりに

今回はTVerのバックエンドチームにおけるEnabling Teamの立ち上げについてざっくり書いてみました。 まだまだ小さく歴史の浅い組織で課題が山積みですが、サービス成長を支えるべく、よりよい技術組織を目指して日々取り組んでおります。

TVerではエンジニアを積極的に採用中ですので、 TVerのミッション「テレビを開放して、もっとワクワクする未来をTVerと新しい世界を、一緒に。」に共感いただける方、ぜひお気軽にご応募ください!

https://herp.careers/v1/tver

*1:今は相当泥臭いフェーズだと思っています。

*2:ただし(一般化できない)コード負債の回収のような表面的な課題は範囲外としています。

CloudNativeDaysSummer2024で登壇しました #CNDS2024

はじめに

はじめまして!

TVerのSREチームでオブザーバビリティ推進を担当している鈴木 彩人と申します。

6/15(土)に札幌で開催されたCloudNative Days Summer 2024にて登壇しました! event.cloudnativedays.jp

本イベントのダイヤモンドスポンサーであるNew Relic様から声をかけていただいたため、貴重な体験ができると思い登壇することにしました。

(弊社では会社の費用でカンファレンスに参加できる非常に良い制度があります)

CloudNative Daysについて

公式サイトより引用。

CloudNative Days はコミュニティ、企業、技術者が一堂に会し、クラウドネイティブムーブメントを牽引することを目的としたテックカンファレンスです。 最新の活用事例や先進的なアーキテクチャを学べるのはもちろん、ナレッジの共有やディスカッションの場を通じて登壇者と参加者、参加者同士の繋がりを深め、初心者から熟練者までが共に成長できる機会を提供します。

参考 : https://cloudnativedays.jp/

登壇内容について

登壇資料は以下になります。 speakerdeck.com

今回の登壇ではサービス規模拡大に伴いカスタマーサポートと開発チーム双方の運用負荷が増加している課題をNew Relicを活用して改善した事例について話しました。エンジニアではないメンバーがここまでNew Relicを活用して課題解決する話は珍しいと思い、今回の登壇テーマにしました。

登壇では実際の活用事例を交えて弊社のカスタマーサポートと開発チームの連携について紹介しましたが、顧客満足度の向上の重要性について話せなかったため、本ブログにて補足させてください。

顧客満足度に関連する法則としてグッドマンの法則というものが存在します。

グッドマンの法則とは要約すると以下になります。

  • 不満を申し立てたお客様の苦情に迅速に解決することが出来ると、リピーターになりやすい
  • ネガティブな口コミはポジティブな口コミと比較して2倍以上も広がりやすい
  • 消費者の信頼を獲得することで行為的な口コミの波及と商品購入意図が高まり市場拡大に繋がりやすい

参照元顧客ロイヤルティ協会・佐藤知恭

上記法則から、多くの利用者にサービスを提供をする上で顧客満足度の向上が非常に重要であることが分かります。

オブザーバビリティを確保することで顧客満足度向上に繋がることから、引き続きオブザーバビリティの推進を頑張っていきたいと思います。

登壇の様子

ちなみに今回の登壇資料では最新の会社紹介資料のスライドを引用しております。TVerを知る上でとてもわかりやすい資料となっておりますのでぜひご覧下さい。

speakerdeck.com

まとめ

200人弱の前で話す機会は滅多にないため、貴重な体験ができたことに大変感謝しております。(緊張で少しぎこちない登壇にはなってしまいましたが…笑)

登壇後にXや満足度アンケートの結果を確認したところ、ポジティブなご意見が多かったので、登壇した甲斐がありました。

イベントや懇親会では様々なエンジニアから貴重なお話を聞くことができ、モチベーション向上にも繋がったため、こういった機会があればまた登壇したいと思います。

TVerはiOSDC Japan 2024に協賛します!

こんにちは、TVerでエンジニアリングマネージャーをしている高橋 (@ukitaka) です。 TVerは今年もiOSDCに協賛させていただくことになりました!

TVeriOSエンジニア

昨年のiOSDCの時点では「iOSエンジニアがいなくても泣かない!配信サービスのiOSアプリにおける オブザーバビリティの導入と改善」というタイトルで発表があった通り、TVerにはiOSエンジニアが不在の状況だったのですが、昨年1名iOSエンジニアが入社したところからチームが立ち上がり、今年4月には完全内製化が完了しました。さらに5月には元iOSエンジニア(?)の自分もエンジニアリングマネージャーとしてjoinし、徐々に体制が整いつつあります!

内製化への歩み

現在のTVer iOSアプリにおける主な取り組み

さらに品質や生産性を向上させていくための取り組みとして、現在TVeriOS/Androidアプリではリアーキテクチャに取り組んでいます。UIKitやRxSwiftなどを中心とした現在の技術スタックからSwiftUIやTCA、Swift Concurrencyなどのモダンな技術スタックに移行を進めていっているところです。

ありがたいことにTVerはサービスは順調に成長しており、MUB 3,500万、月間4.5億回再生などの数字から見ても分かる通り非常に多くの利用者を抱えるサービスになっています。

そのような状況の中でのリアーキテクチャ実施にはいくつか考慮しなければならないポイントがあります。 例えば不具合発生時のリスクをいかに小さくするかです。 当然ながら影響範囲が大きいほど不具合は発生しやすくなってしまいますし、大規模サービスであれば1%の不具合発生率であっても数十万人に影響することになってしまいます。また いかにサービスグロースを止めずにリアーキテクチャを推進するか?という観点も重要になってきます。 iOSDC2024のTVerのスポンサーセッションでは、そのあたりを考慮しながらどのようにリアーキテクチャを推進していくのかについて、弊社iOSエンジニアの小森が発表させていただきます。 8/23 (金) Day1 Track Aにて14:30〜発表予定ですので、ご興味ある方はぜひ覗いてみてください!

TVerで働くiOSエンジニアをまだまだ募集中!

アーキテクチャのようなベースの内部品質をあげていくための取り組みだけでなく、TVerアプリをより使いやすく便利にしていくための様々な機能開発を行っていくためにもまだまだiOSエンジニアが必要な状況です。 TVerのミッション「#テレビを開放して、もっとワクワクする未来を」に共感いただけるテレビの未来を支えるエンジニアの方をお待ちしております!

累計7,000万DLの民放公式テレビポータルのアプリ開発を担う、iOSエンジニアを募集! 

実務でのテーブル結合時のケア(重複排除など)について

こんにちは、TVerでデータ分析をしている高橋です。
弊社の分析業務の多くは BigQuery に蓄積されているログを使った分析で、大量のログを扱うため前処理から集計まで全てSQLで行っています。
本記事では、SQLを書く上で特に気を付けているテーブル結合時のケアについて紹介します。

分析業務の一例

「ホーム画面を開いてから10分以内にコンテンツを再生した割合を知りたい」という依頼が来ました1
この集計は訪問ログ視聴ログを使い、ホーム画面に訪問したログを10分以内に再生した or 再生してないの2種類に分ければできそうです。
ここで、集計に用いるテーブルを簡単に紹介します。

訪問ログ (view_logs)

ホーム、マイページ、番組ページ、エピソードページなどに訪問したタイミングで発報されるログです。
ユーザー毎に時系列順に並べることで、サービス内でのページ遷移が分かります。

-- view_logs サンプルデータ
SELECT TIMESTAMP("2024-03-01 19:30:00") AS viewed_at, "hoge" AS user_id, "/home" AS url, 
UNION ALL SELECT TIMESTAMP("2024-03-01 19:32:00"), "hoge", "/mypage/fav"
UNION ALL SELECT TIMESTAMP("2024-03-01 19:35:00"), "hoge", "/episodes"
UNION ALL SELECT TIMESTAMP("2024-03-01 21:45:00"), "fuga", "/home"
UNION ALL SELECT TIMESTAMP("2024-03-01 22:25:00"), "piyo", "/home"
UNION ALL SELECT TIMESTAMP("2024-03-01 22:30:00"), "hogera", "/home"
UNION ALL SELECT TIMESTAMP("2024-03-01 22:32:00"), "hogera", "/search"
UNION ALL SELECT TIMESTAMP("2024-03-01 22:34:00"), "hogera", "/home"
viewed_at user_id url
2024-03-01 19:30:00 hoge /home
2024-03-01 19:32:00 hoge /mypage/fav
2024-03-01 19:35:00 hoge /episodes
2024-03-01 21:45:00 fuga /home
2024-03-01 22:25:00 piyo /home
2024-03-01 22:30:00 hogera /home
2024-03-01 22:32:00 hogera /search
2024-03-01 22:34:00 hogera /home

視聴ログ (play_logs)

視聴中の行動が記録されているログです。これを見るとシークバー移動のタイミングなどが分かります。
今回は視聴開始した時刻の情報だけ使用します。

-- サンプルデータ
SELECT TIMESTAMP("2024-03-01 19:35:30") AS begin_at, "hoge" AS user_id, "begin" AS action,
UNION ALL SELECT TIMESTAMP("2024-03-01 22:26:00"), "piyo", "begin",
UNION ALL SELECT TIMESTAMP("2024-03-01 22:27:00"), "piyo", "begin",
UNION ALL SELECT TIMESTAMP("2024-03-01 22:35:00"), "hogera", "begin"
begin_at user_id action
2024-03-01 19:35:30 hoge begin
2024-03-01 22:26:00 piyo begin
2024-03-01 22:27:00 piyo begin
2024-03-01 22:35:00 hogera begin

集計用クエリを書いてみる

今回の集計を行うには、どのようなクエリを書けばよいでしょうか?
素朴にやるなら、

  1. 訪問ログからホーム画面(/home)のログを抽出する
  2. 1に視聴ログを LEFT JOIN する

でしょうか。書いてみましょう。

SELECT
    view_logs.*,
    play_logs.begin_at,
    play_logs.begin_at IS NOT NULL AS has_played,
FROM (
    SELECT
        *
    FROM
        view_logs
    WHERE
        url = "/home"
) AS view_logs
LEFT OUTER JOIN
    play_logs
ON
    view_logs.user_id = play_logs.user_id
    -- 訪問後 10 分以内に再生してるか
    AND play_logs.begin_at BETWEEN view_logs.viewed_at AND TIMESTAMP_ADD(view_logs.viewed_at, INTERVAL 10 MINUTE)
ORDER BY
    viewed_at
viewed_at user_id url begin_at has_played
2024-03-01 19:30:00 hoge /home 2024-03-01 19:35:30 true
2024-03-01 21:45:00 fuga /home false
2024-03-01 22:25:00 piyo /home 2024-03-01 22:26:00 true
2024-03-01 22:25:00 piyo /home 2024-03-01 22:27:00 true
2024-03-01 22:30:00 hogera /home 2024-03-01 22:35:00 true
2024-03-01 22:34:00 hogera /home 2024-03-01 22:35:00 true

一見良さそうに見えますが、以下の問題があります。

  • user_id=piyo の 1回の/home訪問に2回の再生が紐づいている(JOIN によってレコードが増えた、いわゆる重複)
    • 分母となるホーム画面の訪問数が増えてしまう
  • user_id=hogera の2回の/home訪問に1回の再生が紐づいている
    • 時系列で考えると再生に直接寄与したのは2回目の/home訪問だと考えられるが、1回目の/home訪問も再生に寄与したと見なされてしまう

このまま集計すると間違った示唆を与えてしまうおそれがあります。
順番に解決していきましょう。

1回の訪問に2回の再生が紐づいているケース

このケースは短時間で複数回再生した場合に発生します(ザッピング的な再生など)。
今回の集計で興味があるのは10分以内の再生有無だけなので、/home訪問後最初の視聴が紐づくログだけ残すようにしましょう。
この処理は以下のようなQUALIFY句によって実現することができます。

QUALIFY
  -- 最初の視聴だけ残す
    ROW_NUMBER() OVER(PARTITION BY user_id, viewed_at ORDER BY begin_at ASC) = 1

ちなみに viewed_at, user_id, url をキーとしてGROUP BYしても同様の処理が可能です。
個人的にはSELECT文を変更する必要がない QUALIFY句を使用することが多いです。

2回の訪問に1回の再生が紐づいているケース

短時間で回遊したのちに再生した場合などでしばしば発生します。
このケースは視聴ログの突合条件に次の/home訪問までに再生しているかという条件を追加することで排除できそうです。
はじめに、view_logsに次の/home訪問した時刻の列を追加します。

SELECT
    *,
    -- 次の /home 訪問時刻
    LEAD(viewed_at, 1) OVER(PARTITION BY user_id ORDER BY viewed_at) AS next_viewed_at,
FROM
    view_logs
WHERE
    url = "/home"

この列を使い、以下のような処理を突合部分に追加します。

-- 次の /home 訪問までに再生してるか (次の /home 訪問がなければ無視)
AND IF(view_logs.next_viewed_at IS NOT NULL, play_logs.begin_at < view_logs.next_viewed_at, TRUE)

改良版クエリ

最終的にこのようになりました。

SELECT
    view_logs.* EXCEPT(next_viewed_at),
    play_logs.begin_at,
    play_logs.begin_at IS NOT NULL AS has_played,
FROM (
    SELECT
        *,
        -- 次の /home 訪問時刻
        LEAD(viewed_at, 1) OVER(PARTITION BY user_id ORDER BY viewed_at) AS next_viewed_at,
    FROM
        view_logs
    WHERE
        url = "/home"
) AS view_logs
LEFT OUTER JOIN
    play_logs
ON
    view_logs.user_id = play_logs.user_id
    -- 訪問後 10 分以内に再生してるか
    AND play_logs.begin_at BETWEEN view_logs.viewed_at AND TIMESTAMP_ADD(view_logs.viewed_at, INTERVAL 10 MINUTE)
    -- 次の /home 訪問までに再生してるか (次の /home 訪問がなければ無視)
    AND IF(view_logs.next_viewed_at IS NOT NULL, play_logs.begin_at < view_logs.next_viewed_at, TRUE)
-- 最初の視聴だけ残す
QUALIFY
    ROW_NUMBER() OVER(PARTITION BY user_id, viewed_at ORDER BY begin_at ASC) = 1
ORDER BY
    viewed_at
viewed_at user_id url begin_at has_played
2024-03-01 19:30:00 hoge /home 2024-03-01 19:35:30 true
2024-03-01 21:45:00 fuga /home false
2024-03-01 22:25:00 piyo /home 2024-03-01 22:26:00 true
2024-03-01 22:30:00 hogera /home false
2024-03-01 22:34:00 hogera /home 2024-03-01 22:35:00 true

viewed_at, begin_at どちらも一意化することができました。
このCTE を GROUP BYすることで目的の集計をすることができます。

最後に

テーブル結合時のケアについて2つの事例を紹介しましたが、これらの事象の発生を集計値だけ見て気付くことは非常に難しいです。
そのためレビューの際にはクエリのロジックを確認することは勿論のこと、中間テーブルの出力を確認したり個票チェックをしたりすることで集計ロジックの確からしさを検証しています。
同時に「レビューしやすいクエリ」を書くために、ロジックを考えたり社内ルール整備をしたりなどを日々行っています。この取り組みは分析チームが一丸となり、相当な力を入れて取り組んでいます2

採用のこと

TVerでは一緒に分析業務をしてくれる方を募集しています。
カジュアル面談も受け付けていますので、「こういう取り組みのことをもっと知りたい」「普段どんな分析してるか知りたい」と思った方は以下よりお気軽にご連絡ください。お待ちしております!

herp.careers


1: 一例なのでかなり大味な依頼になっており、この集計結果を受けて具体的なアクションを行うことが難しいと予想されます。実際にこのような依頼が来た場合は、ホーム画面から再生までのユーザーの振る舞いや遷移についての仮説出しをしてからクエリを作成することが望ましいです。

2: テックブログにておいおい紹介する予定です。

AWS LambdaとSlackを連携してツールを作った話

こんにちは。
アドテク領域のエンジニアをしています安部です。
こちらは TVer Advent Calendar 2023 の14日目の記事です。

13日目の記事で「ツールを作成した」という話をちらっと書きました。
今回はそのツールについて備忘として書きます。
ツールは作成時は半自動状態(起動トリガーが手動)、12月に全自動化となりました。

ツールを作ったきっかけ

4月には広告入稿システムのリプレイスがありました。
このシステムと対向システムでデータの同期しているのですが、データに差分が出ていないか確認する必要があります。
そのため対向システムから不整合が発生している可能性のあるデータを連携していただき、確認するという定常業務がありました。
不整合の可能性があるデータは平均20件ほどあり、毎日手動で確認することは現実的ではありませんでした。
どうにか自動化できないかとツールを作成することにしました。

ツール作成時の条件

  1. 連携されるデータはテキストデータとして受領する(Slackでメッセージの受信ができない)
  2. 対向システム側に対応をお願いすることはできない(S3に直接アップロードしてください、特定の形式のファイルでくださいなど)
  3. 項目が多すぎると全て連携されないことがある(途中で途切れたメッセージになる)

1と2がネックとなりデータを自動でLambdaへ取り込むことができませんでした。
そのため

  • データを取得し.mdファイルを作成してS3に置く
    →手動
  • ファイルを読み込み、データを突合し、その結果をSlackの特定のチャンネルに投稿
    →Lambdaで自動化

という形式の半自動化ツールを作成しました。

なぜAWS、Lambdaを選んだのか

  • 広告入稿システムが乗っているAWSと同じ場所に作ることで自分以外の人もメンテナンスできる
  • RDSとの連携が容易
  • Pythonでさくっと書きたい
    の3点が主な理由です。

システム構成図

・半自動

・全自動

ツールの詳細

①S3のバケットからファイルを取得

ライブラリ(boto3)をありがたく活用しました。

s3_client = boto3.client('s3')
# ファイルを読み込み、使用できる状態にする
response = s3_client.get_object(Bucket=MD_BUCKET_NAME, Key=md_key_name)
lines = response['Body'].readlines()

SQLの作成

ファイルを1行ずつ読み込みながらRDSへ投げるSQLを作成します。

③RDS接続・確認

RDSの接続情報についてはSecretsManagerに設定されているので、SecretsManagerから情報を取得しRDSへ接続します。
 SecretsManagerからの取得はこんな感じ
 REGION_NAMEにはリージョン名を、SECRETE_NAMEにはシークレットの名前を入れます。
シークレットキー(DB_USER_NAME,DB_USER_PASSWORD)を指定することでシークレットの値を取得することができます。

# Create a Secrets Manager client
session = boto3.session.Session()
client = session.client(
  service_name='secretsmanager',
  region_name=REGION_NAME
)

try:
  get_secret_value_response = client.get_secret_value(
    SecretId=SECRETE_NAME
  )
  except ClientError as e:
    raise e

# Decrypts secret using the associated KMS key.
secret_data = get_secret_value_response['SecretString']
secret = ast.literal_eval(secret_data)
# 接続情報設定
db_user = secret['DB_USER_NAME']
db_pass = secret['DB_USER_PASSWORD']

この接続情報を用いてRDSへ接続します。 RDS接続の際はVPC設定が必要です。
こちらを参考に作成しました。
SQLの結果で不整合データの有り無しを確認します。

④Slackへ結果を送信

Incoming Webhook URLを使用してSlackへ結果をポストします。
他の投稿と差別化したかったので引用マークが付くように設定しています。

 # SlackのwebhookURL
WEB_HOOK_URL = 'WEBHOOK_URL'
# alert-adcas-app-error宛
CHANNEL_ID = 'CHANNEL_ID' 

# Slack投稿の情報
send_data = {
  'channel': CHANNEL_ID,
  'username': 'CHECK_TOOL',
  'icon_emoji': ':beer:',
  'attachments':[{        #投稿の引用マーク部分の設定
    'color': '#ffb6c1',
    'text': '投稿テストです。', 
  }],
}
send_text = json.dumps(send_data)
request = urllib.request.Request(
  WEB_HOOK_URL, 
  data=send_text.encode('utf-8'), 
  method="POST"
)
with urllib.request.urlopen(request) as response:
  response_body = response.read().decode('utf-8')

こんな感じで投稿されます。

⑤起動トリガーの設定

最後に起動トリガーを設定します。
S3にファイルを置いたタイミングで起動してほしいので、トリガーにS3を設定します。
関数の概要部分の「+トリガーを追加」からS3を選択し、ファイルを置くバケットを選択します。
ファイルの配置と同時になのでEvent typesはPUT(一応POSTも)、アップロードするファイルサイズが大きくなるとマルチパートになるらしいので、念のためMultipart upload completedも設定します。
Suffixに「.md」が設定してあるのは、mdファイルを読み込むという条件にしているためです。

これでファイルを配置と同時にツールが実行されるようになりました。

ファイルアップロードが手動というのがもったいないですが、しばらくこれで運用していきます。

全自動化

12月、ついにデータがSlackのメッセージで受信できるようになりました。
これで取り込みが自動化できます。

半自動化している部分は活用したいので、メッセージを取得しS3にファイルを配置するという関数を作成します。

①Slack Appの準備

Slack Appの作成方法は多くの方が書いていると思うのでそこに譲りまして、メッセージ取得・送信に必要なOAuthの設定だけご紹介します。
今回はBotとして使用するためBot Token Scopesに必要なScopeを追加します。
※画像の黒線部分はSlack Appのアプリ名が表示されています。 - groups:history
取得したいメッセージがプライベートチャンネルに投稿される場合の設定です。
パブリックチャンネルの場合は「channels:history」を追加します。 - chat:write
メッセージを投稿するための設定です。
- chat:write.customize
メッセージ投稿をする際にユーザー名やユーザーのアイコンを自由に設定できるようにするための設定です。
カスタマイズしない場合は「Basic Information」の「Display Information」で設定しているApp Nameとアイコンが表示されます。

②メッセージの取得

conversations.history を使用して取得します。
こんな感じ

def get_slack_message():
  SLACK_BOT_TOKEN = os.environ['SLACK_BOT_TOKEN']
  CHANNEL_ID = os.environ['CHANNEL_ID']
  slackGetMessageUrl = 'https://slack.com/api/conversations.history'
  
  dt_today = datetime.combine(date.today(),time(0,0,0))
  d_ut = datetime.timestamp(dt_today)
  data = {
    'channel': CHANNEL_ID,
    'oldest': d_ut
  }
  post_data = urllib.parse.urlencode(data)
  req = urllib.request.Request(slackGetMessageUrl, 
  data=post_data.encode())
  req.add_header('Authorization','Bearer ' + SLACK_BOT_TOKEN)
  with urllib.request.urlopen(req, timeout=1) as response:
      response_data = json.loads(response.read())
  print(response_data)

トークンとメッセージを取得したいチャンネルのチャンネルIDはlambdaの環境変数に設定してそこから取得しています。
conversations.history には色々オプションを設定できますが、実行当日のメッセージのみ取得したいのでoldestに当日の0:00のunixtimeを設定しています。
オプションについてはドキュメントに書いてあるので必要に応じて追加します。

api.slack.com

確認できるようにprintでメッセージを表示するようにしています。

③S3へ配置

取得したメッセージをこねこねして元のツールが読み取れるファイルを作成し、S3へ配置します。
実行ロールにS3のput権限をつけることをお忘れなく!
(設定タブ > アクセス権限 > ロール名を押すとIAMに飛ぶので許可を追加できます)

expost_data = '';
for line_data in reversed(response_data['messages']):
  #こねこねしてexport_dataに1行ずつ追加
  expost_data += line_data['text'];

S3_CLIENT = boto3.client('s3')
response = S3_CLIENT.put_object(
  Body=expost_data,
  Bucket=MD_BUCKET_NAME,
  Key=MD_FILENAME
)

この1行ずつ読み取るタイミングで

3.項目が多すぎると全て連携されないことがある(途中で途切れたメッセージになる)

のチェックも行っています。 連携の最後にendのプレフィックスを設定していただいているので、そのプレフィックスが存在しない場合はエラー通知をSlackに送信しファイル配置と読み取りの後続処理が動かないようにしています。

④Slack投稿部分のSlack API

最後に、Slack投稿部分をwebhookからSlack APIに変更します。
権限は①で付与済みなのでリクエスト送信部分をちょっと変えるだけ。

slackPostMessageUrl = 'https://slack.com/api/chat.postMessage';
   
# Slack投稿の情報(ここは変わらず)
send_data = {
  'channel': CHANNEL_ID,
  'username': 'CHECK_TOOL',
  'icon_emoji': ':beer:',
  'attachments':[{        #投稿の引用マーク部分の設定
      'color': '#ffb6c1',
      'text': '投稿テストです。', 
  }],
}
    
send_text = json.dumps(send_data)
request = urllib.request.Request(
  slackPostMessageUrl, 
  data=send_text.encode('utf-8'), 
  method="POST"
)
# トークン情報だけ追加
req.add_header('Authorization','Bearer ' + SLACK_BOT_TOKEN)
with urllib.request.urlopen(request) as response:
  response_body = response.read().decode('utf-8')

⑤起動トリガーの設定

EventBridgeで定時起動するように設定します。
Lambdaの設定タブ > トリガー > トリガーを追加 でEventBridgeを選択し、cron形式で書きました。
UTC時刻で記載することに注意です。
私はJSTで設定し、11:30に動かしたいのに20:30に起動したことがありました。
お恥ずかしい。

これで夢の全自動化完了です。

ツール作成してどうだった?

  1. 気持ちが楽になった
    そもそも最初はSQL手動実行だったので、手作業でミスしないかというプレッシャーがありました。
    ツール化することで手動でミスする不安から開放されました。 また、ファイル作って、AWSにアクセスして、S3にファイルを置いて…という時間にして3分もかからない対応ですがやるのとやらないのでは気持ちが違います。
    長期休みのときもチェック漏れが起こらないので安心です。

  2. Python好きだなぁ
    このツールでほぼ初めてPythonを触りましたが、個人的に書きやすくてもっと使いたいなと思いました。
    Pythonを使っているシステムはあまりないのですが、ツール職人するときは積極的に使っていこうと思います。

  3. 知見が広がった
    業務で使わないAWSの部分(EventBridgeなど)も触ることができて勉強になりました。
    もっと使いこなせるようになりたい!