KINTOテクノロジーズのブログ - TECH PLAY

TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

1123

This article is the entry for day 17 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-No-Michi-No-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station project was officially launched and later established as a team. As part of this initiative, we also run an in-house podcast, and for this year's Advent Calendar, we’re excited to share some of its episodes with you. What is Manabi-No-Michi-No-Eki (Learning Roadside Station) The Learning Roadside Station is a project launched to enhance accessibility and effectiveness of the study sessions that are frequently held within the company. The aim is to promote knowledge sharing by supporting study sessions led by volunteers within the company. Security & Privacy Study Session In our Podcast, we interview team members who hold in-house study sessions. It´s called “A Peek into the Study Session Next Door”. Today, we welcome Kuwahara-san, Morino-san, and Kasai-san, who are leading the Security and Privacy Study Sessions. Thank you all for joining us today. Could you start by introducing yourselves? Interview Morino-san: I’m Morino from the Security & Privacy Group. It’s a pleasure to be here. Kasai-san: My name is Kasai, and I’m part of the Data & Privacy Governance Team. Thank you for having me. Kuwahara-san: I’m Kuwahara from the Security CoE Group. Pleasure to meet you. HOKA-san: First, could you tell us about your daily work and responsibilities? Morino-san: Our team is divided into three sub-teams. The Data & Privacy Governance Team, the Information Security Team, and the Cybersecurity Defense Team. The Data & Privacy Governance Team is responsible for establishing rules and implementing governance regarding data and privacy. The Information Security Team conducts assessments based on information security standards. The Cybersecurity Defense Team manages vulnerabilities and investigates threat intelligence. Kasai-san: My team, the Data & Privacy Governance Team, establishes rules regarding data security management and personal information governance, conducts risk assessments, and reports the results to management. Kuwahara-san: The Security CoE Group is responsible for the security of cloud environments such as AWS, Azure, and Google Cloud. We set up guardrails to ensure safe cloud usage, monitor security risks, and provide support against potential threats. HOKA-san: Next, could you tell us more about the Security & Privacy Study Session? What kind of study session is it? Kuwahara-san: We started this study session to spread security-related knowledge among employees. Last year, we created detailed security guidelines, and to ensure awareness and adoption, we began hosting these study sessions. Morino-san: Our goal is to establish Security by Design and Privacy by Design as standard considerations in all projects. Kasai-san: Ultimately, we want engineers to be able to focus on development, while we support them with privacy and security risk management. HOKA-san: How has the response been so far? Kuwahara-san: So far, we have hosted four sessions, and the response has been very positive. Many attendees have shared that they found the sessions informative, and we’ve also received valuable feedback for improvements. Morino-san: Some participants have mentioned that our explanations have become clearer, and we continue to refine our approach. HOKA-san: What changes do you hope to see at KTC through these study sessions? Morino-san: We aim to have security and privacy features implemented as standard practices. Kuwahara-san: Ideally, security should become so natural and ingrained that our roles become unnecessary. Kasai-san: I want to create an environment where engineers can focus on development with confidence, knowing that security is taken care of. HOKA-san: Finally, what inspired each of you to pursue this field? Morino-san: I started in software development and gradually developed a deep interest in security. Kasai-san: Initially, I wasn't particularly interested in security or privacy, but I became fascinated by governance and its impact. Kuwahara-san: I began my career as an infrastructure engineer, but after experiencing a security breach on a mail server I was managing, I shifted my focus to security. HOKA-san: Thank you all for your time today. We hope the Security and Privacy Study Session continues to be a valuable resource for everyone at KTC. Thank you everyone. This time, we provided details about the Security and Privacy Study Session, the background to its operation, and future prospects. Please look forward to the next study session!
This article is the entry for day 17 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-No-Michi-No-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station project was officially launched and later established as a team. As part of this initiative, we also run an in-house podcast, and for this year's Advent Calendar, we’re excited to share some of its episodes with you. What is Manabi-No-Michi-No-Eki (Learning Roadside Station) The Learning Roadside Station is a project launched to enhance accessibility and effectiveness of the study sessions that are frequently held within the company. The aim is to promote knowledge sharing by supporting study sessions led by volunteers within the company. Security & Privacy Study Session In our Podcast, we interview team members who hold in-house study sessions. It´s called “A Peek into the Study Session Next Door”. Today, we welcome Kuwahara-san, Morino-san, and Kasai-san, who are leading the Security and Privacy Study Sessions. Thank you all for joining us today. Could you start by introducing yourselves? Interview Morino-san: I’m Morino from the Security & Privacy Group. It’s a pleasure to be here. Kasai-san: My name is Kasai, and I’m part of the Data & Privacy Governance Team. Thank you for having me. Kuwahara-san: I’m Kuwahara from the Security CoE Group. Pleasure to meet you. HOKA-san: First, could you tell us about your daily work and responsibilities? Morino-san: Our team is divided into three sub-teams. The Data & Privacy Governance Team, the Information Security Team, and the Cybersecurity Defense Team. The Data & Privacy Governance Team is responsible for establishing rules and implementing governance regarding data and privacy. The Information Security Team conducts assessments based on information security standards. The Cybersecurity Defense Team manages vulnerabilities and investigates threat intelligence. Kasai-san: My team, the Data & Privacy Governance Team, establishes rules regarding data security management and personal information governance, conducts risk assessments, and reports the results to management. Kuwahara-san: The Security CoE Group is responsible for the security of cloud environments such as AWS, Azure, and Google Cloud. We set up guardrails to ensure safe cloud usage, monitor security risks, and provide support against potential threats. HOKA-san: Next, could you tell us more about the Security & Privacy Study Session? What kind of study session is it? Kuwahara-san: We started this study session to spread security-related knowledge among employees. Last year, we created detailed security guidelines, and to ensure awareness and adoption, we began hosting these study sessions. Morino-san: Our goal is to establish Security by Design and Privacy by Design as standard considerations in all projects. Kasai-san: Ultimately, we want engineers to be able to focus on development, while we support them with privacy and security risk management. HOKA-san: How has the response been so far? Kuwahara-san: So far, we have hosted four sessions, and the response has been very positive. Many attendees have shared that they found the sessions informative, and we’ve also received valuable feedback for improvements. Morino-san: Some participants have mentioned that our explanations have become clearer, and we continue to refine our approach. HOKA-san: What changes do you hope to see at KTC through these study sessions? Morino-san: We aim to have security and privacy features implemented as standard practices. Kuwahara-san: Ideally, security should become so natural and ingrained that our roles become unnecessary. Kasai-san: I want to create an environment where engineers can focus on development with confidence, knowing that security is taken care of. HOKA-san: Finally, what inspired each of you to pursue this field? Morino-san: I started in software development and gradually developed a deep interest in security. Kasai-san: Initially, I wasn't particularly interested in security or privacy, but I became fascinated by governance and its impact. Kuwahara-san: I began my career as an infrastructure engineer, but after experiencing a security breach on a mail server I was managing, I shifted my focus to security. HOKA-san: Thank you all for your time today. We hope the Security and Privacy Study Session continues to be a valuable resource for everyone at KTC. Thank you everyone. This time, we provided details about the Security and Privacy Study Session, the background to its operation, and future prospects. Please look forward to the next study session!
こんにちは!KINTOテクノロジーズで Prism Japan という観光メディア(Web/iOS/Android)を運営している喜多です。 2024年11月からPrism JapanのSNS運用を担当しています。従来の広告配信に留まらず、SNSを用いることでサービスの認知拡大と新たな集客方法として確立することを目的としていました。 ここではその約4ヶ月間の振り返りをしてみたいと思います! できたこと まずはSNS運用をしていく上で「これはできたな」ということからまとめていきます。 ショート動画の作成 TiktokおよびInstagram中心に、ショート動画は今のSNS環境においてキーとなる存在です。各媒体のアルゴリズムでは、投稿されたショート動画をフォロワー外のユーザーにも届け、その反応次第でさらに伸びるかどうかが決まります。 SNSを用いた「認知拡大」という点においては必須科目だったため、類似したカテゴリにおいてバズっている閲覧数の多い動画を真似するところからスタートしました。 そのイメージを元に動画編集ツールを触りながらスキルを習得しました。主に利用したツールは下記です。 ツール名 特徴 Canva 基本的に無料で利用可能。画像、動画の編集が自由自在に可能であり、共同編集者とのシェアもしやすく管理が楽である。過去一度だけ作業中にサーバーが落ちてやり直しになった。 Adobe Express 基本的に無料で利用可能。Canvaと大きくは変わらないが、サーバーが安定していて作業途中で落ちることもなかったため、個人的にはこちらが好み。 Capcut 基本的に無料で利用可能。動画編集機能がメイン。文字の読み上げ機能や画像の3D化、SNSで「よく見る」あのフォントが利用できるなど、SNS向けに特化した機能が多い。 ※無料版に付く「透かし」はInstagramにおいては評価されないため注意。 使う人や目的によって若干好みや使い方が分かれる部分もありますが、基本的にはどのツールを使っても問題はなく、無料の範囲でも充分なクオリティの動画が作成できると思います。 インプレッションの獲得 各媒体共通して多くのインプレッションを獲得することができました。 ここでは運用を本格的に開始した2024年11月1日~2025年3月17日(執筆時点)の期間で前後比較してみます。 X インプレッション数は293%増の62,500件となりました。 Tiktok Tiktokは初めての運用だったため、比較対象がないのですが、全体を通して44万回再生されています。 他のSNSと比較すると、後から再生数がじわじわと伸びてくるような特徴がありました。 Instagram リーチ数が90%増の5万件となりました。 ※閲覧数、インタラクションともに、2024年8月以前のデータが集計不可であったため正しい比較はできませんでした。 Tiktok,Instagramは特に顕著なのですが、年末年始のタイミングで山がグッと発生しています。 多くの人がお休みであったタイミングと初詣シーズンにぴったりの神社を紹介したことが重なって伸びたのだと考えています。 本格的なSNS運用前後で比較すると、投稿回数の増加やそもそも媒体を運用していなかったことも要因として挙げられますが、動画の形式や季節、話題性といったトレンドを押さえたこともあって、大幅な閲覧数の増加が見込めました。 これらにより一定の「認知拡大」に貢献できたのではないかと思っております。 ユーザーとのコミュニケーション 殊にXにおいては新たなユーザーの発掘およびコミュニケーションを取ることができたと思います。 従来の運用では、こちらからフォローを働きかけたりリプライするなどのアクションを行なっていませんでした。 しかし、Xはエンゲージメント重視のアルゴリズムが採用されているため、積極的に観光系に興味を持つユーザーへアプローチしました。 その結果、とあるユーザーさんが実際にアプリのインストールだけでなく、掲載しているスポットへお出かけレビューまで投稿してくださったことが判明しました! 何より企画のアイディアなどもいただけて大変参考になりました。プロダクトやサービスを持っている方は積極的に動くことで結果的にサービス利用やフィードバックがもらえることもあるのでおすすめです。 できなかったこと 続いてできなかったことを振り返りたいと思います。 アプリのインストールやWebサイトの流入には至らなかった 上記に記載したように、「閲覧数」という観点においては増加するなどの結果であった反面、アプリのインストールやWebサイトへの流入は見込めない結果となりました。 下記では各媒体の結果を分析したいと思います。評価期間はSNS運用を本格的に開始した2024年11月1日~2025年3月17日とします。 以下はAppsFlyerで計測した期間中のアプリのインストール数になります。 ※具体的なインストール数の公開は控えさせていただきます。 Tiktok × アプリインストール数 まずはTiktokの各指標とアプリのインストール数に相関関係がないか調査しました。 視聴数 上記で既出の画像となりますが、視聴数とインストール数を照らし合わせてみました。 ぱっと見のグラフの比較でお分かりかもしれませんが、相関関係はないと考えています。 例えば、視聴数が盛り上がっている2025年1月2日のインストール数に変化がありませんでした。 反対にインストール数が盛り上がっている2025年1月29日の視聴数には変化がありません。 このことからもTiktokの視聴数とインストール数は相関関係にないと結論づけています。 プロフィールの表示回数 続いてはプロフィールの表示回数です。プロフィールに来てくれている時点で一定の興味を持ってくれているユーザーであると考えていました。 嬉しいことに増加傾向にあるものの、現状ではインストール数に大きな影響を与えているとは言えないと思います。 いいね いいねの付いた数はどうでしょうか。ぱっと見は視聴数と同じような推移だと思います。 こちらも同様にいいねが最も付いた2025年1月2日のインストール数には変化がなく、2025年1月29日のインストール数の増加にも寄与しているとは言えませんでした。 コメント コメント数は全体通して81件であり、閲覧数を考慮すると少ない結果となりました。日によっては0件と連動しています。 2025年2月17日のコメント数は4件とやや多めですが、インストール数に変化はなかったです。 シェア シェアも視聴数、いいね数と似たような推移となりました。 いいねやシェアが多い動画は評価されて「おすすめ」に表示され、結果的に視聴数が伸びるというアルゴリズムを身を持って実感しました。 Instagram × アプリインストール数 続いてはInstagramです。 閲覧数 こちらも同様に年末年始にかけて大きく山ができています。 そのタイミングでは上記の通りインストール数に影響はありません。 一方で、複数日においてはやや相関関係にありました。全体を通して見ると相関関係は強くはないのですが、Tiktokと比較すると少しだけ関係がありそうです。 また、リーチ数(いわゆるユニーク数)も上記と同様の傾向が見られたためここでは割愛します。 インタラクション 最後にインタラクションです。Instagramにおいてはいいねや保存、コメント、シェアがこちらに該当します。 こちらも同様にインストールとの相関は見られませんでした。 まとめ 全体を通して、インストール数で見ると相関関係は見られず、SNSによって集客ができた、とは言えない状況だとわかりました。 ただ、あくまで相関関係を見ただけなので、仮に視聴者が時間をおいてインストールした可能性もあるとは思いますが、正確な評価は正直なところ難しいと考えています。 Webサイトへの流入 SEO以外の経路としてもSNSの集客が一定貢献することを想定していました。実際のところ、Instagramのストーリー機能およびXのポストから流入を獲得できましたが、総数で見ても約300件とわずかな送客しかできませんでした。 これはXのフォロワー数が低いことによる拡散力の頼りなさ、Instagramのストーリー閲覧者を確保できなかったことが主な要因だと考えています。 インプレッション数は伸びたが、大幅なフォロワー数の増加は見込めなかった X:425フォロワー Xにおいてはフォロー数やいいねなどのアクションがまだまだ足りていませんでした。 他媒体との兼ね合いもあって、長い期間をかけてゆっくりと活動していたことが原因です。 早期のうちに短期間でフォロー、いいねを行うことが好ましいかと思います。過度なフォローはシャドウバンや凍結にもつながるのでご注意ください。 Tiktok:155フォロワー 視聴数の割に全くフォロワーがつきませんでした。理由は大きく二つかと思います。 一つは汎用的な情報を流していたこと。このアカウントをフォローしないと見逃してしまう情報だ、というオリジナリティや独創性に欠けていたと思います。 二つ目はターゲットが曖昧であったこと。Prism Japanの特性上、旅行したいと考える人という幅広い層がターゲットになるかと思います。 その上で、Tiktokではどういう層(年代、性別)にするか曖昧であったため、結局間口が広いまま多数の人に情報を届けていました。 Instagram:6667フォロワー 一見、多い印象を受けるのですが、広告の配信を停止して以降は下降傾向にありました。 広告配信して獲得したフォロワーはエンゲージメントが低い傾向にあるのが一般的とも言われていることに加え、広告配信停止後の投稿は以前とガラリとニュアンスを変えていたため余計にユーザー離れが加速したように思います。 各SNSの特徴と改善ポイント ここでは実際に運用して感じた各媒体の強みと、投稿内容の分析をしていきます。 TikTok:フォロワー数が多くなくても内容次第でバズることができる 最も伸びた投稿: 東京で女子旅するのにぴったりの穴場スポット3選! 「東京で女子旅する人」というかなり狭めたターゲットを勝手に設定して投稿しました。 内容としてはアプリの実際の画面上で穴場のスポットを探すという実際のフローを踏んでいます。 扱っているスポットはまさに穴場で、「海外風、某アニメ、縁結び」をテーマに取り上げました。 結果として、94%の人が検索から流入し、「東京 遊ぶ場所 女子2人」「東京観光」といったクエリで検索した結果流入してくれました。 ターゲットを狭め、穴場として実際にリアルなスポットを取り上げられたことが要因の一つかと考えています。 最も伸びなかった投稿: 日比谷のイルミネーションが綺麗すぎた! 伸びなかった動画は日比谷のイルミネーションです。こちらは実際に現地に足を運んで撮影し、イルミネーションの鮮やかや雰囲気を伝えたかったのがポイントです。 また冒頭部分はBGMとムービーが連動していて興味を引くような作り方をしたつもりでしたが、全く視聴されませんでした。 「クリスマス」「イルミネーション」といったハイクオリティな動画を提供する競合がひしめくキーワード帯に突撃してしまったことが原因ではないかと考えています。 まとめ 上記を比較すると、伸びた動画はターゲットが明確かつ内容がそのターゲットにとって有用であったことが挙げられるかと思います。 結果、平均視聴数やフル視聴率といった各指標も高く、視聴数を大幅に上げられました。 X:ユーザーとのコミュニケーションが強み 最も伸びた投稿: 神社仏閣×紅葉のおすすめ4選 紅葉と神社仏閣のコラボが見どころのスポットをまとめて紹介しました。各スポットの位置情報や見どころポイントとそれに付随した画像を載せています。 最も伸びなかった投稿: 今日はクリスマスですね! クリスマスに盛り上がっていた #MerryChristmas のタグとともに後述する生成AIを利用した動画を添えて投稿しました。 目的としてはアカウントの「人間味」であったのとタグで若干のインプレッションを獲得したいところにありましたが、結果として盛り上がりに欠ける投稿となりました。 まとめ Xにおいては他の伸びた投稿を見ても時季やトレンドに関する投稿が伸びる傾向にありました。 そして情報として有用か(まとまっているか、自分にとって価値があるか)が重要だと思いました。 Instagram:ショート動画を用いてフォロワー外にリーチしやすくなった 最も伸びた投稿: 【24~25シーズン】都内から行ける!おすすめのスキー場4選 冬のスキーシーズン開始に伴って関東から行けるスキー場を画像で紹介しました。 各スキー場の開場期間や時間、料金情報を記載しました。その結果、「保存」が10件と他投稿と比べると多くなりました。 よって、55.4%のフォロワー外ユーザーへもリーチできています。私の感覚では、「リール動画」は保存数あまり関係なく半数はフォロワー外のユーザーへリーチできる一方、画像投稿は9割がフォロー内に留まる印象でした。 振り返るとユーザーが「保存」したくなる投稿が何よりも重要であると考えています。 最も伸びなかった投稿: AIがあなたを診断! こちらは上記とは打って変わってアプリの機能「気分で検索」をリール動画としてアピールしたものです。 実際のアプリ画面を録画し、手順通り進めることでユーザーが使うシーンを想起してもらうことを意図していましたが、結果としては最も再生されない動画でした。 ターゲットが不明瞭で誰にも刺さらない動画であったことと、ユーザーの便益が伝わりにくいものであったのではないかと思います。 まとめ リール動画、画像投稿によってフォロワー外へのリーチのしやすさ、に違いがあるものの、得てして「保存」されることで投稿が伸びるかどうかが決まっている印象を受けました。 有益な情報であることはもちろん、保存を促したり、定期的にその分析を行うことが大事です。 全体のまとめと今後の方向性 全体を通して、SNSが強く集客に貢献しているとは言えないことが分かりました。一方で必ずしも貢献していないとも断言はできないため、引き続き振り返りを経て改善したいと思います。 投稿において特に重要なことは下記2点です。 ターゲットを明確にすること 視聴者にとって有益な情報であること 当たり前と言えば当たり前かもしれないですが、この2点は媒体問わず共通して伸びる動画にとって必要であることだと実感しました。 また、Tiktokの内容をInstagramへもそのまま流用していましたが、それだと媒体でユーザー層が異なるため、反応が得にくい状態にありました。 とはいえ、媒体別に投稿することが望ましいのですが、リソースにも限界があるため、有意義な取り組みになっていませんでした。媒体ごとに特性やユーザー層が異なるため、目的に合わせて何がベストかを見極めることをおすすめします。 参考文献 SNSマーケティング7つの鉄則
This article is the entry for day 16 in the KINTO Technologies Advent Calendar 2024 🎅🎄 1. Introduction Hello. I'm Okita from the Mobile App Development Group. It's December! The end of the year is here! Looking back, many events took place this year. One of the things that strongly stands out is: My participation in Developers Summit 2024 KANSAI as a member of Osaka Tech Lab. So, this time I'll be sharing the following: My participation and presentation at Developers Summit 2024 KANSAI The latest news from Osaka Tech Lab 2. How I Felt Before the Presentation As a sponsor, our company had the opportunity to set up a booth and give a presentation at the summit. *For more details, please check out this article^^ Reflecting on Developers Summit 2024 KANSAI We want to recruit people who will join us in envisioning the future! To that end, we want people to get to know KINTO Technologies Corporation and learn about Osaka Tech Lab! With this goal in mind, our company applied to become a sponsor. This is the first attempt of its kind for Osaka Tech Lab. I was entrusted with the important role of delivering a presentation as a sponsor on behalf of our company. At first, I was worried... I'd never given a speech before, so I wondered if I could do it... But it was a valuable opportunity so I decided to give it my best shot! 3. The Day of My Presentation - a Moment of Tension and Emotion Amidst all this, the day arrived. In fact, I spent the morning of the day refining my presentation and rehearsed until the very last minute. And, overwhelmed with nerves, I arrived at the venue early and spent some time wandering around. Meanwhile, the clock kept ticking, and before I knew it, it was my turn. The theme of my presentation was " Challenges " . "There's no point in panicking now." "Let's stay calm and speak slowly and carefully so that our message can reach as many people as possible." With that in mind, I began to speak slowly. As my speech neared its end, I looked up and saw people wearing black T-shirts. To my surprise, they were colleagues from our company, warmly watching my presentation in their KINTO Technologies T-shirts. I was deeply touched. While the audience of other companies participated in bright-colored T-shirts like orange, yellow, and blue, my coworkers wore the less noticeable black. That was so like the Osaka Tech Lab, which made me chuckle and relax. They quietly watched over me as I struggled to prepare for my presentation. Team members from other offices in our company also supported me, someone with no prior presentation experience, by helping me compile my profile and create the presentation content. A few days before the presentation, the managers and team leaders of the Mobile App Development Group suddenly found themselves scrambling to assist me, as I had reached out for their help. That's right; I just happened to be the one presenting, but the content was the result of the efforts and ideas of many others. Thanks to everyone, 174 people attended my presentation. I would like to take this opportunity to express my gratitude to the event management staff, our company members who supported me, and everyone who participated in the session. 4. How I Felt After the Presentation I would like to share my thoughts as I reflect on my experience after the presentation. I began preparing more than a month before, but I found myself scrambling until the last minute. -> It turned out to be a great experience, but I spent too much time overthinking, so I now believe there were more efficient ways to approach it. As it was my first presentation, I didn’t know how to prepare for it. -> In hindsight, I should have first clarified the purpose, goals, and key message of my speech before preparing the materials. During my rehearsals, no matter how many times I practiced, my presentation always finished in just 20 minutes. But on the day of the presentation, I was able to speak for exactly 30 minutes. I was holding the remote upside down, so even though I pressed the button, the next slide wouldn't show, and I got nervous right from the start.  I prepared the materials in PowerPoint and then exported them as a PDF but the font I had carefully chosen was not reflected in the PDF. It was disappointing, but it became a valuable lesson. As mentioned above, the preparations were very challenging, but in the end, I was able to give a presentation that I have no regrets about. I also made new connections with other companies that participated as sponsors, as well as people who contacted me through 'Ask the Speaker'. And to my surprise, the editorial department of CodeZine kindly offered to release a session report about me. What is the appeal of working at Osaka Tech Lab for PM and mobile app engineers challenging themselves in the Toyota Group? (in Japanese) Truly, I was deeply moved. Summary of my presentation - Learning and Growth This is a brief summary of my presentation. ■ Good things and things I've grown from By delivering my presentation at the event, I was able to organize and verbalize my daily efforts. As a result, the next steps for the project I belong to became clear. I was also able to level up myself.   -> I was able to experience the significance of "input and output" and "verbalization." ■ Improvements It was a bit heavy to have a 30-minute presentation slot for the first time in my life -> It is recommended to experience a presentation at an outside event in the 5 to 10-minute presentation slot first.♪ While I was able to convey my thoughts, I regret that my presentation lacked concrete examples that went a step further. -> I would like to aim for a presentation from which the participants can take away at least one lesson. I should have clarified the main points of my presentation before preparing my presentation materials and story. -> If you create the core of your presentation first, it will be easier to create a consistent story and you will be able to prepare more efficiently. (Sample) Key points for a presentation ■ Purpose of presentation ■ Goal (what to achieve in the presentation) ■ Messages to convey 6. Recent developments at Osaka Tech Lab Well, well. Time flies. It's already December. After the Developers Summit 2024 KANSAI, new developments have emerged at Osaka Tech Lab. In other words, taking on a challenge of organizing external events. The first of our challenges was: "Kansai Front-End Year-End Party 2024 HACK.BAR × KINTO Technologies" In the future, I hope to organize the Mobile App Development Group’s events. It would be fun to collaborate and hold events with other companies ♪ The list of things I want to try is overflowing, and it's becoming a bit of a problem, lol. I want to take one step at a time (or maybe half a step at a time, lol). 7. Conclusion How was it? I hope you found this article interesting. Join us in shaping the future of Osaka Tech Lab as we step into an exciting new era! We look forward to your application! KINTO Technologies Corporation Recruitment Top Page Wantedly
こんにちは!KINTOテクノロジーズで Prism Japan という観光メディア(Web/iOS/Android)を運営している喜多です。 2024年11月からPrism JapanのSNS運用を担当しています。従来の広告配信に留まらず、SNSを用いることでサービスの認知拡大と新たな集客方法として確立することを目的としていました。 ここではその約4ヶ月間の振り返りをしてみたいと思います! できたこと まずはSNS運用をしていく上で「これはできたな」ということからまとめていきます。 ショート動画の作成 TiktokおよびInstagram中心に、ショート動画は今のSNS環境においてキーとなる存在です。各媒体のアルゴリズムでは、投稿されたショート動画をフォロワー外のユーザーにも届け、その反応次第でさらに伸びるかどうかが決まります。 SNSを用いた「認知拡大」という点においては必須科目だったため、類似したカテゴリにおいてバズっている閲覧数の多い動画を真似するところからスタートしました。 そのイメージを元に動画編集ツールを触りながらスキルを習得しました。主に利用したツールは下記です。 ツール名 特徴 Canva 基本的に無料で利用可能。画像、動画の編集が自由自在に可能であり、共同編集者とのシェアもしやすく管理が楽である。過去一度だけ作業中にサーバーが落ちてやり直しになった。 Adobe Express 基本的に無料で利用可能。Canvaと大きくは変わらないが、サーバーが安定していて作業途中で落ちることもなかったため、個人的にはこちらが好み。 Capcut 基本的に無料で利用可能。動画編集機能がメイン。文字の読み上げ機能や画像の3D化、SNSで「よく見る」あのフォントが利用できるなど、SNS向けに特化した機能が多い。 ※無料版に付く「透かし」はInstagramにおいては評価されないため注意。 使う人や目的によって若干好みや使い方が分かれる部分もありますが、基本的にはどのツールを使っても問題はなく、無料の範囲でも充分なクオリティの動画が作成できると思います。 インプレッションの獲得 各媒体共通して多くのインプレッションを獲得することができました。 ここでは運用を本格的に開始した2024年11月1日~2025年3月17日(執筆時点)の期間で前後比較してみます。 X インプレッション数は293%増の62,500件となりました。 Tiktok Tiktokは初めての運用だったため、比較対象がないのですが、全体を通して44万回再生されています。 他のSNSと比較すると、後から再生数がじわじわと伸びてくるような特徴がありました。 Instagram リーチ数が90%増の5万件となりました。 ※閲覧数、インタラクションともに、2024年8月以前のデータが集計不可であったため正しい比較はできませんでした。 Tiktok,Instagramは特に顕著なのですが、年末年始のタイミングで山がグッと発生しています。 多くの人がお休みであったタイミングと初詣シーズンにぴったりの神社を紹介したことが重なって伸びたのだと考えています。 本格的なSNS運用前後で比較すると、投稿回数の増加やそもそも媒体を運用していなかったことも要因として挙げられますが、動画の形式や季節、話題性といったトレンドを押さえたこともあって、大幅な閲覧数の増加が見込めました。 これらにより一定の「認知拡大」に貢献できたのではないかと思っております。 ユーザーとのコミュニケーション 殊にXにおいては新たなユーザーの発掘およびコミュニケーションを取ることができたと思います。 従来の運用では、こちらからフォローを働きかけたりリプライするなどのアクションを行なっていませんでした。 しかし、Xはエンゲージメント重視のアルゴリズムが採用されているため、積極的に観光系に興味を持つユーザーへアプローチしました。 その結果、とあるユーザーさんが実際にアプリのインストールだけでなく、掲載しているスポットへお出かけレビューまで投稿してくださったことが判明しました! 何より企画のアイディアなどもいただけて大変参考になりました。プロダクトやサービスを持っている方は積極的に動くことで結果的にサービス利用やフィードバックがもらえることもあるのでおすすめです。 できなかったこと 続いてできなかったことを振り返りたいと思います。 アプリのインストールやWebサイトの流入には至らなかった 上記に記載したように、「閲覧数」という観点においては増加するなどの結果であった反面、アプリのインストールやWebサイトへの流入は見込めない結果となりました。 下記では各媒体の結果を分析したいと思います。評価期間はSNS運用を本格的に開始した2024年11月1日~2025年3月17日とします。 以下はAppsFlyerで計測した期間中のアプリのインストール数になります。 ※具体的なインストール数の公開は控えさせていただきます。 Tiktok × アプリインストール数 まずはTiktokの各指標とアプリのインストール数に相関関係がないか調査しました。 視聴数 上記で既出の画像となりますが、視聴数とインストール数を照らし合わせてみました。 ぱっと見のグラフの比較でお分かりかもしれませんが、相関関係はないと考えています。 例えば、視聴数が盛り上がっている2025年1月2日のインストール数に変化がありませんでした。 反対にインストール数が盛り上がっている2025年1月29日の視聴数には変化がありません。 このことからもTiktokの視聴数とインストール数は相関関係にないと結論づけています。 プロフィールの表示回数 続いてはプロフィールの表示回数です。プロフィールに来てくれている時点で一定の興味を持ってくれているユーザーであると考えていました。 嬉しいことに増加傾向にあるものの、現状ではインストール数に大きな影響を与えているとは言えないと思います。 いいね いいねの付いた数はどうでしょうか。ぱっと見は視聴数と同じような推移だと思います。 こちらも同様にいいねが最も付いた2025年1月2日のインストール数には変化がなく、2025年1月29日のインストール数の増加にも寄与しているとは言えませんでした。 コメント コメント数は全体通して81件であり、閲覧数を考慮すると少ない結果となりました。日によっては0件と連動しています。 2025年2月17日のコメント数は4件とやや多めですが、インストール数に変化はなかったです。 シェア シェアも視聴数、いいね数と似たような推移となりました。 いいねやシェアが多い動画は評価されて「おすすめ」に表示され、結果的に視聴数が伸びるというアルゴリズムを身を持って実感しました。 Instagram × アプリインストール数 続いてはInstagramです。 閲覧数 こちらも同様に年末年始にかけて大きく山ができています。 そのタイミングでは上記の通りインストール数に影響はありません。 一方で、複数日においてはやや相関関係にありました。全体を通して見ると相関関係は強くはないのですが、Tiktokと比較すると少しだけ関係がありそうです。 また、リーチ数(いわゆるユニーク数)も上記と同様の傾向が見られたためここでは割愛します。 インタラクション 最後にインタラクションです。Instagramにおいてはいいねや保存、コメント、シェアがこちらに該当します。 こちらも同様にインストールとの相関は見られませんでした。 まとめ 全体を通して、インストール数で見ると相関関係は見られず、SNSによって集客ができた、とは言えない状況だとわかりました。 ただ、あくまで相関関係を見ただけなので、仮に視聴者が時間をおいてインストールした可能性もあるとは思いますが、正確な評価は正直なところ難しいと考えています。 Webサイトへの流入 SEO以外の経路としてもSNSの集客が一定貢献することを想定していました。実際のところ、Instagramのストーリー機能およびXのポストから流入を獲得できましたが、総数で見ても約300件とわずかな送客しかできませんでした。 これはXのフォロワー数が低いことによる拡散力の頼りなさ、Instagramのストーリー閲覧者を確保できなかったことが主な要因だと考えています。 インプレッション数は伸びたが、大幅なフォロワー数の増加は見込めなかった X:425フォロワー Xにおいてはフォロー数やいいねなどのアクションがまだまだ足りていませんでした。 他媒体との兼ね合いもあって、長い期間をかけてゆっくりと活動していたことが原因です。 早期のうちに短期間でフォロー、いいねを行うことが好ましいかと思います。過度なフォローはシャドウバンや凍結にもつながるのでご注意ください。 Tiktok:155フォロワー 視聴数の割に全くフォロワーがつきませんでした。理由は大きく二つかと思います。 一つは汎用的な情報を流していたこと。このアカウントをフォローしないと見逃してしまう情報だ、というオリジナリティや独創性に欠けていたと思います。 二つ目はターゲットが曖昧であったこと。Prism Japanの特性上、旅行したいと考える人という幅広い層がターゲットになるかと思います。 その上で、Tiktokではどういう層(年代、性別)にするか曖昧であったため、結局間口が広いまま多数の人に情報を届けていました。 Instagram:6667フォロワー 一見、多い印象を受けるのですが、広告の配信を停止して以降は下降傾向にありました。 広告配信して獲得したフォロワーはエンゲージメントが低い傾向にあるのが一般的とも言われていることに加え、広告配信停止後の投稿は以前とガラリとニュアンスを変えていたため余計にユーザー離れが加速したように思います。 各SNSの特徴と改善ポイント ここでは実際に運用して感じた各媒体の強みと、投稿内容の分析をしていきます。 TikTok:フォロワー数が多くなくても内容次第でバズることができる 最も伸びた投稿: 東京で女子旅するのにぴったりの穴場スポット3選! 「東京で女子旅する人」というかなり狭めたターゲットを勝手に設定して投稿しました。 内容としてはアプリの実際の画面上で穴場のスポットを探すという実際のフローを踏んでいます。 扱っているスポットはまさに穴場で、「海外風、某アニメ、縁結び」をテーマに取り上げました。 結果として、94%の人が検索から流入し、「東京 遊ぶ場所 女子2人」「東京観光」といったクエリで検索した結果流入してくれました。 ターゲットを狭め、穴場として実際にリアルなスポットを取り上げられたことが要因の一つかと考えています。 最も伸びなかった投稿: 日比谷のイルミネーションが綺麗すぎた! 伸びなかった動画は日比谷のイルミネーションです。こちらは実際に現地に足を運んで撮影し、イルミネーションの鮮やかや雰囲気を伝えたかったのがポイントです。 また冒頭部分はBGMとムービーが連動していて興味を引くような作り方をしたつもりでしたが、全く視聴されませんでした。 「クリスマス」「イルミネーション」といったハイクオリティな動画を提供する競合がひしめくキーワード帯に突撃してしまったことが原因ではないかと考えています。 まとめ 上記を比較すると、伸びた動画はターゲットが明確かつ内容がそのターゲットにとって有用であったことが挙げられるかと思います。 結果、平均視聴数やフル視聴率といった各指標も高く、視聴数を大幅に上げられました。 X:ユーザーとのコミュニケーションが強み 最も伸びた投稿: 神社仏閣×紅葉のおすすめ4選 紅葉と神社仏閣のコラボが見どころのスポットをまとめて紹介しました。各スポットの位置情報や見どころポイントとそれに付随した画像を載せています。 最も伸びなかった投稿: 今日はクリスマスですね! クリスマスに盛り上がっていた #MerryChristmas のタグとともに後述する生成AIを利用した動画を添えて投稿しました。 目的としてはアカウントの「人間味」であったのとタグで若干のインプレッションを獲得したいところにありましたが、結果として盛り上がりに欠ける投稿となりました。 まとめ Xにおいては他の伸びた投稿を見ても時季やトレンドに関する投稿が伸びる傾向にありました。 そして情報として有用か(まとまっているか、自分にとって価値があるか)が重要だと思いました。 Instagram:ショート動画を用いてフォロワー外にリーチしやすくなった 最も伸びた投稿: 【24~25シーズン】都内から行ける!おすすめのスキー場4選 冬のスキーシーズン開始に伴って関東から行けるスキー場を画像で紹介しました。 各スキー場の開場期間や時間、料金情報を記載しました。その結果、「保存」が10件と他投稿と比べると多くなりました。 よって、55.4%のフォロワー外ユーザーへもリーチできています。私の感覚では、「リール動画」は保存数あまり関係なく半数はフォロワー外のユーザーへリーチできる一方、画像投稿は9割がフォロー内に留まる印象でした。 振り返るとユーザーが「保存」したくなる投稿が何よりも重要であると考えています。 最も伸びなかった投稿: AIがあなたを診断! こちらは上記とは打って変わってアプリの機能「気分で検索」をリール動画としてアピールしたものです。 実際のアプリ画面を録画し、手順通り進めることでユーザーが使うシーンを想起してもらうことを意図していましたが、結果としては最も再生されない動画でした。 ターゲットが不明瞭で誰にも刺さらない動画であったことと、ユーザーの便益が伝わりにくいものであったのではないかと思います。 まとめ リール動画、画像投稿によってフォロワー外へのリーチのしやすさ、に違いがあるものの、得てして「保存」されることで投稿が伸びるかどうかが決まっている印象を受けました。 有益な情報であることはもちろん、保存を促したり、定期的にその分析を行うことが大事です。 全体のまとめと今後の方向性 全体を通して、SNSが強く集客に貢献しているとは言えないことが分かりました。一方で必ずしも貢献していないとも断言はできないため、引き続き振り返りを経て改善したいと思います。 投稿において特に重要なことは下記2点です。 ターゲットを明確にすること 視聴者にとって有益な情報であること 当たり前と言えば当たり前かもしれないですが、この2点は媒体問わず共通して伸びる動画にとって必要であることだと実感しました。 また、Tiktokの内容をInstagramへもそのまま流用していましたが、それだと媒体でユーザー層が異なるため、反応が得にくい状態にありました。 とはいえ、媒体別に投稿することが望ましいのですが、リソースにも限界があるため、有意義な取り組みになっていませんでした。媒体ごとに特性やユーザー層が異なるため、目的に合わせて何がベストかを見極めることをおすすめします。 参考文献 SNSマーケティング7つの鉄則
みなさん、こんにちは。KINTOテクノロジーズのモバイル開発グループのマーティンです!このガイドでは、TFLite(TensorFlow Lite)モデルをゼロから構築する方法を簡単に説明します。それでは、早速始めましょう。 この記事は KINTOテクノロジーズアドベントカレンダー2024の9日目の記事です🎅🎄 準備 データセットを準備する方法は基本的に2通りあります。1つは、ローカルでアノテーションプロセスを行う方法、もう1つは、オンラインでデータセットにアノテーションを行い、チームメンバーと初期の工数をより効率的に共有しながら進める方法です。このガイドでは、Roboflow (1) の使用に重点を置いています。 Roboflow のモデルエクスポート機能を使用すると、トレーニング済みのモデルをさまざまな形式でエクスポートできるため、独自のアプリケーションに簡単に展開したり、さらに微調整したりすることができます。今回の場合、TFlite モデルをトレーニングしたいので、下の画像に示すように TFRecord 形式にエクスポートする必要があります。 ただし、Roboflowのようなサードパーティのオンラインアノテーションツールを用いずに、ローカルで画像にアノテーションを付けたい場合は、無料のPythonライブラリlabelImgを試してみてください: https://github.com/HumanSignal/labelImg 一般的に、ローカルでもオンラインでも、まず画像のデータセットを収集し、それらにラベルを付けて、対応する境界ボックス分類メタデータ (xml) ファイルを取得する必要があります。(この場合は、VOC [Visual Object Classes] Pascal メタデータを作成) Pascal VOCの詳細情報は、こちらで確認できます: https://roboflow.com/formats/pascal-voc-xml Google Cloud Platform の標準ランタイムインスタンスを作成したら、それを Colab ノートブックに接続する必要があります。基本的に、Google Colab は、組織内のデータサイエンスチームと機械学習チームに、安全でスケーラブルな共同作業プラットフォームを提供します。それが完了したら、まずTensorFlow(テンソルフロー)を動作させるために必要なライブラリをインポートする必要があります(ステップ1) GCP標準インスタンスの作成: Colab Enterprise( https://cloud.google.com/colab/docs ) ノートブックの作成: Google Cloud バケットを接続する(ステップ 4) 実行 TensorFlow オブジェクト検出 API をインストールします(このガイドのステップ 5) トレーニングに必要な TFRecord ファイルを生成します。(これには、csv ファイルを生成するための generate_tfrecord.pyスクリプトが必要です) モデルパイプライン構成ファイルを編集し、事前にトレーニングされたモデルのチェックポイントをダウンロードします。 モデルのトレーニングと評価 モデルをエクスポートして、TFlite(TensorFlow Lite)形式に変換します。 展開 TFlite モデルを Android / iOS / IoT デバイスに展開します。 それでは、始めましょう Colab Enterprise ノートブック内で実行すべき手順を詳しく説明します: 1) ライブラリをインポートします !pip install tensorflow==2.13.0 import os import glob import xml.etree.ElementTree as ET import pandas as pd import tensorflow as tf print(tf.__version__) 2) GCPクラウドストレージバケットに* customTF2 、 training 、 data *フォルダを作成します(初回のみ必要) GCPクラウドストレージバケットに customTF2 という名前のフォルダを作成します。 customTF2 フォルダ内に training と data という2つのサブフォルダを作成します( training フォルダはトレーニング中にチェックポイントが保存される場所です) GCSバケット内のフォルダ構造の作成: 3) 以下を generate_tfrecord.py ファイルとしてダウンロードして保存し、バケットの CustomTF2 フォルダにアップロードします。(初回のみ必要) from __future__ import division from __future__ import print_function from __future__ import absolute_import import os import io import pandas as pd import tensorflow as tf import argparse from PIL import Image from tqdm import tqdm from object_detection.utils import dataset_util from collections import namedtuple, OrderedDict def __split(df, group): data = namedtuple('data', ['filename', 'object']) gb = df.groupby(group) return [data(filename, gb.get_group(x)) for filename, x in zip(gb.groups.keys(), gb.groups)] def create_tf_example(group, path, class_dict): with tf.io.gfile.GFile(os.path.join(path, '{}'.format(group.filename)), 'rb') as fid: encoded_jpg = fid.read() encoded_jpg_io = io.BytesIO(encoded_jpg) image = Image.open(encoded_jpg_io) 幅、高さ = 画像.サイズ filename = group.filename.encode('utf8') image_format = b'jpg' xmins = [] xmaxs = [] ymins = [] ymaxs = [] classes_text = [] classes = [] for index, row in group.object.iterrows(): if set(['xmin_rel', 'xmax_rel', 'ymin_rel', 'ymax_rel']).issubset(set(row.index)): xmin = row['xmin_rel'] xmax = row['xmax_rel'] ymin = row['ymin_rel'] ymax = row['ymax_rel'] elif set(['xmin', 'xmax', 'ymin', 'ymax']).issubset(set(row.index)): xmin = row['xmin'] / width xmax = row['xmax'] / width ymin = row['ymin'] / height ymax = row['ymax'] / height xmins.append(xmin) xmaxs.append(xmax) ymins.append(ymin) ymaxs.append(ymax) classes_text.append(str(row['class']).encode('utf8')) classes.append(class_dict[str(row['class'])]) tf_example = tf.train.Example(features=tf.train.Features( feature={ 'image/height': dataset_util.int64_feature(height), 'image/width': dataset_util.int64_feature(width), 'image/filename': dataset_util.bytes_feature(filename), 'image/source_id': dataset_util.bytes_feature(filename), 'image/encoded': dataset_util.bytes_feature(encoded_jpg), 'image/format': dataset_util.bytes_feature(image_format), 'image/object/bbox/xmin': dataset_util.float_list_feature(xmins), 'image/object/bbox/xmax': dataset_util.float_list_feature(xmaxs), 'image/object/bbox/ymin': dataset_util.float_list_feature(ymins), 'image/object/bbox/ymax': dataset_util.float_list_feature(ymaxs), 'image/object/class/text': dataset_util.bytes_list_feature(classes_text), 'image/object/class/label': dataset_util.int64_list_feature(classes), })) return tf_example def class_dict_from_pbtxt(pbtxt_path): # open file, strip \n, trim lines and keep only # lines beginning with id or display_name with open(pbtxt_path, 'r', encoding='utf-8-sig') as f: data = f.readlines() name_key = None if any('display_name:' in s for s in data): name_key = 'display_name:' elif any('name:' in s for s in data): name_key = 'name:' if name_key is None: raise ValueError( "label map does not have class names, provided by values with the 'display_name' or 'name' keys in the contents of the file" ) data = [l.rstrip('\n').strip() for l in data if 'id:' in l or name_key in l] ids = [int(l.replace('id:', '')) for l in data if l.startswith('id')] names = [ l.replace(name_key, '').replace('"', '').replace("'", '').strip() for l in data if l.startswith(name_key)] # id と display_names を1つの辞書に結合します class_dict = {} for i in range(len(ids)): class_dict[names[i]] = ids[i] return class_dict if __name__ == '__main__': parser = argparse.ArgumentParser( description='Create a TFRecord file for use with the TensorFlow Object Detection API.', formatter_class=argparse.RawDescriptionHelpFormatter) parser.add_argument('csv_input', metavar='csv_input', type=str, help='Path to the CSV input') parser.add_argument('pbtxt_input', metavar='pbtxt_input', type=str, help='Path to a pbtxt file containing class ids and display names') parser.add_argument('image_dir', metavar='image_dir', type=str, help='Path to the directory containing all images') parser.add_argument('output_path', metavar='output_path', type=str, help='Path to output TFRecord') args = parser.parse_args() class_dict = class_dict_from_pbtxt(args.pbtxt_input) writer = tf.compat.v1.python_io.TFRecordWriter(args.output_path) path = os.path.join(args.image_dir) examples = pd.read_csv(args.csv_input) grouped = __split(examples, 'filename') for group in tqdm(grouped, desc='groups'): tf_example = create_tf_example(group, path, class_dict) writer.write(tf_example.SerializeToString()) writer.close() output_path = os.path.join(os.getcwd(), args.output_path) print('Successfully created the TFRecords: {}'.format(output_path)) 4) GCSバケットをマウントし、GCSFUSEをインストールしてフォルダをリンクします。 from google.colab import auth auth.authenticate_user() !echo「deb https://packages.cloud.google.com/apt gcsfuse-bionic main」> /etc/apt/sources.list.d/gcsfuse.list !curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add - !apt -qq update !apt -qq install gcsfuse !gsutil ls -r gs://your-cloud-storage-bucket-name !mkdir customTF2 !gcsfuse --implicit-dirs your-cloud-storage-bucket-name customTF2 5) TensorFlow モデルのGitレポジトリをクローンし、TensroFlow オブジェクト検出APIをインストールします。 %cd /content # colab クラウドvmに tensorflowモデルをクローンする !git clone --q https://github.com/tensorflow/models.git # protosをコンパイルするために/models/research フォルダにナビゲートします。 %cd models/research # protosをコンパイルします。 !protoc object_detection/protos/*.proto --python_out=. # TensorFlowオブジェクト検出 API をインストールします。 !cp object_detection/packages/tf2/setup.py . !python -m pip install . 6) モデルビルダーをテストします (推奨) %cd /content/models/research # モデルビルダーのテスト !pip install 'tf-models-official >=2.5.1, <2.16.0' !python object_detection/builders/model_builder_tf2_test.py 7)事前にトレーニングされたモデルチェックポイントをダウンロードします(初めての場合のみ必要) 現在の作業ディレクトリは /content/customTF2/customTF2/data/ ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz を  データ  フォルダにダウンロードして解凍します。 その他の Tensorflow 2.x の検出チェックポイントのリストは こちら 。 %cd /content/customTF2/customTF2/data/ #事前トレーニング済みモデル ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz をダウンロードしてデータフォルダに入れて解凍します。 !wget http://download.tensorflow.org/models/object_detection/tf2/20200711/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz !tar -xzvf ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.tar.gz * 8)モデルパイプライン構成ファイルを入手して変更を加え、 データ フォルダに入れます(クラス番号の量を変更するたびに必要) * 現在の作業ディレクトリは /content/customTF2/customTF2/data/ ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config を /content/models/research/object_detection/configs/tf2 からダウンロードします。必要な変更を加え、 /content/customTF2/customTF2/data/ フォルダーにアップロードします。 または colab内 で /content/models/research/object_detection/configs/tf2 からの構成ファイルを編集し、編集済みの構成ファイルを /content/customTF2/customTF2/data フォルダにコピーします。 パイプライン構成ファイルは、前のステップでダウンロードしたモデルチェックポイントフォルダ内にもあります。 次の変更を行う必要があります。 num_classes を自分のクラス数に変更します。 test.record パス、 train.record パス、 labelmap パスを、これらのファイルを作成した場所のパスに変更します(トレーニング中は現在の作業ディレクトリに対する相対パスである必要があります) fine_tune_checkpoint を、ステップ 12 でダウンロードしたチェックポイントがあるディレクトリのパスに変更します。 fine_tune_checkpoint_type を、分類タイプに応じて数値 classification または detection に変更します。 batch_size を、使用する GPU の能力に応じて 8 の倍数に変更します(例:24、128、...、512) - 通常、標準的な colab enterprise インスタンスでは、小さなデータセットには 24、大きなデータセットには 32 が適しています。 num_steps を、検出器にトレーニングさせたいステップ数に変更します。 #編集した構成ファイルを configs/tf2 ディレクトリから GCP ストレージの data/ フォルダにコピーします。 !cp /content/models/research/object_detection/configs/tf2/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config /content/customTF2/customTF2/data 次のステップでは、公式の TensorBoard ツールを使用して、実行状況やグラフを可視化し、学習と分類の損失を時間経過とともに確認したいと思います。グラフの読み方やツールの使い方に関する詳細情報は、こちらをご覧ください: https://www.tensorflow.org/tensorboard/get_started#:~:text=TensorBoard%20is%20a%20tool%20for,during%20the%20machine%20learning%20workflow。 9) TensorBoardをロードします (推奨) # cload tensorboard %cd /content/customTF2/customTF2/training # !pip install tensorboard # tensorboard --inspect --logdir /content/customTF2/customTF2/training # !gcloud init # !gcloud auth application-default login %reload_ext tensorboard %tensorboard --logdir '/content/customTF2/customTF2/training' 10) モデルをトレーニングします colab vm の object_detection フォルダに移動します %cd /content/models/research/object_detection 10 (a) model_main_tf2.py を使用したトレーニング(推奨方法) ここで、 PIPELINE_CONFIG_PATH はパイプライン構成ファイルを指し、 MODEL_DIR はトレーニングのチェックポイントとイベントが書き込まれるディレクトリを指します。 最良の結果を得るためには、損失が0.1未満になった時点でトレーニングを停止してください。それが難しい場合は、損失がしばらくの間、顕著な変化を示さなくなるまでモデルをトレーニングしてください。理想的な損失は0.05未満です(モデルを過学習させずに、損失をできるだけ低く抑えるようにしてください)。モデルがすでに収束している場合(例えば、損失がそれ以上顕著に減少せず、減少に時間がかかる場合)、損失を下げようとしてトレーニングステップを高くしすぎないようにしてください。 !pip install tensorflow==2.13.0 以下のコマンドをcontent/models/research/object_detection ディレクトリから実行してください。 """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training --alsologtostderr 10 (b) Evaluation using model_main_tf2.py (Optional, just if you want more customization) これを並行して実行するには、別の Colab ノートブックを開き、上記のトレーニングコマンドと同時にこのコマンドを実行します(その際、gcpストレージをマウントし、TF gitリポジトリをクローンし、TF2オブジェクト検出 API をインストールするのを忘れないでください)。これにより、検証損失、mAP などが表示され、モデルのパフォーマンスがどのようになっているかをより良く把握できます。 ここで、 {CHECKPOINT_DIR} はトレーニングジョブによって生成されたチェックポイントが格納されているディレクトリを指します。評価イベントは {MODEL_DIR/eval} に書き込まれます。 以下のコマンドをcontent/models/research/object_detection ディレクトリから実行してください。 """ PIPELINE_CONFIG_PATH=path/to/pipeline.config MODEL_DIR=path to training checkpoints directory CHECKPOINT_DIR=${MODEL_DIR} NUM_TRAIN_STEPS=50000 SAMPLE_1_OF_N_EVAL_EXAMPLES=1 python model_main_tf2.py -- \ --model_dir=$MODEL_DIR --num_train_steps=$NUM_TRAIN_STEPS \ --checkpoint_dir=${CHECKPOINT_DIR} \ --sample_1_of_n_eval_examples=$SAMPLE_1_OF_N_EVAL_EXAMPLES \ --pipeline_config_path=$PIPELINE_CONFIG_PATH \ --alsologtostderr """ !python model_main_tf2.py --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --model_dir=/content/customTF2/customTF2/training/ --checkpoint_dir=/content/customTF2/customTF2/training/ --alsologtostderr モデルの再トレーニング (接続が切れた場合) 接続が切れたり、colab vmのセッションが失われた場合、トレーニングは中断した場所から再開できます。チェックポイントは training フォルダ内のクラウドストレージに保存されています。トレーニングを再開するには、 ステップ 1, 4, 5, 6, 9, 10 を実行してください。 トレーニングに必要なすべてのファイル(記録ファイル、編集済みpipeline configファイル、label_mapファイル、モデルチェックポイントフォルダなど)が揃っているため、これらを再作成する必要はありません。 model_main_tf2.pyスクリプトは、1000ステップごとにチェックポイントを保存します。 トレーニングは、最後に保存されたチェックポイントから自動的に再開されます。 ただし、最後のチェックポイントからトレーニングが再開されない場合は、パイプライン構成ファイルに1つの変更を加えることができます。 fine_tune_checkpoint を、最新のトレーニング済みチェックポイントが書き込まれている場所に変更し、最新のチェックポイントを指すように以下のように設定します。 fine_tune_checkpoint: "/content/customTF2/customTF2/training/ckpt-X" (where ckpt-X is the latest checkpoint) 11) トレーニング済みモデルをテストします 推論グラフをエクスポート 現在の作業ディレクトリは /content/models/research/object_detection検出です %cd /content/models/research/object_detection !pip install tensorflow==2.13.0 # #推論グラフをエクスポート !python exporter_main_v2.py --trained_checkpoint_dir=/content/customTF2/customTF2/training --pipeline_config_path=/content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --output_directory /content/customTF2/customTF2/data/inference_graph 訓練済みのオブジェクト検出モデルを画像でテストします(好みのテスト画像を提供して image_path を調整します) 現在の作業ディレクトリは /content/models/research/object_detection検出です %cd /content/models/research/object_detection # ラベルテキストの異なるフォントタイプ。(このステップはオプション) !wget https://www.freefontspro.com/d/14454/arial.zip !unzip arial.zip -d . %cd utils/ !sed-i「s/font = ImageFont.TrueType ('arial.ttf', 24) /font = ImageFont.TrueType ('arial.ttf', 50)/」visualization_utils.py %cd .. %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" #保存された_モデルのローディング import tensorflow as tf import time import numpy as np import warnings warnings.filterwarnings('ignore') from PIL import Image from google.colab.patches import cv2_imshow from object_detection.utils import label_map_util from object_detection.utils import visualization_utils as viz_utils IMAGE_SIZE = (12, 8) # Output display size as you want import matplotlib.pyplot as plt PATH_TO_SAVED_MODEL="/content/customTF2/customTF2/data/inference_graph/saved_model" print('Loading model...', end='') # 保存したモデルを読み込み、検出機能を構築します。 detect_fn=tf.saved_model.load(PATH_TO_SAVED_MODEL) print('Done!') #ラベル_マップのローディング category_index=label_map_util.create_category_index_from_labelmap("/content/customTF2/customTF2/data/label_map.pbtxt",use_display_name=True) def load_image_into_numpy_array(path): return np.array(Image.open(path)) # テストイメージに置き換えます image_path = "/content/customTF2/customTF2/data/images/your_test.jpg" #print('Running inference for {}... '.format(image_path), end='') image_np = load_image_into_numpy_array(image_path) # 入力はテンソルでなければなりません。`tf.convert_to_tensor` を使用して変換します。 input_tensor = tf.convert_to_tensor(image_np) # モデルは画像のバッチを想定しているため、`tf.newaxis` で軸を追加してください。 input_tensor = input_tensor[tf.newaxis, ...] detections = detect_fn(input_tensor) # すべての出力はバッチテンソルです。 # numpy 配列に変換し、インデックス [0] を使用してバッチディメンションを削除します。 # 対象とするのは最初の num_detections だけです。 num_detections = int(detections.pop('num_detections')) detections = {key: value[0, :num_detections].numpy() for key, value in detections.items()} detections['num_detections'] = num_detections # 検出_クラスは整数でなければなりません。 detections['detection_classes'] = detections['detection_classes'].astype(np.int64) image_np_with_detections = image_np.copy() viz_utils.visualize_boxes_and_labels_on_image_array( image_np_with_detections, detections['detection_boxes'], detections['detection_classes'], detections['detection_scores'], category_index, use_normalized_coordinates=True, max_boxes_to_draw=200, min_score_thresh=.8, # この値を調整して True として分類される最小確率ボックスを設定します agnostic_mode=False) %matplotlib inline plt.figure(figsize=IMAGE_SIZE, dpi=200) plt.axis("off") plt.imshow(image_np_with_detections) plt.show() トレーニング済みの SSD (シングルショット検出器) モデルから TFLite モデルへの変換 12) tf-nightlyをインストールします TFLiteコンバーターは、tf-nightlyでうまく機能します。 %cd /content/models/research/object_detection !pip install tensorflow=="2.12.0" !pip install numpy==1.26.4 !pip install tf-nightly 13) SSDTFLiteグラフをエクスポートします 現在の作業ディレクトリは /content/models/research/object_detection検出です # !pip3 uninstall keras # !pip3 install keras==2.14.0 !pip3 install --upgrade tensorflow keras !pip3 install tensorflow=="2.12.0" # !pip3 install --upgrade tensorflow keras # !pip3 install tensorflow=="2.13.1" # !pip3 install numpy --upgrade # !pip3 uninstall numpy # !pip3 install numpy=="1.22.0" # !pip3 install tensorflow --upgrade #!python --version %cd /content/models/research/object_detection !python export_tflite_graph_tf2.py --pipeline_config_path /content/customTF2/customTF2/data/ssd_mobilenet_v2_fpnlite_320x320_coco17_tpu-8.config --trained_checkpoint_dir /content/customTF2/customTF2/training --output_directory /content/customTF2/customTF2/data/tflite 14) TF 保存モデルを TFLite モデルに変換 現在の作業ディレクトリは /mydrive/customTF2/data/ です %cd /content/customTF2/customTF2/data/ 入力テンソル名と出力テンソル名を確認 !saved_model_cli show--dir /content/customTF2/customTF2/data/tflite/saved_model--tag_set serve—all TFLite への変換: 方法 (a) または方法 (b) のいずれかを使用します。 方法 (a) コマンドラインツール tflite_convert を使用-(基本モデル変換) # デフォルトの推論タイプは浮動小数点です。 %cd /content/customTF2/customTF2/data/ !tflite_convert --saved_model_dir=tflite/saved_model --output_file=tflite/detect.tflite 方法 (b) Python API を使用-(最適化などによる高度なモデル変換用) %cd /mydrive/customTF2/data/ #'''******************************** # 浮動小数点推論用 #*********************************''' #import tensorflow as tf saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' #converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) #tflite_model = converter.convert() #open("/content/customTF2/customTF2/data/tflite/detect.tflite", "wb").write(tflite_model) #'''************************************************** # 最適化による浮動小数点推論用 #***************************************************''' import tensorflow as tf converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir,signature_keys=['serving_default']) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.experimental_new_converter = True converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS] tflite_model = converter.convert() with tf.io.gfile.GFile('/mydrive/customTF2/data/tflite/detect.tflite', 'wb') as f: f.write(tflite_model) #'''********************************** # ダイナミックレンジ量子化用 #************************************* # このモデルは重みが量子化され、少し小さくなりましたが、他の変数データはまだfloat形式です。''' # import tensorflow as tf # converter = tf.lite.TFLiteConverter.from_saved_model('/content/customTF2/customTF2/data/tflite/saved_model',signature_keys=['serving_default']) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # tflite_quant_model = converter.convert() # with tf.io.gfile.GFile('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''*********************************************************************** # 整数と浮動小数点のフォールバック量子化(デフォルトの最適化を使用) # ************************************************************************** # これで、すべての重みと変数データが量子化され、元のTensorFlow Liteモデルと比較してモデルが大幅に小さくなりました。 # ただし、float形式のモデル入力および出力テンソルを従来使用しているアプリケーションとの互換性を維持するために、 # TensorFlow Lite コンバーターはモデルの入力テンソルと出力テンソルをフロートのままにします''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # tflite_quant_model = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model) # '''********************************* # 全整数量子化用 # ************************************ # 内部量子化は前の浮動小数点フォールバック量子化方法と同じままです。 # しかし、入力と出力のテンソルは、ここでも整数形式になっていることがわかります。''' # import tensorflow as tf # import numpy as np # saved_model_dir = '/content/customTF2/customTF2/data/tflite/saved_model' # def representative_dataset(): # for _ in range(100): # data = np.random.rand(1, 320, 320, 3) # yield [data.astype(np.float32)] # converter = tf.lite.TFLiteConverter.from_saved_model(saved_model_dir) # converter.optimizations = [tf.lite.Optimize.DEFAULT] # converter.representative_dataset = representative_dataset # converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] # converter.inference_input_type = tf.uint8 # converter.inference_output_type = tf.uint8 # tflite_quant_model_full_int = converter.convert() # with open('/content/customTF2/customTF2/data/tflite/detect.tflite', 'wb') as f: # f.write(tflite_quant_model_full_int) トレーニング後の量子化の詳細は、 こちら をご覧ください。これらについては、 こちら のColabノートブックでも読むことができます。 15) TFLite メタデータを作成します !pip install tflite_support_nightly %cd /content/customTF2/customTF2/data/ %d tflite/ !mkdir tflite_with_metadata %cd .. データ フォルダ内の各行にクラスの名前が書き込まれた labelmap.txt ファイルを作成します。 最後に、次のセルを実行して、メタデータが添付された detect.tflite モデルを作成します。 現在の作業ディレクトリは /content/customTF2/customTF2/data/ %cd /content/customTF2/customTF2/data/ !pip uninstall tensorflow !pip install tensorflow=="2.13.1" # TFLite にメタデータを添付 from tflite_support.metadata_writers import object_detector from tflite_support.metadata_writers import writer_utils import flatbuffers import platform from tensorflow_lite_support.metadata import metadata_schema_py_generated from tensorflow_lite_support.metadata import schema_py_generated from tensorflow_lite_support.metadata.python import metadata from tensorflow_lite_support.metadata.python import metadata_writers import flatbuffers import os from tensorflow_lite_support.metadata import metadata_schema_py_generated as _metadata_fb from tensorflow_lite_support.metadata.python import metadata as _metadata from tensorflow_lite_support.metadata.python.metadata_writers import metadata_info from tensorflow_lite_support.metadata.python.metadata_writers import metadata_writer from tensorflow_lite_support.metadata.python.metadata_writers import writer_utils ObjectDetectorWriter = object_detector.MetadataWriter _MODEL_PATH = "/content/customTF2/customTF2/data/tflite/detect.tflite" _LABEL_FILE = "/content/customTF2/customTF2/data/labelmap.txt" _SAVE_TO_PATH = "/content/customTF2/customTF2/data/tflite/tflite_with_metadata/detect.tflite" writer = ObjectDetectorWriter.create_for_inference( writer_utils.load_file(_MODEL_PATH), [127.5], [127.5], [_LABEL_FILE]) writer_utils.save_file(writer.populate(), _SAVE_TO_PATH) # 入力されたメタデータと関連ファイルを確認します。 displayer = metadata.MetadataDisplayer.with_model_file(_SAVE_TO_PATH) print("Metadata populated:") print(displayer.get_metadata_json()) print("Associated file(s) populated:") print(displayer.get_packed_associated_file_list()) model_meta = _metadata_fb.ModelMetadataT() model_meta.name = "SSD_Detector" model_meta.description = ( "Identify which of a known set of objects might be present and provide " "information about their positions within the given image or a video " "stream.") # 入力情報を作成します。 input_meta = _metadata_fb.TensorMetadataT() input_meta.name = "image" input_meta.content = _metadata_fb.ContentT() input_meta.content.contentProperties = _metadata_fb.ImagePropertiesT() input_meta.content.contentProperties.colorSpace = ( _metadata_fb.ColorSpaceType.RGB) input_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.ImageProperties) input_normalization = _metadata_fb.ProcessUnitT() input_normalization.optionsType = ( _metadata_fb.ProcessUnitOptions.NormalizationOptions) input_normalization.options = _metadata_fb.NormalizationOptionsT() input_normalization.options.mean = [127.5] input_normalization.options.std = [127.5] input_meta.processUnits = [input_normalization] input_stats = _metadata_fb.StatsT() input_stats.max = [255] input_stats.min = [0] input_meta.stats = input_stats # 出力情報を作成します。 output_location_meta = _metadata_fb.TensorMetadataT() output_location_meta.name = "location" output_location_meta.description = "The locations of the detected boxes." output_location_meta.content = _metadata_fb.ContentT() output_location_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.BoundingBoxProperties) output_location_meta.content.contentProperties = ( _metadata_fb.BoundingBoxPropertiesT()) output_location_meta.content.contentProperties.index = [1, 0, 3, 2] output_location_meta.content.contentProperties.type = ( _metadata_fb.BoundingBoxType.BOUNDARIES) output_location_meta.content.contentProperties.coordinateType = ( _metadata_fb.CoordinateType.RATIO) output_location_meta.content.range = _metadata_fb.ValueRangeT() output_location_meta.content.range.min = 2 output_location_meta.content.range.max = 2 output_class_meta = _metadata_fb.TensorMetadataT() output_class_meta.name = "category" output_class_meta.description = "The categories of the detected boxes." output_class_meta.content = _metadata_fb.ContentT() output_class_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_class_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_class_meta.content.range = _metadata_fb.ValueRangeT() output_class_meta.content.range.min = 2 output_class_meta.content.range.max = 2 label_file = _metadata_fb.AssociatedFileT() label_file.name = os.path.basename("labelmap.txt") label_file.description = "Label of objects that this model can recognize." label_file.type = _metadata_fb.AssociatedFileType.TENSOR_VALUE_LABELS output_class_meta.associatedFiles = [label_file] output_score_meta = _metadata_fb.TensorMetadataT() output_score_meta.name = "score" output_score_meta.description = "The scores of the detected boxes." output_score_meta.content = _metadata_fb.ContentT() output_score_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_score_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) output_score_meta.content.range = _metadata_fb.ValueRangeT() output_score_meta.content.range.min = 2 output_score_meta.content.range.max = 2 output_number_meta = _metadata_fb.TensorMetadataT() output_number_meta.name = "number of detections" output_number_meta.description = "The number of the detected boxes." output_number_meta.content = _metadata_fb.ContentT() output_number_meta.content.contentPropertiesType = ( _metadata_fb.ContentProperties.FeatureProperties) output_number_meta.content.contentProperties = ( _metadata_fb.FeaturePropertiesT()) # サブグラフ情報を作成します。 group = _metadata_fb.TensorGroupT() group.name = "detection result" group.tensorNames = [ output_location_meta.name, output_class_meta.name, output_score_meta.name ] subgraph = _metadata_fb.SubGraphMetadataT() subgraph.inputTensorMetadata = [input_meta] subgraph.outputTensorMetadata = [ output_location_meta, output_class_meta, output_score_meta, output_number_meta ] subgraph.outputTensorGroups = [group] model_meta.subgraphMetadata = [subgraph] b = flatbuffers.Builder(0) b.Finish( model_meta.Pack(b), _metadata.MetadataPopulator.METADATA_FILE_IDENTIFIER) metadata_buf = b.Output() 質問されたら、'Y'で進みます 16) TFLite モデルをダウンロードします お疲れ様でした。これで完了です! まとめ Google Colab Enterpriseは、機械学習のための強力なクラウドベースのプラットフォームであり、TensorFlow Liteモデルを構築する上で理想的な環境です。このプラットフォームを1年以上使用してみて、最も時間がかかるのはデータ準備と初期の試行錯誤段階であることが分かりました。この段階では、データセットの特定の部分を認識する際の課題を特定し、画像が誤って分類される誤検知に対処するために、著しい反復とテストが必要です。 *Android ロボットのヘッダー画像は、Google が作成および共有した画像から複製または変更されたものであり、クリエイティブ コモンズ 3.0 帰属ライセンスに記載されている条件に従って使用されています。
This article is part of day 6 of KINTO Technologies Advent Calendar 2024 . Introduction Hello. I am Uehara ( @penpen_77777 ), and I am part of the KINTO FACTORY Development Group. I joined in July 2024, and I was assigned to the backend development of KINTO FACTORY. This time, using sample code, I will introduce the data conflicts that you should be cautious of when processing S3 events, and how to resolve them. Intended Readers Those who are concerned with AWS S3 event notifications being duplicated or the order of notifications being changed. Those with basic knowledge of Rust, S3, DynamoDB, Lambda, and Terraform. Having this knowledge will make it easier to understand when reading the sample code. S3 Event Overview An S3 event^[AWS document regarding S3 event notifications https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/EventNotifications.html] is an event that occurs when an operation such as uploading or deleting an object to S3 is triggered. By detecting S3 events using Lambda functions or SNS, various processes concerning S3 can be automated. Issues with S3 Events Something you should be cautious of when processing S3 events is that the event notifications could be duplicated or the order could be changed. For example, let’s think about the process of creating an object after deleting an object for the same object key. In this case, first you will be notified of the object delete event, then you can expect to be notified of the create event (remove the same object => see the diagram of the expected S3 event reception order when created). However, there are times when the S3 event order isn’t secured, and the create event notification comes before the delete event notification (remove the same object => see the diagram of the order of reception of possible S3 events when created). As a result, the processing result by the event that deletes the object will be the latest, and the issue of data consistency not being guaranteed depending on the processing content occurs. gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :done, cre2, 00:00:03, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :active, cre2, 00:00:04, 1s gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :active, cre2, 00:00:04, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :done, cre2, 00:00:03, 1s As a solution to this problem, there is a way to guarantee the order of events using the sequencer key included in S3 events^[document regarding S3 event structure. https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-content-structure.html] One way to determine the sequence of events is to use the sequencer key. There is no guarantee that event notifications will be delivered in the order in which the event occurred. However, notifications and delete objects from events that create objects (PUT) include the sequencer. This can be used to determine the order of events for a particular object key. When comparing the sequencer strings of two event notifications for the same object key, you can see that the event notification with the larger 16 hexadecimal value sequencer is the event that occurred later. When maintaining a separate database or index of Amazon S3 objects using event notifications, I recommend that you compare and save the sequencer value each time you process an event notification. Please note the following: The sequencer cannot be used to determine the order of events for multiple object keys. The length of the sequencer may be different. To compare these values, first input 0 on the right of the shorter value, and then perform a lexicographic comparison. To Summarize: The sequencer is a value included in an object's PUT or DELETE event, and can be used to determine the order of events. Lexicographically compare the sequencers. The larger the value, the later the event that occurred. If the lengths differ, compare after inputting 0 on the right side of the shorter value. It cannot be used to determine the order of S3 events between multiple objects. It is used to determine the order of PUT and DELETE events for the same object. For example, if you want to implement the S3 event sequencer comparison in Rust, you can implement it as follows: Define the fields and constructor of the structure S3Sequencer to express the nature of the S3 sequencer. // 1. Define the structure S3Sequencer use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct S3Sequencer { bucket_name: String, object_key: String, sequencer: String, } // 2. Define the S3Sequencer constructor // Take the bucket name, object key, and sequencer as argument impl S3Sequencer { pub fn new(bucket_name: &str, objcet_key: &str, sequencer: &str) -> Self { Self { bucket_name: bucket_name.to_owned(), object_key: objcet_key.to_owned(), sequencer: sequencer.to_owned(), } } } Next, to determine the order of events by comparing the size of the S3Sequencer, use the PartialOrd ^[Mathematically speaking, it can represent a partially ordered set. There is also a full ordered set that implements the ‘Ord’ trait. It is interesting that traits are intentionally divided to implement comparison methods. I will implement https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html https://ja.wikipedia.org/wiki/%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88#%E5%8D%8A%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88] and PartialEq trait^[PartialEq trait definition https://doc.rust-lang.org/std/cmp/trait.PartialEq.html] By implementing these two, you can compare the size of the sequencers using comparison operators such as == , < , or > as shown below. let seq1 = S3Sequencer::new("bucket1", "object1", "abc123"); let seq2 = S3Sequencer::new("bucket1", "object1", "abc124"); if seq1 < seq2 { println!("seq1 is an older event than seq2"); } else if seq1 == seq2 { println!("seq1 and seq2"); } else { println!("seq1 is a newer event than seq2"); } Implement the partial_cmp method required to implement the PartialOrd trait as shown below. impl PartialOrd for S3Sequencer { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { // Sequencers with different bucket names cannot be compared if self.bucket_name != other.bucket_name { return None; } // Sequencers with different object keys cannot be compared if self.object_key != other.object_key { return None; } // Compare by adding 0 to the end of the shorter one to match with the longer one let max_len = std::cmp::max(self.sequencer.len(), other.sequencer.len()); let self_sequencer = self .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); let other_sequencer = other .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); Some(self_sequencer.cmp(other_sequencer)) } Since there is no meaning in comparing sequencers with different bucket names and object keys, return None using early return if they are different. You can compare the sequencers once you confirm that the bucket names and object keys are the same, but the process will be in the following order. Compare the length of the sequencers, and store the longer one in max_len Add 0 to the end of the shorter sequencer to match max_len Compare the sequencers created in 2 in lexicographic order and return the size The PartialEq trait is implemented as follows: impl PartialEq for S3Sequencer { fn eq(&self, other: &Self) -> bool { self.partial_cmp(other) .map_or(false, |o| o == std::cmp::Ordering::Equal) } } Determine if the sequencer comparison results are equal using the partial_cmp method of the PartialOrd trait. With the above implementations, it is now possible to compare sequencers for S3 events. Example of S3 Event Processing Implementation Considering Data Consistency Architecture Diagram Now, I will introduce how to guarantee the order of S3 events using sequencers, along with sample code The sample code converts the image file uploaded to the S3 bucket to grayscale on Lambda and saves it to the S3 bucket for output. Below is an architecture diagram. ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) When an image file is uploaded to the image input bucket, the Lambda function is triggered through an S3 event. The launched Lambda function checks DynamoDB to confirm that it is not processing, then sets the processing flag and processes the image file. Once processing has finished, it clears the processing flag and waits for the next image file to be processed. If the order of the create and delete event notifications is reversed, problems such as accidently deleting images that should exist could occur. For example, let’s assume that the process is implemented as follows. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (3) Event reception, Object deletion :done, cre2, 00:00:03, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (4) Event reception, convert to grayscale :active, cre2, 00:00:04, 1s However, there is a possibility of the notification order for 3 and 4 being reversed in S3 events, so the following flow may occur. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (4) Event reception, Object deletion :active, cre2, 00:00:04, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (3) Event reception, convert to grayscale :done, cre2, 00:00:03, 1s In this case, the problem occurs that even though image file A exists in the input bucket, the grayscaled image file A' does not exist in the output bucket. In order to prevent problems such as this, use the S3 event sequencer to implement exclusive processing. Additionally, it is also necessary to implement exclusive processing to manage image processing status by DynamoDB. Use DynamoDB conditional write to set an in-progress flag in order to prevent multiple Lambda functions from processing the same image file at the same time. The sample code is published on GITHUB. Take a look using the link below. (You will need to build an AWS infrastructure to run it, but it has been made easy to try using the terraform code.) https://github.com/kinto-technologies/techblog-s3-sequencer-example Implementing Sample Code with Rust This time, I will implement a Lambda function using Rust. It is convenient to use cargo-lambda to implement Lambda functions. https://www.cargo-lambda.info/ I will omit the details of how to use cargo-lambda. Creating an Entry Point When you run the initialization command on cargo-lambda, main.rs is automatically generated as shown below. cargo lambda init use lambda_runtime::{ run, service_fn, tracing::{self}, Error, }; mod handler; mod image_task; mod lock; mod s3_sequencer; #[tokio::main] async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); run(service_fn(handler::function_handler)).await } Since handler:function_handler is specified as the entry point for the Lambda function, I will write the implementation in handler.rs The function_handler is implemented as follows. use crate::image_task::ImageTask; use aws_lambda_events::event::s3::S3Event; use lambda_runtime::{tracing::info, Error, LambdaEvent}; pub async fn function_handler(event: LambdaEvent<S3Event>) -> Result<(), Error> { // Convert S3 events to ImageTask let tasks: Vec<_> = event .payload .records .into_iter() .map(ImageTask::try_from) .collect::<Result<Vec<_>, _>>()?; // Use futures::future::join_all to create the task to be executed let execute_tasks = tasks.iter().map(|task| task.execute()); // Use join_all to execute or wait all tasks // Store the execution results in ret let ret = futures::future::join_all(execute_tasks).await; // Output the execution results to log for (t, r) in tasks.iter().zip(&ret) { info!("object_key: {}, Result: {:?}", t.object_key, r); } // Return error is there is an error if ret.iter().any(|r| r.is_err()) { return Err("Some tasks failed".into()); } // Successful termination Ok(()) } Convert the S3 event vector to an ImageTask structure vector. Since the conversion method implements the TryFrom trait, you just have to call the try_form method. Create an image processing task based on the ImageTask structure vector. Use the tokio crate join_all function to execute all tasks in parallel. Output the results returned by join_all in 3 to the log. If there is an error, return the error and terminate the Lambda function abnormally. If there are no errors it will successfully terminate. Image Processing Implementation The ImageTask structure used in 1 is defined as below and holds the information necessary to execute Lambda. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type")] pub enum TaskType { Grayscale, Delete, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageTask { pub bucket_name: String, #[serde(rename = "id")] pub object_key: String, pub sequencer: S3Sequencer, pub task_type: TaskType, pub processing: bool, } Field name Description bucket_name S3 bucket name object_key Object key sequencer S3 event sequencer task_type Enumeration indicating the type of task (Grayscale, Delete) processing Processing flag Implement specific image processing within the execute method of the ImageTask structure. impl ImageTask { pub async fn execute(&self) -> Result<(), Error> { // 1. Acquire lock let lock = S3Lock::new(&self).await?; // 2. Process according to the task type match self.task_type { TaskType::Grayscale => { // Convert image to grayscale and save in output bucket let body = lock.read_input_object().await?; let format = image::ImageFormat::from_path(&self.object_key)?; let img = image::load_from_memory_with_format(&body, format)?; let img = img.grayscale(); let mut buf = Vec::new(); img.write_to(&mut Cursor::new(&mut buf), format)?; lock.write_output_object(buf).await?; } // Delete image from output bucket TaskType::Delete => lock.delete_output_object().await?, } // 3. Release lock lock.free().await?; Ok(()) } } Apply exclusive processing to prevent S3 object data inconsistencies Process according to the task type If a file was added to a previous bucket, convert the image to grayscale and save it in the output bucket If a file was deleted, delete the file from the output bucket Release lock when processing has finished Implement Lock Processing Define the S3Lock structure to implement lock processing. pub struct S3Lock { dynamodb_client: aws_sdk_dynamodb::Client, table_name: String, s3_client: aws_sdk_s3::Client, input_bucket_name: String, input_object_key: String, output_bucket_name: String, output_object_key: String, } The specific lock acquisition process is implemented in the constructor. The code is a bit long, but roughly speaking, it is as follows. If writing to DynamoDB is successful, the lock is considered to have been acquired. If writing fails, try again every 2 seconds. If the lock cannot be acquired within 30 seconds, a timeout occurs. The lock processing sequence diagram is shown below. sequenceDiagram participant ImageTask participant S3Lock participant DynamoDB ImageTask->>S3Lock: Acquire lock loop alt timeout (30 seconds) S3Lock->>ImageTask: Return error (timeout) end S3Lock->>DynamoDB: Acquire processing status DynamoDB->>S3Lock: Return results alt record exists S3Lock->>S3Lock: Sequencer comparison alt is old S3Lock->>ImageTask: Return error (skip) else is new S3Lock->>S3Lock: Retry acquiring lock end else S3Lock->>DynamoDB: Acquire lock with conditional write DynamoDB->>S3Lock: Write return result alt write successful S3Lock->>ImageTask: Lock acquisition successful else failure S3Lock->>S3Lock: retry end end end The code in the constructor is as follows: impl S3Lock { pub async fn new(task: &ImageTask) -> Result<Self, Error> { let table_name = std::env::var("DYNAMODB_TABLE_NAME").unwrap(); let output_bucket_name = std::env::var("OUTPUT_BUCKET_NAME").unwrap(); let require_lock_timeout = Duration::from_secs( std::env::var("REQUIRE_LOCK_TIMEOUT") .unwrap_or_else(|_| "30".to_string()) .parse::<u64>() .unwrap(), ); let interval_retry_time = Duration::from_secs( std::env::var("RETRY_INTERVAL") .unwrap_or_else(|_| "2".to_string()) .parse::<u64>() .unwrap(), ); let config = aws_config::load_defaults(aws_config::BehaviorVersion::v2024_03_28()).await; let s3_client = aws_sdk_s3::Client::new(&config); let dynamodb_client = aws_sdk_dynamodb::Client::new(&config); // Acquire lock Measure execution time let start = Instant::now(); loop { // It will time out if the lock cannot be acquired for more than 30 seconds. if start.elapsed() > require_lock_timeout { return Err("Failed to acquire lock, timeout".into()); } Acquire sequencer from DynamoDB with strong read consistency let item = dynamodb_client .get_item() .table_name(table_name.clone()) .key("id", AttributeValue::S(task.object_key.clone())) .consistent_read(true) .send() .await?; Compare sequencer if acquired item exists if let Some(item) = item.item { let item: ImageTask = from_item(item)?; if task.sequencer <= item.sequencer { If the sequencer itself is old, there is no need to process it, so skip it return Err("Old sequencer".into()); } If the sequencer itself is new, wait until other processing has finished if item.processing { warn!( "Waiting for other process to finish task, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(interval_retry_time); continue; } } // Acquire lock with conditional write to DynamoDB // If the record exists at that time, only write if the processing flag is false let resp = dynamodb_client .put_item() .table_name(table_name.clone()) .set_item(Some(to_item(&task).unwrap())) .condition_expression("attribute_not_exists(id) OR processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await; Once acquired, exit the loop and continue processing If it could not be acquired, continue trying until the lock can be acquired. match resp { Ok(_) => break, Err(SdkError::ServiceError(e)) => match e.err() { PutItemError::ConditionalCheckFailedException(_) => { warn!( "Failed to acquire lock, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(Duration::from_secs(2)); continue; } _ => return Err(format!("{:?}", e).into()), }, Err(e) => return Err(e.into()), } } return Ok(Self { dynamodb_client, output_bucket_name, s3_client, table_name, input_bucket_name: task.bucket_name.clone(), input_object_key: task.object_key.clone(), output_object_key: task.object_key.clone(), }); } } The process to release the lock is implemented as shown below, and the lock is released by updating the processing flag to false. impl S3Lock { pub async fn free(self) -> Result<(), Error> { // Release DynamoDB lock // Only update the processing flag self.dynamodb_client .update_item() .table_name(self.table_name) .key("id", AttributeValue::S(self.input_object_key)) .update_expression("SET processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await?; Ok(()) } } Since I want to force a lock when touching an S3 object, I have created a method for manipulating S3 objects in S3Lock^[I feel that reusability will be lower if I create it in S3Lock, but for simplicity I will define it in the same structure. Though there seems to be a better way...] impl S3Lock { pub async fn read_input_object(&self) -> Result<Vec<u8>, Error> { // Acquire S3 object let object = self .s3_client .get_object() .bucket(&self.input_bucket_name) .key(&self.input_object_key) .send() .await?; let body = object.body.collect().await?.to_vec(); Ok(body) } pub async fn write_output_object(&self, buf: Vec<u8>) -> Result<(), Error> { // Save S3 object let byte_stream = ByteStream::from(buf); self.s3_client .put_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .body(byte_stream) .send() .await?; Ok(()) } pub async fn delete_output_object(&self) -> Result<(), Error> { // Delete S3 object self.s3_client .delete_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .send() .await?; Ok(()) } } Trying it Out Let’s try using the sample code. I need to prepare an image that I want to convert to grayscale, so this time I will use Hyogo Prefectural Park Awaji Hanasajiki^[a beautiful flower garden on Awaji Island https://awajihanasajiki.jp/about/] . I took the photo. I will use the image from https://awajihanasajiki.jp/about/ . Infrastructure First, use terraform apply to build the AWS infrastructure. Then, clone the GitHub repository and implement the following command. cd terraform # Modify variables.tf and provider.tf appropriately. terraform init terraform apply ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) Upload the Image File to the S3 Bucket If infrastructure is completed, upload the image file to the input S3 bucket. The Lambda function process begins when the upload finishes, and the item is added to the DynamoDB table. The image file is saved in the output S3 bucket when the process finishes, and the processing flag of the DynamoDB item is set to false. You can see the image added to the output S3 bucket and converted to grayscale. Delete the Image File from the S3 Bucket When an object is deleted from the input bucket, it is also deleted from the output bucket. Confirm that Exclusive Processing is Working When the processing flag of an item added to DynamoDB is set to true, processing will wait even if an S3 event that should be processed arrives. To confirm this behavior, let's intentionally set the DynamoDB processing flag to true and upload a file with the same name. If you look at CloudWatch Logs, you can see that it waits for other processes to complete without processing newly generated S3 events. The log message will indicate that Lambda is waiting. Setting the DynamoDB processing flag back to false will restart processing. When the DynamoDB item's processing flag is set back to false, the log will indicate that processing is completed. Thanks to exclusive processing, the order of processing is guaranteed even if delete and upload events occur at approximately the same time. Conclusion This time, I introduced sample code for image processing that uses a sequencer to guarantee the order of S3 events. To guarantee the order of S3 events, you need to utilize a sequencer to compare the order of events. I implemented the sample code in Rust as my own hobby, but it should be possible to implement something similar in other languages. Please feel free to use this as a reference.
This article is part of day 6 of KINTO Technologies Advent Calendar 2024 . Introduction Hello. I am Uehara ( @penpen_77777 ), and I am part of the KINTO FACTORY Development Group. I joined in July 2024, and I was assigned to the backend development of KINTO FACTORY. This time, using sample code, I will introduce the data conflicts that you should be cautious of when processing S3 events, and how to resolve them. Intended Readers Those who are concerned with AWS S3 event notifications being duplicated or the order of notifications being changed. Those with basic knowledge of Rust, S3, DynamoDB, Lambda, and Terraform. Having this knowledge will make it easier to understand when reading the sample code. S3 Event Overview An S3 event^[AWS document regarding S3 event notifications https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/EventNotifications.html] is an event that occurs when an operation such as uploading or deleting an object to S3 is triggered. By detecting S3 events using Lambda functions or SNS, various processes concerning S3 can be automated. Issues with S3 Events Something you should be cautious of when processing S3 events is that the event notifications could be duplicated or the order could be changed. For example, let’s think about the process of creating an object after deleting an object for the same object key. In this case, first you will be notified of the object delete event, then you can expect to be notified of the create event (remove the same object => see the diagram of the expected S3 event reception order when created). However, there are times when the S3 event order isn’t secured, and the create event notification comes before the delete event notification (remove the same object => see the diagram of the order of reception of possible S3 events when created). As a result, the processing result by the event that deletes the object will be the latest, and the issue of data consistency not being guaranteed depending on the processing content occurs. gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :done, cre2, 00:00:03, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :active, cre2, 00:00:04, 1s gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion Object deletion :done, cre1, 00:00:01, 1s Receive delete event / Process :active, cre2, 00:00:04, 1s section Upload object Upload object :done, cre1, 00:00:02, 1s Receive create event / Process :done, cre2, 00:00:03, 1s As a solution to this problem, there is a way to guarantee the order of events using the sequencer key included in S3 events^[document regarding S3 event structure. https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/notification-content-structure.html] One way to determine the sequence of events is to use the sequencer key. There is no guarantee that event notifications will be delivered in the order in which the event occurred. However, notifications and delete objects from events that create objects (PUT) include the sequencer. This can be used to determine the order of events for a particular object key. When comparing the sequencer strings of two event notifications for the same object key, you can see that the event notification with the larger 16 hexadecimal value sequencer is the event that occurred later. When maintaining a separate database or index of Amazon S3 objects using event notifications, I recommend that you compare and save the sequencer value each time you process an event notification. Please note the following: The sequencer cannot be used to determine the order of events for multiple object keys. The length of the sequencer may be different. To compare these values, first input 0 on the right of the shorter value, and then perform a lexicographic comparison. To Summarize: The sequencer is a value included in an object's PUT or DELETE event, and can be used to determine the order of events. Lexicographically compare the sequencers. The larger the value, the later the event that occurred. If the lengths differ, compare after inputting 0 on the right side of the shorter value. It cannot be used to determine the order of S3 events between multiple objects. It is used to determine the order of PUT and DELETE events for the same object. For example, if you want to implement the S3 event sequencer comparison in Rust, you can implement it as follows: Define the fields and constructor of the structure S3Sequencer to express the nature of the S3 sequencer. // 1. Define the structure S3Sequencer use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct S3Sequencer { bucket_name: String, object_key: String, sequencer: String, } // 2. Define the S3Sequencer constructor // Take the bucket name, object key, and sequencer as argument impl S3Sequencer { pub fn new(bucket_name: &str, objcet_key: &str, sequencer: &str) -> Self { Self { bucket_name: bucket_name.to_owned(), object_key: objcet_key.to_owned(), sequencer: sequencer.to_owned(), } } } Next, to determine the order of events by comparing the size of the S3Sequencer, use the PartialOrd ^[Mathematically speaking, it can represent a partially ordered set. There is also a full ordered set that implements the ‘Ord’ trait. It is interesting that traits are intentionally divided to implement comparison methods. I will implement https://doc.rust-lang.org/std/cmp/trait.PartialOrd.html https://ja.wikipedia.org/wiki/%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88#%E5%8D%8A%E9%A0%86%E5%BA%8F%E9%9B%86%E5%90%88] and PartialEq trait^[PartialEq trait definition https://doc.rust-lang.org/std/cmp/trait.PartialEq.html] By implementing these two, you can compare the size of the sequencers using comparison operators such as == , < , or > as shown below. let seq1 = S3Sequencer::new("bucket1", "object1", "abc123"); let seq2 = S3Sequencer::new("bucket1", "object1", "abc124"); if seq1 < seq2 { println!("seq1 is an older event than seq2"); } else if seq1 == seq2 { println!("seq1 and seq2"); } else { println!("seq1 is a newer event than seq2"); } Implement the partial_cmp method required to implement the PartialOrd trait as shown below. impl PartialOrd for S3Sequencer { fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { // Sequencers with different bucket names cannot be compared if self.bucket_name != other.bucket_name { return None; } // Sequencers with different object keys cannot be compared if self.object_key != other.object_key { return None; } // Compare by adding 0 to the end of the shorter one to match with the longer one let max_len = std::cmp::max(self.sequencer.len(), other.sequencer.len()); let self_sequencer = self .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); let other_sequencer = other .sequencer .chars() .chain(std::iter::repeat('0')) .take(max_len); Some(self_sequencer.cmp(other_sequencer)) } Since there is no meaning in comparing sequencers with different bucket names and object keys, return None using early return if they are different. You can compare the sequencers once you confirm that the bucket names and object keys are the same, but the process will be in the following order. Compare the length of the sequencers, and store the longer one in max_len Add 0 to the end of the shorter sequencer to match max_len Compare the sequencers created in 2 in lexicographic order and return the size The PartialEq trait is implemented as follows: impl PartialEq for S3Sequencer { fn eq(&self, other: &Self) -> bool { self.partial_cmp(other) .map_or(false, |o| o == std::cmp::Ordering::Equal) } } Determine if the sequencer comparison results are equal using the partial_cmp method of the PartialOrd trait. With the above implementations, it is now possible to compare sequencers for S3 events. Example of S3 Event Processing Implementation Considering Data Consistency Architecture Diagram Now, I will introduce how to guarantee the order of S3 events using sequencers, along with sample code The sample code converts the image file uploaded to the S3 bucket to grayscale on Lambda and saves it to the S3 bucket for output. Below is an architecture diagram. ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) When an image file is uploaded to the image input bucket, the Lambda function is triggered through an S3 event. The launched Lambda function checks DynamoDB to confirm that it is not processing, then sets the processing flag and processes the image file. Once processing has finished, it clears the processing flag and waits for the next image file to be processed. If the order of the create and delete event notifications is reversed, problems such as accidently deleting images that should exist could occur. For example, let’s assume that the process is implemented as follows. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (3) Event reception, Object deletion :done, cre2, 00:00:03, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (4) Event reception, convert to grayscale :active, cre2, 00:00:04, 1s However, there is a possibility of the notification order for 3 and 4 being reversed in S3 events, so the following flow may occur. Image file A is deleted from the input bucket Image file A is re-uploaded to the input bucket Lambda receives a create event (corresponding to 2), processes image file A, converts it to grayscale, and saves it in the output bucket Lambda receives a delete event (corresponding to 1) and deletes image file A from the output bucket gantt title Remove the same object => Expected S3 event reception order when created dateFormat HH:mm:ss axisFormat %H:%M:%S section Object deletion (1) Object deletion :done, cre1, 00:00:01, 1s (4) Event reception, Object deletion :active, cre2, 00:00:04, 1s section Upload object (2) Upload object :done, cre1, 00:00:02, 1s (3) Event reception, convert to grayscale :done, cre2, 00:00:03, 1s In this case, the problem occurs that even though image file A exists in the input bucket, the grayscaled image file A' does not exist in the output bucket. In order to prevent problems such as this, use the S3 event sequencer to implement exclusive processing. Additionally, it is also necessary to implement exclusive processing to manage image processing status by DynamoDB. Use DynamoDB conditional write to set an in-progress flag in order to prevent multiple Lambda functions from processing the same image file at the same time. The sample code is published on GITHUB. Take a look using the link below. (You will need to build an AWS infrastructure to run it, but it has been made easy to try using the terraform code.) https://github.com/kinto-technologies/techblog-s3-sequencer-example Implementing Sample Code with Rust This time, I will implement a Lambda function using Rust. It is convenient to use cargo-lambda to implement Lambda functions. https://www.cargo-lambda.info/ I will omit the details of how to use cargo-lambda. Creating an Entry Point When you run the initialization command on cargo-lambda, main.rs is automatically generated as shown below. cargo lambda init use lambda_runtime::{ run, service_fn, tracing::{self}, Error, }; mod handler; mod image_task; mod lock; mod s3_sequencer; #[tokio::main] async fn main() -> Result<(), Error> { tracing::init_default_subscriber(); run(service_fn(handler::function_handler)).await } Since handler:function_handler is specified as the entry point for the Lambda function, I will write the implementation in handler.rs The function_handler is implemented as follows. use crate::image_task::ImageTask; use aws_lambda_events::event::s3::S3Event; use lambda_runtime::{tracing::info, Error, LambdaEvent}; pub async fn function_handler(event: LambdaEvent<S3Event>) -> Result<(), Error> { // Convert S3 events to ImageTask let tasks: Vec<_> = event .payload .records .into_iter() .map(ImageTask::try_from) .collect::<Result<Vec<_>, _>>()?; // Use futures::future::join_all to create the task to be executed let execute_tasks = tasks.iter().map(|task| task.execute()); // Use join_all to execute or wait all tasks // Store the execution results in ret let ret = futures::future::join_all(execute_tasks).await; // Output the execution results to log for (t, r) in tasks.iter().zip(&ret) { info!("object_key: {}, Result: {:?}", t.object_key, r); } // Return error is there is an error if ret.iter().any(|r| r.is_err()) { return Err("Some tasks failed".into()); } // Successful termination Ok(()) } Convert the S3 event vector to an ImageTask structure vector. Since the conversion method implements the TryFrom trait, you just have to call the try_form method. Create an image processing task based on the ImageTask structure vector. Use the tokio crate join_all function to execute all tasks in parallel. Output the results returned by join_all in 3 to the log. If there is an error, return the error and terminate the Lambda function abnormally. If there are no errors it will successfully terminate. Image Processing Implementation The ImageTask structure used in 1 is defined as below and holds the information necessary to execute Lambda. #[derive(Serialize, Deserialize, Debug, Clone)] #[serde(tag = "type")] pub enum TaskType { Grayscale, Delete, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageTask { pub bucket_name: String, #[serde(rename = "id")] pub object_key: String, pub sequencer: S3Sequencer, pub task_type: TaskType, pub processing: bool, } Field name Description bucket_name S3 bucket name object_key Object key sequencer S3 event sequencer task_type Enumeration indicating the type of task (Grayscale, Delete) processing Processing flag Implement specific image processing within the execute method of the ImageTask structure. impl ImageTask { pub async fn execute(&self) -> Result<(), Error> { // 1. Acquire lock let lock = S3Lock::new(&self).await?; // 2. Process according to the task type match self.task_type { TaskType::Grayscale => { // Convert image to grayscale and save in output bucket let body = lock.read_input_object().await?; let format = image::ImageFormat::from_path(&self.object_key)?; let img = image::load_from_memory_with_format(&body, format)?; let img = img.grayscale(); let mut buf = Vec::new(); img.write_to(&mut Cursor::new(&mut buf), format)?; lock.write_output_object(buf).await?; } // Delete image from output bucket TaskType::Delete => lock.delete_output_object().await?, } // 3. Release lock lock.free().await?; Ok(()) } } Apply exclusive processing to prevent S3 object data inconsistencies Process according to the task type If a file was added to a previous bucket, convert the image to grayscale and save it in the output bucket If a file was deleted, delete the file from the output bucket Release lock when processing has finished Implement Lock Processing Define the S3Lock structure to implement lock processing. pub struct S3Lock { dynamodb_client: aws_sdk_dynamodb::Client, table_name: String, s3_client: aws_sdk_s3::Client, input_bucket_name: String, input_object_key: String, output_bucket_name: String, output_object_key: String, } The specific lock acquisition process is implemented in the constructor. The code is a bit long, but roughly speaking, it is as follows. If writing to DynamoDB is successful, the lock is considered to have been acquired. If writing fails, try again every 2 seconds. If the lock cannot be acquired within 30 seconds, a timeout occurs. The lock processing sequence diagram is shown below. sequenceDiagram participant ImageTask participant S3Lock participant DynamoDB ImageTask->>S3Lock: Acquire lock loop alt timeout (30 seconds) S3Lock->>ImageTask: Return error (timeout) end S3Lock->>DynamoDB: Acquire processing status DynamoDB->>S3Lock: Return results alt record exists S3Lock->>S3Lock: Sequencer comparison alt is old S3Lock->>ImageTask: Return error (skip) else is new S3Lock->>S3Lock: Retry acquiring lock end else S3Lock->>DynamoDB: Acquire lock with conditional write DynamoDB->>S3Lock: Write return result alt write successful S3Lock->>ImageTask: Lock acquisition successful else failure S3Lock->>S3Lock: retry end end end The code in the constructor is as follows: impl S3Lock { pub async fn new(task: &ImageTask) -> Result<Self, Error> { let table_name = std::env::var("DYNAMODB_TABLE_NAME").unwrap(); let output_bucket_name = std::env::var("OUTPUT_BUCKET_NAME").unwrap(); let require_lock_timeout = Duration::from_secs( std::env::var("REQUIRE_LOCK_TIMEOUT") .unwrap_or_else(|_| "30".to_string()) .parse::<u64>() .unwrap(), ); let interval_retry_time = Duration::from_secs( std::env::var("RETRY_INTERVAL") .unwrap_or_else(|_| "2".to_string()) .parse::<u64>() .unwrap(), ); let config = aws_config::load_defaults(aws_config::BehaviorVersion::v2024_03_28()).await; let s3_client = aws_sdk_s3::Client::new(&config); let dynamodb_client = aws_sdk_dynamodb::Client::new(&config); // Acquire lock Measure execution time let start = Instant::now(); loop { // It will time out if the lock cannot be acquired for more than 30 seconds. if start.elapsed() > require_lock_timeout { return Err("Failed to acquire lock, timeout".into()); } Acquire sequencer from DynamoDB with strong read consistency let item = dynamodb_client .get_item() .table_name(table_name.clone()) .key("id", AttributeValue::S(task.object_key.clone())) .consistent_read(true) .send() .await?; Compare sequencer if acquired item exists if let Some(item) = item.item { let item: ImageTask = from_item(item)?; if task.sequencer <= item.sequencer { If the sequencer itself is old, there is no need to process it, so skip it return Err("Old sequencer".into()); } If the sequencer itself is new, wait until other processing has finished if item.processing { warn!( "Waiting for other process to finish task, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(interval_retry_time); continue; } } // Acquire lock with conditional write to DynamoDB // If the record exists at that time, only write if the processing flag is false let resp = dynamodb_client .put_item() .table_name(table_name.clone()) .set_item(Some(to_item(&task).unwrap())) .condition_expression("attribute_not_exists(id) OR processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await; Once acquired, exit the loop and continue processing If it could not be acquired, continue trying until the lock can be acquired. match resp { Ok(_) => break, Err(SdkError::ServiceError(e)) => match e.err() { PutItemError::ConditionalCheckFailedException(_) => { warn!( "Failed to acquire lock, retrying, remaining time: {:?}", require_lock_timeout - start.elapsed() ); thread::sleep(Duration::from_secs(2)); continue; } _ => return Err(format!("{:?}", e).into()), }, Err(e) => return Err(e.into()), } } return Ok(Self { dynamodb_client, output_bucket_name, s3_client, table_name, input_bucket_name: task.bucket_name.clone(), input_object_key: task.object_key.clone(), output_object_key: task.object_key.clone(), }); } } The process to release the lock is implemented as shown below, and the lock is released by updating the processing flag to false. impl S3Lock { pub async fn free(self) -> Result<(), Error> { // Release DynamoDB lock // Only update the processing flag self.dynamodb_client .update_item() .table_name(self.table_name) .key("id", AttributeValue::S(self.input_object_key)) .update_expression("SET processing = :false") .expression_attribute_values(":false", AttributeValue::Bool(false)) .send() .await?; Ok(()) } } Since I want to force a lock when touching an S3 object, I have created a method for manipulating S3 objects in S3Lock^[I feel that reusability will be lower if I create it in S3Lock, but for simplicity I will define it in the same structure. Though there seems to be a better way...] impl S3Lock { pub async fn read_input_object(&self) -> Result<Vec<u8>, Error> { // Acquire S3 object let object = self .s3_client .get_object() .bucket(&self.input_bucket_name) .key(&self.input_object_key) .send() .await?; let body = object.body.collect().await?.to_vec(); Ok(body) } pub async fn write_output_object(&self, buf: Vec<u8>) -> Result<(), Error> { // Save S3 object let byte_stream = ByteStream::from(buf); self.s3_client .put_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .body(byte_stream) .send() .await?; Ok(()) } pub async fn delete_output_object(&self) -> Result<(), Error> { // Delete S3 object self.s3_client .delete_object() .bucket(&self.output_bucket_name) .key(&self.output_object_key) .send() .await?; Ok(()) } } Trying it Out Let’s try using the sample code. I need to prepare an image that I want to convert to grayscale, so this time I will use Hyogo Prefectural Park Awaji Hanasajiki^[a beautiful flower garden on Awaji Island https://awajihanasajiki.jp/about/] . I took the photo. I will use the image from https://awajihanasajiki.jp/about/ . Infrastructure First, use terraform apply to build the AWS infrastructure. Then, clone the GitHub repository and implement the following command. cd terraform # Modify variables.tf and provider.tf appropriately. terraform init terraform apply ![sample code architecture diagram. Convert the image file uploaded to the S3 bucket to grayscale on Lambda and save it to the S3 bucket for output. Use DynamoDB to implement lock process.](/assets/blog/authors/uehara/2024-12-02-how-to-gurantee-s3-event-order/architecture.svg =600x) Upload the Image File to the S3 Bucket If infrastructure is completed, upload the image file to the input S3 bucket. The Lambda function process begins when the upload finishes, and the item is added to the DynamoDB table. The image file is saved in the output S3 bucket when the process finishes, and the processing flag of the DynamoDB item is set to false. You can see the image added to the output S3 bucket and converted to grayscale. Delete the Image File from the S3 Bucket When an object is deleted from the input bucket, it is also deleted from the output bucket. Confirm that Exclusive Processing is Working When the processing flag of an item added to DynamoDB is set to true, processing will wait even if an S3 event that should be processed arrives. To confirm this behavior, let's intentionally set the DynamoDB processing flag to true and upload a file with the same name. If you look at CloudWatch Logs, you can see that it waits for other processes to complete without processing newly generated S3 events. The log message will indicate that Lambda is waiting. Setting the DynamoDB processing flag back to false will restart processing. When the DynamoDB item's processing flag is set back to false, the log will indicate that processing is completed. Thanks to exclusive processing, the order of processing is guaranteed even if delete and upload events occur at approximately the same time. Conclusion This time, I introduced sample code for image processing that uses a sequencer to guarantee the order of S3 events. To guarantee the order of S3 events, you need to utilize a sequencer to compare the order of events. I implemented the sample code in Rust as my own hobby, but it should be possible to implement something similar in other languages. Please feel free to use this as a reference.
Introduction Hello, I'm Shimakawa, a member of the Cloud Infrastructure Group. The Cloud Infrastructure Group is responsible for everything from designing to operating the company's entire infrastructure, including AWS. As generative AI adoption grows across various products in our company, the Cloud Infrastructure Group has been actively supporting these initiatives. In this article, I’ll share my experience building an Amazon Bedrock Knowledge Base using Terraform. I’ll also touch on the RAG Evaluation announced at re:Invent 2024. Configuration Here is the architecture we will be building. We will use OpenSearch Serverless as the Vector store for Amazon Bedrock Knowledge Base, and specify S3 as the data source. Building with Terraform The directory structure is as follows. I will explain each file’s content in detail. The Terraform version used in this setup is 1.7.5 . $ tree . ├── aoss.tf # OpenSearch Serverless ├── bedrock.tf # Bedrock resources ├── iam.tf # iam ├── s3.tf # S3 for bedrock ├── locals.tf # variable definitions ├── provider.tf # provider definitions └── terraform.tf # Backend settings, etc. This section defines the variables. locals { env = { environment = "dev" region_name = "us-west-2" sid = "test" } aoss = { vector_index = "vector_index" vector_field = "vector_field" text_field = "text_field" metadata_field = "metadata_field" vector_dimension = 1024 } } We specify the AWS provider, the OpenSearch provider version, and the S3 backend to store the tfstate. The S3 bucket used here was created manually and is not included in this Terraform code. terraform { required_providers { # https://registry.terraform.io/providers/hashicorp/aws/ aws = { source = "hashicorp/aws" version = "~> 5.0" } opensearch = { source = "opensearch-project/opensearch" version = "2.2.0" } } backend "s3" { bucket = "***-common-bucket" region = "ap-northeast-1" key = "hogehoge-terraform.tfstate" encrypt = true } } This defines the AWS and OpenSearch providers. The OpenSearch provider is used to create and manage indexes. provider "aws" { region = local.env.region_name default_tags { tags = { SID = local.env.sid Environment = local.env.environment } } } provider "opensearch" { url = aws_opensearchserverless_collection.collection.collection_endpoint aws_region = local.env.region_name healthcheck = false } Create OpenSearch Serverless resources and an index. I referred to Deploy Amazon OpenSearch Serverless with Terraform . For this setup, the security policy is set to public, but ideally, access should be restricted using a VPC endpoint. data "aws_caller_identity" "current" {} # Creates a collection resource "aws_opensearchserverless_collection" "collection" { name = "${local.env.sid}-collection" type = "VECTORSEARCH" standby_replicas = "DISABLED" depends_on = [aws_opensearchserverless_security_policy.encryption_policy] } # Creates an encryption security policy resource "aws_opensearchserverless_security_policy" "encryption_policy" { name = "${local.env.sid}-encryption-policy" type = "encryption" description = "encryption policy for ${local.env.sid}-collection" policy = jsonencode({ Rules = [ { Resource = [ "collection/${local.env.sid}-collection" ], ResourceType = "collection" } ], AWSOwnedKey = true }) } # Creates a network security policy resource "aws_opensearchserverless_security_policy" "network_policy" { name = "${local.env.sid}-network-policy" type = "network" description = "public access for dashboard, VPC access for collection endpoint" policy = jsonencode([ ###References for using VPC endpoints # { # Description = "VPC access for collection endpoint", # Rules = [ # { # ResourceType = "collection", # Resource = [ # "collection/${local.env.sid}-collection}" # ] # } # ], # AllowFromPublic = false, # SourceVPCEs = [ # aws_opensearchserverless_vpc_endpoint.vpc_endpoint.id # ] # }, { Description = "Public access for dashboards and collection", Rules = [ { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ] }, { ResourceType = "dashboard" Resource = [ "collection/${local.env.sid}-collection" ] } ], AllowFromPublic = true } ]) } # Creates a data access policy resource "aws_opensearchserverless_access_policy" "data_access_policy" { name = "${local.env.sid}-data-access-policy" type = "data" description = "allow index and collection access" policy = jsonencode([ { Rules = [ { ResourceType = "index", Resource = [ "index/${local.env.sid}-collection/*" ], Permission = [ "aoss:*" ] }, { ResourceType = "collection", Resource = [ "collection/${local.env.sid}-collection" ], Permission = [ "aoss:*" ] } ], Principal = [ data.aws_caller_identity.current.arn, iam_role.bedrock.arn, ] } ]) } resource "opensearch_index" "vector_index" { name = local.aoss.vector_index mappings = jsonencode({ properties = { "${local.aoss.metadata_field}" = { type = "text" index = false } "${local.aoss.text_field}" = { type = "text" index = true } "${local.aoss.vector_field}" = { type = "knn_vector" dimension = "${local.aoss.vector_dimension}" method = { engine = "faiss" name = "hnsw" } } } }) depends_on = [aws_opensearchserverless_collection.collection] } Create the Knowledge Base and data source. data "aws_bedrock_foundation_model" "embedding" { model_id = "amazon.titan-embed-text-v2:0" } resource "aws_bedrockagent_knowledge_base" "this" { name = "test-kb" role_arn = iam_role.bedrock.arn knowledge_base_configuration { type = "VECTOR" vector_knowledge_base_configuration { embedding_model_arn = data.aws_bedrock_foundation_model.embedding.model_arn } } storage_configuration { type = "OPENSEARCH_SERVERLESS" opensearch_serverless_configuration { collection_arn = aws_opensearchserverless_collection.collection.arn vector_index_name = local.aoss.vector_index field_mapping { vector_field = local.aoss.vector_field text_field = local.aoss.text_field metadata_field = local.aoss.metadata_field } } } depends_on = [iam_role.bedrock] } resource "aws_bedrockagent_data_source" "this" { knowledge_base_id = aws_bedrockagent_knowledge_base.this.id name = "test-s3-001" data_source_configuration { type = "S3" s3_configuration { bucket_arn = "arn:aws:s3:::****-dev-test-***" ### Masked bucket name } } depends_on = [aws_bedrockagent_knowledge_base.this] } Set the service role that bedrock will use. resource "aws_iam_role" "bedrock" { name = "bedrock-role" managed_policy_arns = [aws_iam_policy.bedrock.arn] assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = "sts:AssumeRole" Effect = "Allow" Sid = "" Principal = { Service = "bedrock.amazonaws.com" } }, ] }) } resource "aws_iam_policy" "bedrock" { name = "bedrock-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = ["bedrock:InvokeModel"] Effect = "Allow" Resource = "*" }, { Action = [ "s3:GetObject", "s3:ListBucket", ] Effect = "Allow" Resource = "***-dev-test-***" ### ARN of the S3 bucket that was }, { Action = [ "aoss:APIAccessAll", ] Effect = "Allow" Resource = "arn:aws:aoss:us-west-2:12345678910:collection/*" }, ] }) } Create an S3 bucket for use with Bedrock. Also, configure CORS. Below is a reference image of the error. resource "aws_s3_bucket" "bedrock" { bucket = "***-dev-test-***" ### Masked bucket name } resource "aws_s3_bucket_cors_configuration" "this" { bucket = aws_s3_bucket.bedrock.id cors_rule { allowed_headers = ["*"] allowed_methods = [ "GET", "PUT", "POST", "DELETE" ] allowed_origins = ["*"] } } Execution Use terraform apply to create all resources at once. Verifying the Created Resources Check that the Knowledge Base has been created in Bedrock and that the data source is available. Next, verify that an OpenSearch collection has been created and that an index has been set. Testing the Knowledge Base Upload some sample text to S3 to use as a data source. Dogs like meat. Cats like fish aws s3 cp ./test001.txt s3://[S3 Bucket Name]/test001.txt Next, synchronize the data source. Now, let’s test it by asking a question. (Using Claude 3.5 Sonnet for the Prompt) The system correctly retrieves answers from the provided text while refraining from answering questions about information that is not present. This was a brief overview of setting up a Knowledge Base and OpenSearch Serverless using Terraform. Trying out RAG Evaluation Next, we will test the RAG evaluation announced at re:Invent 2024 on the Knowledge Base we created. @ card Preparation First, prepare a dataset file in JSONL format for evaluation. This file contains prompts with their expected answers. {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"Cats' favorite food is fish."}]}],"prompt":{"content":[{"text":"What do cats like? {"conversationTurns":[{"referenceResponses":[{"content":[{"text":"Dogs’ favorite food is meat."}]}],"prompt":{"content":[{"text":"What do dogs like? Since the evaluation references data from S3, upload the dataset file to an S3 bucket. aws s3 cp ./dataset001.txt s3://[S3 Bucket Name]/datasets/dataset001.txt Creating a Job Next, we will create a job. While this can be done through the management console, this time, I executed it using the CLI. @ card aws bedrock create-evaluation-job \ --job-name "rag-evaluation-complete-stereotype-docs-app" \ --job-description "Evaluates Completeness and Stereotyping of RAG for docs application" \ --role-arn "arn:aws::iam:<region>:<account-id>:role/AmazonBedrock-KnowledgeBases" \ --evaluation-context "RAG" \ --evaluationConfig file://knowledge-base-evaluation-config.json \ --inference-config file://knowledge-base-evaluation-inference-config.json \ --output-data-config '{"s3Uri":"s3://docs/kbevalresults/"}' You need to specify the JSONL file and the destination for saving the results in knowledge-base-evaluation-config.json . Checking the Job After waiting about 15 to 20 minutes, the job was completed, so I checked the results. I started by reviewing the summary. The responses were almost exactly as expected, so there wasn’t much surprise. However, the Correctness and Completeness scores were both 1, indicating that the system performed as expected. The only exception was the Helpfulness score, which was 0.83. When I checked the evaluation comments, it stated: “The answer is neither particularly interesting nor unexpected, but in this context, it doesn't need to be.” I think the fact that this context does not have to be the case is what is causing the score to drop. Final Thoughts Our company has been increasingly integrating generative AI, including Amazon Bedrock, and its real-world applications are expanding. Moving forward, I plan to explore more features and ensure we are well-prepared to meet project requirements. I hope this blog post serves as a useful reference. Thank you for reading!
AIが急速に進化する2025年、エンジニアにとって「AIをどう活用するか」が重要なスキルとなってきました。 しかし、AIを効果的に活用するには、適切なプロンプト(指示の出し方)を理解する必要があり、そのためには 慣れや知見 が不可欠です。 そこで、AIとコーディングを行う最初のステップとして、今回のテーマである TDD(test-driven development: テスト駆動開発) と AI を活用した開発を紹介します。 TDD × AIのメリット ✅ 実装コストが激減!エンジニアは"テストを書くだけ"でOK テストを書くだけで、複雑な指示もプロンプトも不要。あとはAIが自動でコードを生成。 ✅ 開発速度が爆速化!細かな往復が劇的に減少 TDDのステップごとにAIが即座にコードを生成するので、開発効率が驚異的にアップ。 ✅ 圧倒的なコード品質!テストでしっかりAIの暴走をコントロール 明確なテストでAIの生成コードをしっかり制御。結果的にバグの少ないコードになる。 TDDとは? 大前提となるTDD(Test-Driven Development: テスト駆動開発)について簡単に説明します。 https://www.amazon.co.jp/dp/4274217884 「TDD(Test-Driven Development: テスト駆動開発)」はKent Beck氏が👆の書籍で20年以上前に提唱した手法です。 上の図の「まずテストを書いて、次にテストを通過する実装をする、最後にリファクタリング」というシンプルなサイクルを繰り返すことで、品質の高いコードを生み出すことができます。 また、テストが実装の起点になるので、 テスタブルな構造を担保しつつ、実装を進めることが出来ます。 TDDを実践する :::message VSCodeの GitHub CopilotのAgentモードを使って実施しています。 ::: https://code.visualstudio.com/docs/copilot/copilot-edits#_use-agent-mode-preview 今回の方法では、エンジニアがテストを書き、生成AIが実装・リファクタリングを担うことで、コード品質を維持しつつ効率的に開発を進めます。 今回は パスワードのバリデーション を例に、TDD × AIの流れを説明していきます。 まずは、 文字数のバリデーション から始めていきましょう。 :::message alert 最初からすべての仕様を詰め込まず、細かいステップで進めることが重要です! ::: ステップ1: 文字数バリデーションテストの作成 対象になるPasswordバリデーションのクラスとテストファイルを用意しましょう。 この段階ではどちらも空のファイルで問題ありません。 ファイルが作成できたらそのままPasswordバリデーションクラスを書きたくなるところですが、今回はTDDなのでテストを書いていきます。 @Test("パスワードの文字数が8文字以上16文字以下でバリデーションが成功すること") func testPasswordLengthValidation() async throws { let sut = PasswordValidator() let validPasswordShort = "Valid123" let validPasswordLong = "ValidPassword123" let invalidPasswordShort = "Short01" let invalidPasswordLong = "ThisPasswordIsWay" #expect(sut.validate(validPasswordShort) == true) #expect(sut.validate(validPasswordLong) == true) #expect(sut.validate(invalidPasswordShort) == false) #expect(sut.validate(invalidPasswordLong) == false) } この段階では PasswordValidator クラスや validate メソッドが未実装のため、当然エラーが発生します。 次に、AIにプロンプトを入力して実装を依頼します。 ![input prompt](/assets/blog/authors/HiroyaHinomori/2025_03_tdd_x_ai_02.png =500x) すると、数十秒後にAIが実装を生成してくれます。 struct PasswordValidator { // パスワードの長さの制約を定数として定義 private let minimumPasswordLength = 8 private let maximumPasswordLength = 16 /// パスワードが有効かどうかを検証します /// - Parameter password: 検証するパスワード /// - Returns: パスワードが有効な場合はtrue、そうでない場合はfalse func validate(_ password: String) -> Bool { let passwordLength = password.count return passwordLength >= minimumPasswordLength && passwordLength <= maximumPasswordLength } } 次に、数字・文字・記号のミックス判定を追加してみます。 ステップ2: 数字、文字、記号の含有バリデーションの追加 @Test("パスワードの文字列の中に数字、文字、記号が含まれている場合にバリデーションが成功すること") func testPasswordCharacterValidation() async throws { let sut = PasswordValidator() let validPassword = "ValidPassword12!" let invalidPasswordNoNumber = "InvalidPassword!" let invalidPasswordNoCharacter = "1234567890!" let invalidPasswordNoSymbol = "InvalidPassword1" #expect(sut.validate(validPassword) == true) #expect(sut.validate(invalidPasswordNoNumber) == false) #expect(sut.validate(invalidPasswordNoCharacter) == false) #expect(sut.validate(invalidPasswordNoSymbol) == false) } この段階でエラーが発生しますが、気にせずテストケースをAIに渡して実装を生成します。 struct PasswordValidator { // パスワードの長さの制約を定数として定義 private let minimumPasswordLength = 8 private let maximumPasswordLength = 16 /// パスワードが有効かどうかを検証します /// - Parameter password: 検証するパスワード /// - Returns: パスワードが有効な場合はtrue、そうでない場合はfalse func validate(_ password: String) -> Bool { let passwordLength = password.count guard passwordLength >= minimumPasswordLength && passwordLength <= maximumPasswordLength else { return false } let hasNumber = password.rangeOfCharacter(from: .decimalDigits) != nil let hasLetter = password.rangeOfCharacter(from: .letters) != nil let hasSymbol = password.rangeOfCharacter(from: .symbols) != nil || password.rangeOfCharacter(from: .punctuationCharacters) != nil return hasNumber && hasLetter && hasSymbol } } ここまで 10分もかからず 実装できました。 通常のコーディングではプロンプトに色々な条件や仕様を伝える必要がありますが、この方法では テスト条件を満たした実装をして というお願いをするだけです。 実装内容に関してはTestに全て書かれているので、複雑なプロンプトの指定はほぼ不要になります。 AIとのコミュニケーションをさらに効率化するには 実装のルールや制約を事前に「copilot-instructions.md」に書いておけば、毎回AIに細かい指示を伝える必要もありません。 日本語で返答してください。 ### コーディングルール - テストはswift-testingを使用してください。 - 実装には基本的にマジックナンバーは使わないこと - DRYの原則に則って実装してください - KISSの原則に則って実装してください - YAGNIの原則に則って実装してください AI時代に活躍するエンジニアになるために AIは万能ではありません。しかし、だからといって諦めるのはとてももったいないです! 「AIが得意なこと」と「人間が担うべきこと」を冷静に見極めること が重要です。 「TDD × AI」で、AIコーディングの癖を理解し、次世代の開発スピードと品質を手に入れましょう🚀
1.start-(概要) 初めまして! KINTOテクノロジーズのCloud Infrastructure GでInfrastructure Architectを担当している劉(YOU)です。 今年の1月に入社して、techblogには初執筆なのでこれからよろしくお願いします! AWSの認定は23年10月のSAAを始じめ、25年2月のMLAを最後に、1年4ヶ月でやっとAWS認定12冠を取りました。せっかくなので、AWS認定12冠を達成しながら感じた個人的な意見と情報を共有します。 先に話しますと、AWS認定12冠とは、AWS社が主催する資格認定を全て取得したことを指します。その基準は毎年変更されるんですけど、 https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-all-certifications-engineers/ AWS JAPAN APN ブログ からその詳細を前もって発表して、選出された方々を表彰します。2024年は1,222名が正式に「AWS All Certifications Engineers」として認められています。公式記事では、 ”AWS 認定資格を全て取得し・維持する事” は AWS のテクノロジーを正しく理解し、お客様に信頼性の高い最新の技術をご案内できる基盤をお持ちだという証になります。 と述べています。 AWS以外でも、AzureとかGCPとかクラウドサービスを提供している会社は多いですが、クラウドサービスの量と質、高い占有率からできる汎用性、圧倒的なアップデートスピード、どう考えてもAWSは クラウド業界の標準だと言い切れます。 そして、最近AIの注目が高くなることと伴い、クラウドの重要性も上がっています。「クラウドとかAIとか、私と関係ないだろう」と思う方がいらっしゃるかも知れませんが、現時点でほぼ全ての業務にパソコンを使うようになった事と同じく、もうじきAIを日常で使うことになる時代がやって来ると思います。 AIその物になるモデルとモデルを動かすためのコンピュータ、それが簡単に提供されるプラットフォームがクラウドであり、時代を追いつくためにもクラウドを習得する必要があると言うことです。それで、AWS及びクラウドを勉強するために資格はなぜ必要なのか? その答えを次から説明して行きます。 2.status-(現状) 残念ながら、資格はあってもなくてもクラウド活用に大きい影響を与えません。例を挙げると、今からクラウドを英語だと見なして考えてください。英語を活用するためにTOEICを準備して、高得点をしました。それが本当の意味で英語を上達することだと思いますか? どれだけ試験のテクニックが良くても、単語と文法などを暗記しても、実際に英語を要求される時に使いこなせなかったら無駄です。 しかし、TOEICが英語の実力向上に役立たないって言ったらそれは違うと断言できます。意味が無かったら数々の大学・企業からTOEICの点数を基準として評価する訳がないです。ビジネス英語力を点数に換算する試験がTOEICだからこそ、ただの点数ではなくて能力として認められてることになります。 そう言う意味で、AWS認定12冠はクラウドに対して分かりやすい天井です。 実体化されていない知識の塊を資格という形で見えるようにします。こう言う見える化によって得られる効果を整理しますと、 明確な目標設定(Goal):AWS社が証明してくれるロードマップを従った認定なので、階段式でスケジューリング可能 努力の源(Motivation):受験日を決めて勉強することで、頑張れる環境が作られる 知識の保証(Knowledge):資格を取るための最低限の知識が保証される 振り返り(Remind):そもそもクラウドに詳しい人であっても、資格で要求される知識を点検することができる キャッチアップ(Discovery):試験はアップデートを沿って変わっていくので、触れる機会がない情報を勉強することができる になります。 逆に英語で置換して見ても、違和感なく受け入れられる内容ではないでしょうか?結局、資格を取ったらクラウド力が上がる、クラウドやりたいから資格取ろうではなく、鍛えること、それ自体に意味があると思います。 AWS認定のこれから 続いて、私が一年ちょっと超える時間をAWS認定準備しながら感じた「これからAWS認定はどうなる?」を突っ込んで行きます。 :::message あくまでも、 根拠のない自分勝手の推測なので、AWSで公式の何かがあることではありません。 引用の時にはご注意お願いします。 ::: 私がAWS認定の準備し始めた時期は、22年ChatGPTが流行ってAIに対する関心が大きくなってる状況でした。AWSもAIを中心にするサービスをどんどん出して、24年から資格の構成を大きく変更させました。 既存のスペシャル資格三つを24年4月で削除し、 AWS Certified Data Analytics – Specialty (DAS) AWS Certified Database – Specialty (DBS) AWS Certified: SAP on AWS – Specialty (PAS) DASとDBSを代行するために資格が24年3月に登場、 AWS Certified Data Engineer – Associate (DEA) その後、Amazon QとかAmazon Bedrockなど新しく出たAIサービスと、Amazon Sagemaker周りの強化されたAIサービスのロードマップを提示するために24年10月、 AWS Certified AI Practitioner (AIF) AWS Certified Machine Learning Engineer – Associate (MLA) 結構、大変革だったので試験を準備している個人の立場でもちょっと困りました。勉強してた内容が大幅に変更されることと同然だったので、先に計画してた試験日程も変えるしか無かったです。これからも、今後の技術トレンドであるAIを中心に変わっていくことは確かです。 あくまでも推測で過ぎませんが、変更が起きる可能性が高い資格は AWS Certified Machine Learning Engineer – Specialty (MLS) だと思います。 MLSが最後にアップデートされたのは22年7月のなのでAIF, MLAに比べたら内容が古くなっています。現状のSpecialtyのままアップデートされることもありますが、新しいprofessional資格に改編される可能性が高いです。 その理由としては、既存のパスがPractitioner→Associate→Professionalに繋がる仕組みだからです。 ^1 同様にAIF → MLA資格の次に来るProfessional資格が必要になります。単純にスペシャリティがプロフェッショナルになって最新化される…ことはAWS側が決める所ですが、そうなったらDEAの上位資格も想定するしかないです。 (仮)AWS Certified Machine Learning Engineer – Professional (MLP) (仮)AWS Certified Data Engineer – Professional (DEP) これが普通に考えられる予測ですが、これはこれで問題が生じます。AWS社の認定は12冠を象徴として維持してるそうなので、二つが増えれば資格の数が13個を超えてしまうことです。 それを回避する方法があって、まずは上記の資格が増やされる分、曖昧になったSpecialtyを減らすことです。 (仮)AWS Certified Security – Specialty (SCS) AWS Certified Advanced Networking – Specialty (ANS) SCSとANSはもう消えている他のスペシャリティとは違って、プロフェッショナルから携わった知識を深く振り込む内容で構成された認定です。両方とも出る内容が60%以上がプロフェッショナルと重複しつつ、SCSは組織全体のセキュリティを重視し、ANSはオンプレミスとのネットワーキングが主になっています。その中、現状のままではやむを得ない欠陥があります。 SCSはAIのアップデートされなかったので、AIに対するセキュリティの内容が含まれていません。AIの発展が早くなっていながら、AIに対するセキュリティーやコンプライアンスも大事にされてるので、SCSにAIの内容が入れるか、各プロフェッショナルに溶かすかの問題です。すでにAIのトレンドに合わせて統廃合されたスペシャリティ認定が多いので後者がなり得ると思ってます。 ANSの場合、SCSと似ているポジションですが、ネットワークはAIができてもAWS内部に限ってはそんなに差がありません。OpenAIを使うにはAzureを、Geminiを使うにはGCPを、その他のクラウドベンダが運営しているAIを使うにはマルチクラウドが確かに必要になりますが、自社製品じゃないとAWS社が積極的に支援しないスタンスなので、マルチクラウドに関係する認定のアップデートは今の所はないです。代わりに、アンチクラウドの流れからハイブリッドクラウドが浮上していますので、ANS認定の仕組みは維持すると見込んでいます。 とにかく、資格の数を減らしたら12冠は担保できるのでこう言うやり方もあることと、二つのプロフェッショナルを増やさずにDevOps Engineerみたいに一つに納める事もあります。 (仮)AWS Certified MLOps Engineer – Professional (MOP) AWSで紹介している MLOps は、「ML アプリケーション開発 (Dev) と ML システムのデプロイおよび運用 (Ops) を統合する ML カルチャーとプラクティスです」だと記述してます。 ^2 これはMLに関する全体作業を意味してるので、まさにDEAで扱うデータエンジニアリング、データ分析を通して、AIF→MLA→MLSで使われる機械学習の全体を活用できますので、新しいプロフェッショナルがただ一つだけ必要だったらこれで通用できる仕方だと思ってます。 AWS認定の類型 そして、認定の種類だけではなく、試験の類型にも変化が起きています。 既存の試験はSOAのラボ試験が中止になってから、選択肢で正解を選ぶ形式のみが評価方式でした。客観的に定量評価ができる要素は長所ですが、実装とは関係が深くない知識になることも否定できません。AWS社もこれを意識しているかなと思いまして、AIFとMLAから新しい出題形式が出ました。 AIF試験ガイド を見ると、 並べ替え: 指定されたタスクを完了することを目的とした 3~5 つの答えのリストが提示される。設問に対する点数を得るには、正解を選択し、正しい順序に並べる必要がある。 内容一致: 3~7 つのプロンプトのリストと一致する答えのリストが提示される。設問に対する点数を得るは、すべてのペアを正しく一致させる必要がある。 ケーススタディ : 1 つのシナリオに、そのシナリオに関する設問が 2 つ以上含まれている。ケーススタディの各設問のシナリオは同じである。ケーススタディの各設問は個別に採点される。ケーススタディでは正解した設問ごとに点数が得られる。 上記の三つの類型は私の試験にも多い数ではありませんが、試験ガイドで述べている事と同じく出題されました。問題のレベルとしては選択肢の問題と同様で、AWS試験の秘密保証のため問題の詳しい形式は言えませんが、私が感じた出題形式の評価はこうなります。 並べ替えと内容一致は、選択肢の類似性から正解を類推することが出来なくなりました。本当に実施するべきの手順だったり、提示される単語や説明を結び付く、問題から要求する内容を熟知しないと解けない形です。 ケーススタディの基本は設問=選択肢なんですが、単一のケースで複数の問題を提示するやり方です。ここはケースを多角的に接近することもできるし、長問・多問からは知識の応用より読解力を求められる状況があるから、それが解消されます。我らの現実世界もケースがあったら一問一答で絶えず、ケースごとにシミュレーションすることが一般的なので、このケーススタディは受験者としていい類型だと思います。 AWS社は認定に関してこれからも出題形式もそうですし、SOAのラボみたいに「本当に実装できますか?」を目指して変化して行くと考えられます。こういう変更は単発で起きることではなく、連続的に他の認定にも反映されるので、AWS試験を準備していらっしやる方々はキャッチアップして備えて行きましょう! 3.stance-(心構え) 私も周りの人達と話す時に職種不問でよく出る話題なんですが、 「AWSとは関係ない仕事してるけど、これ勉強して本当に使える?」 「AWS資格取るとしたらどこから始まる?」 「何をネタに勉強してる?」 と質問を貰います。私はクラウドエンジニアとして資格を取っていまして、クラウドで業務をするための知識が元々必要になります。実務で使っているからこそ、クラウドに関わる頻度が他の人と比べて非常に多いです。その為、資格を取得したらすぐにクラウドに関わる業務ができるようになるとは言えません。 今まで全然使っていなかったのに、資格を取ったらすぐ使えるようになることはあんまりないです。資格は言わば、何らかのクーポンのような物です。1万円以上に限って10%割引をしてくれるガソリンスタンドのクーポンができたとしても、車がそもそもないから無用、クーポンの店が遠いから行けない、10%割引額を達成するお金がない、クーポンは色んな理由で使えきれないケースがいっぱいです。こう見たらクーポンを使うための条件は明確です。 自分・知り合いが車を持っていたり、車を持つ計画がある。 クーポンを使えるガソリンスタンドとの距離が近い。 クーポンを使える余力がある。 自分自身にそのクーポンが欲しい理由が整えているかを確認してください。 言い換えますと、「資格を活かせるように動けますか?」という意味です。クーポンを手に入れたとして、車が自動で生成されたり、ガソリンスタンドが勝手に家の前にできたり、クーポンを使えるお金が急に現れたり、そんな出来事は現実では起こらないですよね?クラウドもAWSも同じです。クラウドが己の業務と当てはまらない方々、例えば ITと関係ないビジネス系 インフラは専門ではない開発系 オンプレに特化したインフラ系 が挙げられます。 車は高いから買えない人には何を推奨しますか?カーリース・サブスクは月額払いだけできれば車を使えることができます。これがITではクラウドです。私は「技術を借りる」がクラウドの本質だと思ってます。技術を教わることが高かったら借りればいい話です。領域によって詳しくは違うかも知れませんが、知っている事だけで技術の視界が覆ると確信してます。 ガソリンスタンドが遠くて行けないって言ったら、それはそれで十分です。無理矢理に行くことまではないです。ただし、通勤途中で寄り道に行けそうな距離だったらどうしますか?開発系の方にとって、クラウドはそんなに遠い所にある訳ではありません。むしろ、視線を少し横に移すだけで活用できることが溢れるているかもです。 最後に、どれだけお得になるクーポンを持っていても、使わずにいたらない事と同然です。車をすでに持っていて、ガソリンスタンドがすぐ前に出来ても、いつも通っていたガソリンスタンドだけ利用してたらクーポンは使えません。1万円を前払いできないとか、他社のガソリンカードがあるとか、検証できない店は行きたくないとか、それぞれ抱えた理由は千差万別だと思ってます。しかし、否定できない事実はインフラ系の方々は、他の誰よりもクラウドを始めることに特化していることです。オンプレだけやってたら、クラウドはIaaS(Infra as a Service)とかPaaS(Platform as a Service)として提供していて馴染みがないと思われます。それでも、基本構造はインフラの知識の上で作られているから、ビジネス系と開発系に比べれば極めて簡単です。「クラウドまでやれる余力がない」ではなく、「クラウドまでやる余力を作ってみよう」はどうですか? 私もキャリアを開発系でスタートしていましたが、個人的に勉強したクラウドの知識があってクラウド業務も任されました。その後は資格を取りながらクラウド職に転向もできました。多分、私がやろうとしてる事とやっている仕事に限って集中していたらできなかったと思います。AWS認定12冠の達成もチャンスを増やす感覚であります。資格取得で得た知識のうち、KTCに合流してから実践で使える知識は50%前後程度です。しかし、使ってない50%の知識もこれから活かせるように頑張っています。KTCはAIファーストを今年の目標に決めているので、私もAI活動を盛り上げて行くつもりです。 AIファーストと繋ぐKTCの今年目標に興味がある方は、弊社副社長の景山が記事を掲載しているのでぜひ読んでみてください。 https://blog.kinto-technologies.com/posts/2024-12-25-LookBack2024/ 公式から推薦する AWS認定パス もありますので、ここもご参考お願いします! 4.strategy-(攻略法) 勉強法は私以外にもたくさんの方が推薦してくれていると思いますので、観点を変えてAWS認定を効率よく攻略する戦法をテーマにお話しします。 正攻法 真面目に勉強する方法は極簡単です。前述してた AIF試験ガイド で出ている内容をゼロから習得することです。ここはベース知識がなくて誠実に勉強して行きたい方や、受験日程に焦らずにやりたい方にお勧めします。 全部5段階でやってます。 情報収集:検索、SNS、YouTube、Blogなどを参考して己が好むソースを探す。 ソース決定:下記のソースの中で、最も自分に適することを決めます。 AWS公式 AWS社で提供してくれるドキュメントは最新反映されてて、信用度も高いし、内容も上品です。私も他のやり方で勉強しても公式はいつも参照します。一部無料のAWS Traning Centerの活用もすごく助けになりますので、ぜひ活用してください。有料の物は使っていませんが、下で紹介する外部学習サイトと似てる効果だと感じます。 YouTube 無料情報の量は一番多いですが、アップローダーさんによって質も量もバラバラですし、最新化も望めない短所があります。でも、映像や音声で学習できることと言語の縛りさえなければ短所も薄くなります。聞いてみていつでもお気軽にやめる事もできることが嬉しいです。 書籍 アナログ勉強法が好きだったら推しです。ターゲットを絞って情報の質を保証することが本の魅力です。買う前にある程度内容把握もできるし、一冊で求める内容が集約されていることが長所です。しかし、情報の最新化には弱いことは変化が早いAWS認定には向いてないことを注意してください。即時、勉強に入って認定の更新前に試験を受ける事じゃなかったら避けた方がいいです。 外部学習サイト Udemyみたいに有料で提供するソースを記述します。お金を払う分、YouTubeを超える質を持ちながら最新化も早いです。端的に言うと、YouTubeと書籍の長所を合体させた感じなので、個人的に愛用しています。気を付ける所はAWS認定の場合、資格同士に被る内容が結構ありますので目次をよく見て購入してください。 学習開始:試験によって学習時間は違いますが、2-3ヶ月を想定して勉強することが最適でした。 検証:AWS公式とかでやってる模擬試験で自己評価 試験:日程を予め取って受験します。オンラインでもオフラインでも受験できますが、一貫的な試験環境のためにもオフラインを推奨します。オフラインでの試験で何かの問題が起こったら、試験主催側が調整してくれるので安心ですし、試験結果もオフライン試験の方が早く届くのでお得です。 速攻法 逆に「私はAWS知っているから、初めから勉強するまでもない。知らない所だけ勉強したい!」と考えてる方や、「資格を早く取って勉強してもいいから、最速で取りたい!」と思ってる方もいます。私も幾つかの試験はそうだったので、コスパ最高の攻略法を話します。 :::message 生成AI:私はPerplexityを使ってましたが、どんな生成AIでも大丈夫です!そして、紹介するプロンプトは記事用の例であって、個人が向いてるプロンプトやり方でしてもOKです。 ::: キーワード整理 サービスの内容を要約してピンポイントだけ知ることができます。 :::details 回答 以下に、AWSの監視サービスであるCloudWatchを中心に、関連するサービス(CloudTrail、Config、WAF)との比較を整理します。また、これらに含まれない他の監視ツールも簡単に触れます。 CloudWatchの整理 概要 Amazon CloudWatchは、AWSリソースやアプリケーションをリアルタイムで監視するためのサービスです。主に次の機能を提供します: メトリクス収集:CPU使用率、ディスクI/O、ネットワークトラフィックなどのリソースデータを収集。 ログ監視:アプリケーションログやシステムログを収集し、分析可能。 アラーム設定:特定の条件(しきい値)に基づいて通知や自動アクションを実行。 イベント管理:特定のイベントをトリガーとして処理を自動化(例:インスタンス起動時の通知)。 主なユースケース リソースのパフォーマンス監視 異常検知とアラート通知 システム全体の稼働状況の可視化 CloudWatchと関連サービスの比較 サービス名 主な目的 特徴 主なユースケース CloudWatch メトリクス&ログ監視 AWSリソースやアプリケーションの稼働状況をリアルタイムで監視可能。 ほぼリアルタイムで通知。 リソース監視、パフォーマンス最適化、異常検知 CloudTrail 操作履歴の記録 誰がいつ何をしたか(APIコール)の履歴を記録。 コンプライアンスやセキュリティ監査向け。 セキュリティ監査、不正操作検出 AWS Config 設定変更の追跡&評価 リソース構成変更を記録し、ルール違反を検出可能。 設定状態をスナップショットで保存。 設定管理、コンプライアンス遵守、トラブルシューティング AWS WAF Webアプリケーション層のセキュリティ対策 SQLインジェクションやDDoS攻撃などからWebアプリケーションを保護。 CloudFrontと連携可能。 セキュリティ強化、攻撃遮断 比較ポイント 目的と対象範囲: CloudWatchはシステム全体の稼働状況を監視。 CloudTrailは操作履歴に特化。 Configは設定変更とルール評価。 WAFはセキュリティ対策専用。 リアルタイム性: CloudWatchはほぼリアルタイムで通知可能。 CloudTrailは遅延が発生する場合あり。 ユースケース重複: udTrailやConfigで記録したデータをCloudWatch Logsに送信し、統合的に管理することも可能。 他のAWS外部監視ツールとの比較 AWS以外でも利用可能な監視ツールには以下があります: ツール名 特徴 メリット デメリット Zabbix オープンソース。 高度なカスタマイズ性 無料で利用可能。 幅広い環境に対応 導入・運用が複雑 Mackerel SaaS型日本製ツール 簡単導入。 複数環境一元管理が可能 AWS専用機能が少ない Site24x7 マルチクラウド対応 ハイブリッドクラウド環境にも対応 AWS専用ツールより細かい設定が難しい 選択基準 AWS専用ならCloudWatchが最適。 マルチクラウド環境ではSite24x7やMackerelが有効。 高度なカスタマイズが必要ならZabbix。 試験準備に役立つポイント 各サービスの役割とユースケースを明確に理解する。 CloudWatchと他サービス(CloudTrail, Config, WAF)の連携例を把握する。 コスト面(従量課金制)やリアルタイム性なども試験問題に出る可能性あり。 必要なら追加質問してください! ::: 試験問題生成 キーワードの概念を分かったら、簡単な問題を生成して自己評価します。 :::details 回答 問題 1: あなたは、EC2インスタンスのCPU使用率が特定のしきい値を超えた場合に通知を受け取りたいと考えています。この目的を達成するためにどのAWSサービスを使用するべきですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch • C. AWS Config • D. AWS WAF 問題 2: セキュリティチームが、過去3か月間に発生したAWSリソースへのAPIコールを追跡し、特定のユーザーアクティビティを調査したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. Amazon CloudWatch • B. AWS CloudTrail • C. AWS Config • D. Amazon Athena 問題 3: ある企業が、AWSリソースの設定変更を追跡し、コンプライアンス違反を検出したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch Logs • C. AWS Config • D. AWS WAF 問題 4:あなたは、WebアプリケーションをSQLインジェクションやクロスサイトスクリプティング(XSS)攻撃から保護したいと考えています。この目的に最適なAWSサービスはどれですか? 選択肢: • A. Amazon CloudWatch Logs • B. AWS WAF • C. AWS Shield • D. AWS Config ::: 速攻法は私が主に使ってる使用法二つだけ説明できなかったんですけど、使用者によって無限にカスタマイズできる生成AI学習法です。紹介した方法以外にも、細かい質問を数十個を投げてリマインドすることも結構やっています。生成AIが参考できる資料をInputに入れたりしたら、もっと豊かなキーワード整理や試験問題を生成してくれます。 私もAIFとかMLAは速攻方を70%割合で使って、1-2周くらいで取ったんで効果は保証します! (1st-art).最初から始まる美学 AWS認定12冠達成して、私が感じた色々を呟いてみました。 気づいてるか知れませんが、実はタイトルにも本文にもトリックを入れました。最初に戻ってみたらすぐ分かると思います。 1.start-(概要) → (1st-art).最初から始まる美学 こんな面倒臭い仕業を入れた理由は、私の1年4ヶ月の12冠の挑戦はスタート(アート)したから得られた一つの絵だと話かったです。どんな結果物が誕生するか、絵を描く時には知らないと思います。小学生の頃、「私の未来を描いて下さい」って聞いて、私が描いた未来は消防士でした。そして、中学生の頃は小説家でした。現在はクラウドエンジニアで全然違う仕事をしています。 そうだとして、私の幼い頃の絵が意味がなかったことでしょうか。私はその絵を描きながら、自分の夢に向き合ったことに意味があったと信じます。私は今、「AWS認定12冠」と言う絵を完成しました。次も新しい絵を描いて行くつもりです。ここtechblogに書いたこの記事も一つの絵になりますし、KTCでの仕事も別の絵になれると思ってます。 記事を読んでいただきありがとうございました!
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Nice to meet you, I’m Tsuyoshi Yamada, an Android engineer at KINTO Technologies. In this article, we’ll share some handy techniques to help you take the first step in migrating an app built with Android View to a Jetpack Compose -based UI or gradually integrating Jetpack Compose into an existing Android View-based UI. 1. Introduction Development of Jetpack Compose , Android’s Declarative UI , was announced a few years ago, and version 1.0 was officially released in July 2021. Many Android developers quickly embraced the concept of Jetpack Compose, harnessing the flexibility and extensibility of Kotlin , which had already become the second officially supported language for Android development after Java. It seems that the adoption of Jetpack Compose in Android app development has been gradually increasing. With the introduction of declarative UI, developers can write UI more intuitively with less code compared to traditional view-based UI, leading to improved development efficiency and productivity. With the recent release of Compose Multiplatform , the scope of Jetpack Compose skills has expanded beyond Android. Moving forward, we might see more libraries being developed exclusively for Jetpack Compose. To bring these benefits to our existing apps, we have started migrating some of our development projects from Android View-based UIs to Jetpack Compose-based UIs. However, traditional view-based UIs rely heavily on procedural elements, making it challenging to adopt Jetpack Compose’s declarative style seamlessly. This article introduces some practical techniques to compensate for elements that may have been lost or become less visible due to Jetpack Compose’s simplified approach. In particular, we’ll cover tracking Composable locations, checking debug information, and ensuring smooth interoperability between Views and Composables—all with the goal of making the transition from a View-based UI to a Jetpack Compose-based UI as seamless as possible. 2. Aligning and Debugging Composables Replacing the existing View expressions with new Composables came with a big challenge—not just figuring out how to represent the same content, but also whether it could be positioned correctly. Even for someone familiar with Android Views, that was a major concern. To address this, we’ll first go over how to align a Composable’s position with a View’s and how to retrieve and check debugging information to assist in this process. 2.1. Checking the position of Composable and View The View , available since Android API level 1, is built on Java’s object-oriented principles. It not only represents rectangular screen elements but also effectively handles containment relationships between Views, interactions, and extending functionality through subclasses. For example, you can log the positions of a single View or multiple Views inside a ViewGroup using the following code: private fun Resources.findResourceName(@IdRes resId: Int): String = try { getResourceName(resId) } catch (e: Resources.NotFoundException) { "?" } fun View.logCoordinators(logger: (String) -> Unit, res: Resources, outLocation: IntArray, rect: Rect, prefix: String, density: Float) { getLocationInWindow(outLocation) rect.set(outLocation[0], outLocation[1], outLocation[0] + width, outLocation[1] + height) var log = "$prefix${this::class.simpleName}(${res.findResourceName(id)})${rect.toShortString()}(${rect.width().toFloat() / density}dp, ${rect.height().toFloat() / density}dp)" if (this is TextView) { log += "{${if (text.length <= 10) text else text.substring(0, 7) + "..."}}" } logger(log) if (this is ViewGroup) { val nextPrefix = "$prefix " repeat(childCount) { getChildAt(it).logCoordinators(logger, res, outLocation, rect, nextPrefix, density) } } } fun View.logCoordinators(logger: (String) -> Unit = { Log.d("ViewLogUtil", it) }) = logCoordinators(logger, resources, IntArray(2), Rect(), "", resources.displayMetrics.density) Here, View$getLocationInWindow(IntArray) is a function that retrieves the top-left coordinates of the Activity’s window where the View is located. If you’re familiar with Android Views, checking how this code works should be pretty straightforward. One thing to keep in mind is that calling these functions directly from Activity$onResume() , for example, won’t work as expected because the View layout isn’t fully set up yet, so you won’t be able to get meaningful information. In most cases, you’ll need to call them from a callback like View$addOnLayoutChangeListener(OnLayoutChangeListener) . A lot of developers might not be sure how to achieve the same thing in Jetpack Compose, and since it’s not easy to check if Composable behaves the same way, the migration process can feel like a hassle. In Jetpack Compose, you can use the Modifier extension function onGloballyPositioned((LayoutCoordinator) -> Unit) to get a Composable's position like this: @Composable fun BoundsInWindowExample() { Text( modifier = Modifier .padding(16.dp) .background(Color.White) .onGloballyPositioned { Log.d("ComposeLog", "Target boundsInWindow: ${it.boundsInWindow()}") }, text = "Hello, World!", style = TextStyle( color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp ) ) } By passing a callback to onGloballyPositioned(...) , you can get the position coordinates every time the Composable’s position updates. In this case, LayoutCoordinator.boundsInWindow() is an extension function that retrieves the top, bottom, left, and right bounds of the Composable’s rectangle in the Activity coordinate system. Getting the positions of all Composables inside a single Composable at once seems tricky for now, but grabbing the position of an individual Composable is easy. Plus, in many cases, you can get position data without worrying about complex lifecycles like Activities. onGloballyPositioned(...) There’s also onPositioned(...) , a callback similar to onGloballyPositioned(...) , which gets called after the relative position within the parent Composable is determined. Besides boundsInWindow() , there are also boundsInRoot() and boundsInParent() , which you can use depending on the situation, but we’ll spare you the details for now. 2.2. Creating a Composable to Check Screen Display Now that we’ve figured out how to get the position in Composable using boundsInWindow() , which is compatible with View$getLocationInWindow(IntArray) , we can log the position changes during testing to check its behavior. By doing this while developing the Composable, we’ll gradually get used to Compose and be able to recreate something similar to View. This method is simple and effective, but since LogCat floods with text, it can be hard to read—especially when dealing with a lot of information. To make things easier, let’s try creating a separate Composable just for checking the screen display. If you create a debug display area in a part of your app just for testing purposes and keep it constantly updated, you won’t have to scramble to find key logs among the endless stream of LogCats. ...Of course, we’ve known this since the Android View days, but actually implementing it always felt like a hassle... I can almost hear the sighs. With Jetpack Compose, a declarative UI , creating such a debug area is much easier with minimal effort: class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) WindowCompat.getInsetsController(window, window.decorView).isAppearanceLightStatusBars = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK != Configuration.UI_MODE_NIGHT_YES setContentView(R.layout.activity_main) findViewById<WebView>(R.id.webView).let { webView -> webView.loadUrl("https://blog.kinto-technologies.com/") } val targetRect = mutableStateOf(Rect.Zero) // androidx.compose.ui.geometry.Rect findViewById<ComposeView>(R.id.composeTargetContainer).let { containerComposeView -> containerComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) containerComposeView.setContent { KtcAdventCalendar2024Theme { ScrollComposable(targetRect) } } } val visibleRect = mutableStateOf(Rect.Zero) val outLocation = IntArray(2) findViewById<View>(R.id.layoutMain).addOnLayoutChangeListener { v, left, top, right, bottom, _, _, _, _ -> v.getLocationInWindow(outLocation) visibleRect.value = Rect(outLocation[0].toFloat(), outLocation[1].toFloat(), outLocation[0].toFloat() + (right - left), outLocation[1].toFloat() + (bottom - top)) } findViewById<ComposeView>(R.id.composeTargetWatcher).let { watcherComposeView -> watcherComposeView.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) watcherComposeView.setContent { KtcAdventCalendar2024Theme { TargetWatcher(visibleRect.value, targetRect.value) } } } } } The app in the sample code is in the middle of being converted to Jetpack Compose. Right now, it’s still using a View with Activity$setContentView(Int) , and we’re introducing Composable within that View by using ComposeView . Here, mutableStateOf(...) is used to share the rectangular position information between View and Composable, allowing us to observe how it behaves on the screen. The screen layout looks like this: We’re working on making the HorizontalScrollView part composable. To help with that, the bottom part of the screen will be used to display debug information: ![Screen composition](/assets/blog/authors/tsuyoshi_yamada/advent-calendar_sample_screen-area.png =252x) The layout XML file for MainActivity as follows: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:layout_marginVertical="48dp"> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/layoutMain" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <WebView android:id="@+id/webView" android:layout_width="0dp" android:layout_height="match_parent" android:layout_marginHorizontal="16dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toStartOf="@id/scrollView" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:ignore="NestedWeights" /> <HorizontalScrollView android:id="@+id/scrollView" android:layout_width="0dp" android:layout_height="match_parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/webView" app:layout_constraintTop_toTopOf="parent"> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetContainer" android:layout_width="wrap_content" android:layout_height="match_parent" /> </HorizontalScrollView> </androidx.constraintlayout.widget.ConstraintLayout> <androidx.compose.ui.platform.ComposeView android:id="@+id/composeTargetWatcher" android:layout_width="match_parent" android:layout_height="300dp" android:paddingTop="16dp" /> </LinearLayout> MainActivity.kt and activity_main.xml are in the middle of being converted to Jetpack Compose, so they currently contain a mix of Views and Composables. As of this writing, WebView and some other elements don’t yet have Composable equivalents, so for now, a hybrid approach is necessary. [^1] In activity_main.xml, ComposeView inside HorizontalScrollView is intended to replace an existing View with a Composable, while the other ComposeView is placed to track the position of the Composable above. The Composable that replaces the View is structured as follows and implements onGloballyPositioned(...) to check the position of the part labeled "Target". @Composable fun ScrollComposable(targetRect: MutableState<Rect>) { val textStyle = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSecondary, fontSize = 24.sp, fontWeight = FontWeight.W600 ) Row( modifier = Modifier .fillMaxSize(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Red), contentAlignment = Alignment.Center ) { Text("1", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Magenta), contentAlignment = Alignment.Center ) { Text("2", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(MaterialTheme.colorScheme.primary) .onGloballyPositioned { targetRect.value = it.boundsInWindow() }, contentAlignment = Alignment.Center ) { Text("Target", style = textStyle) } Box( modifier = Modifier .padding(16.dp) .width(100.dp) .fillMaxHeight() .background(Color.Cyan), contentAlignment = Alignment.Center ) { Text("4", style = textStyle) } } } Here's a Composable that keeps an eye on this: Here, the Composable itself uses onGloballyPositioned(...) to get its own size. @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { if (visibleRect.width <= 0f || visibleRect.height <= 0f) return val rootAspectRatio = visibleRect.width / visibleRect.height val density = LocalDensity.current // For calculating toDp() val targetColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.25F) var size by remember { mutableStateOf(IntSize.Zero) } Box( modifier = Modifier .fillMaxSize() .onGloballyPositioned { coordinates -> size = coordinates.size } ) { if (size.width <= 0F || size.height <= 0F) return@Box val watchAspectRatio = size.width.toFloat() / size.height val (paddingH: Float, paddingV: Float) = if (rootAspectRatio < watchAspectRatio) { (size.width - size.height * rootAspectRatio) / 2 to 0F } else { 0F to (size.height - size.width / rootAspectRatio) / 2 } with(density) { Box( modifier = Modifier .padding(horizontal = paddingH.toDp(), vertical = paddingV.toDp()) .fillMaxSize() .background(Color.Gray) ) } if (targetRect.width <= 0f || targetRect.height <= 0f) return@Box with(density) { Box( modifier = Modifier .padding( // Caution: exception is thrown if padding is negative start = max( 0F, marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.left ) ).toDp(), end = max( 0F, size.width - marginOf( size.width, paddingH, visibleRect.left, visibleRect.right, targetRect.right ) ).toDp(), top = max( 0F, marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.top ) ).toDp(), bottom = max( 0F, size.height - marginOf( size.height, paddingV, visibleRect.top, visibleRect.bottom, targetRect.bottom ) ).toDp() ) .fillMaxSize() .background(targetColor) ) } } } private fun marginOf( sizePx: Int, halfPx: Float, visibleFrom: Float, visibleTo: Float, px: Float ): Float { val alpha = (px - visibleFrom) / (visibleTo - visibleFrom) return (1 - alpha) * halfPx + alpha * (sizePx - halfPx) } This Composable function illustrates the relationship between the Activity screen and the Composable labeled as "Target". Setting the padding is a bit tricky, but it’s not rocket science. Composable is technically a function, but it maintains UI state by continuously holding values with MutableState and remember . At the same time, automatically recompose when persistent information changes, reducing the need for tedious event handling and allowing for a more declarative coding style. When you scroll the HorizontalScrollView in the upper right corner of the screen left and right, the Composable at the bottom follows along and updates its position. By retrieving the position of a View or Composable inside a ScrollView using View$getLocationInWindow(IntArray) or LayoutCoordinator.boundsInWindow() , you can also get off-screen coordinates. This allows you to check whether elements can move properly in and out of the visible screen.   (However, this does not necessarily apply when scrolling using Modifier.horizontalScroll(...) , Modifier.verticalScroll(...) , or similar methods in a Composable.) In a conventional View, displaying this kind of debug information required modifying both the layout XML file and the Java/Kotlin code. Since these changes wouldn’t be reflected in the released app, it felt like a bit of a hassle. Data binding was also introduced, allowing layout XML files to track variable updates, but it wasn’t the most intuitive approach. Jetpack Compose lets you to design screens as if you were writing information directly into the design, making it easy to see your implementation results almost instantly. Displaying everything graphically isn’t always the best approach, but having more options for expression can make it easier to dive into coding. [^1]: A wrapper that uses WebView functionality as a Composable is available in the accompanist library, but it is currently deprecated . Even in the official implementation, making a View fully Composable doesn’t seem easy. Since it’s common for Views and Composables to coexist for a long time in general app development, there’s no need to worry—go ahead and embrace Composables with confidence. 2.3. Writing Debug Information Declaratively Displaying debug information with Composables allows you to add details even more easily with fewer steps: @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... Text( text = when { targetRect in visibleRect -> "Target: inside screen" visibleRect.overlaps(targetRect) -> "Target: crossing edge of screen" else -> "Target: outside screen" }, modifier = Modifier.align(Alignment.TopStart), style = TextStyle( textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.onSurface, fontSize = 24.sp, fontWeight = FontWeight.W600 ) ) } } /** * "operator fun receiver.contains" defines in and !in (syntax: other in(!in) receiver) */ private operator fun Rect.contains(other: Rect) = left <= other.left && top <= other.top && right >= other.right && bottom >= other.bottom The above uses Text(...) to add debug information. The style argument in Text(...) can be omitted if the default settings are sufficient. You can display information with almost the same amount of typing as Log.d(...) or println(...) . Unlike those methods, the information doesn’t disappear as you scroll. One of the advantages of Declarative UI is that it makes building a UI as effortless as "debugging with print statements". Build and run the app, then scroll horizontally in the top-right scroll view to see the app in action at the bottom of the screen, like this: Off-screen display Partially over screen borders On-screen display 3. Procedural Processing with LaunchedEffect So far, we’ve discussed the significance of Declarative UI , but when it comes to handling events triggered by state changes or adding animations to show those changes, some Procedural processing is also necessary. If you can’t write procedural code like event handling, which has traditionally been done in View, moving to Jetpack Compose won't be possible. In Jetpack Compose, LaunchedEffect is commonly used to handle actions based on state changes. @Composable fun TargetWatcher(visibleRect: Rect, targetRect: Rect) { // ... // ... var currentState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var nextState by remember { mutableStateOf(TargetState.INSIDE) } var stateText by remember { mutableStateOf("") } var isTextVisible by remember { mutableStateOf(true) } nextState = when { visibleRect.overlaps(targetRect) -> TargetState.CROSSING else -> TargetState.OUTSIDE } LaunchedEffect(key1 = nextState) { if (stateText.isNotEmpty()) { if (currentState == nextState) return@LaunchedEffect stateText = when (nextState) { TargetState.INSIDE -> "Target: entered screen" TargetState.OUTSIDE -> "Target: exited screen" TargetState.CROSSING -> if (currentState == TargetState.INSIDE) "Target: exiting screen" else "Target: entering screen" } currentState = nextState repeat(3) { isTextVisible = true delay(250) isTextVisible = false delay(250) } } stateText = when (nextState) { TargetState.INSIDE -> "Target: inside screen" TargetState.CROSSING -> "Target: crossing edge of screen" TargetState.OUTSIDE -> "Target: outside screen" } isTextVisible = true } if (isTextVisible) { Text( text = stateText, modifier = Modifier.align(Alignment.TopStart), ) } } } enum class TargetState { INSIDE, CROSSING, OUTSIDE } You can set multiple keys for LaunchedEffect . If you only want to run the process the first time the Composable is called, you can use LaunchedEffect(Unit) { ... } . Whenever the nextState specified as a key changes, the corresponding process will be executed accordingly. The code above will make the text flash for 1.5 seconds after a state change, showing the current state compared to the previous one, and then display it statically. You can handle events by specifying a state variable as the key of LaunchedEffect and writing the processing inside the block to run when the state changes. Inside the LaunchedEffect block, you can write time-consuming processes using suspendfunctions like delay(...) . If a state change happens before the block’s processing finishes, the current processing is canceled, and the new state’s processing starts from the beginning in response to the change. The LaunchedEffect block handles procedural processing, while the Text(...) follows a Declarative approach based on the values of the variables provided procedurally. For handling changes in the UI, it’s best to use LaunchedEffect . Besides, there are other effects suited for different situations, such as SideEffect , as well as DisposableEffect , which handles processing according to the lifecycle of Activity and Fragment. On the other hand, it’s also important not to overuse these processes to keep the code from becoming unnecessarily complex. For example, it’s recommended to handle event processing triggered by responses from the Internet or inputs from sensors like NFC in the ViewModel, while keeping procedural code in Composable limited to UI-related elements. 4. Interoperability between ComposeView and AndroidView When using a Composable in an Activity or Fragment, you can display it with ComposeView , as shown above. Conversely, if you want to use something like the aforementioned WebView inside a Composable, you can embed a View into a Composable using AndroidView or AndroidViewBinding . For instructions, please refer to Here (Using views in Compose) . This article won’t go into details, but thanks to the AndroidView Composable, even if you’ve made progress in converting your app to Composable but find that replacing certain Views is challenging or time-consuming, you can still continue development by integrating both Compose and View. This interoperability is extremely powerful—you can call AndroidView inside a Composable invoked by ComposeView , then nest another ComposeView inside it to call a Composable again, and even embed AndroidView within that, creating a layered structure that can continue further. By keeping the option to use Views within Composables, you can minimize the risk of wasted effort if the transition to Composable doesn’t progress as expected or if replacing Views takes significantly longer than a development sprint. 5. Preview Function So far, we’ve focused on the flexibility of Composable, which allows you to reflect on the development process. It supports techniques for alignment and debugging at a level comparable to Views, enables procedural processing, and offers the option to revert parts to Views through powerful interoperability when facing challenges in the transition to Composable. Here, we’ll talk about one of the key benefits of Composable—the simple yet powerful preview capabilities of the Preview function. 5.1. Create a Preview function Android Studio’s preview feature allowed you to visualize Views in layout XML files, but Jetpack Compose takes it a step further with even more powerful preview functions. Just create a function with the @Preview annotation, and your Composable will be displayed in the preview: // ... private class TargetWatcherParameterProvider : PreviewParameterProvider<TargetWatcherParameterProvider.TargetWatcherParameter> { class TargetWatcherParameter( val visibleRect: Rect, val targetRect: Rect ) override val values: Sequence<TargetWatcherParameter> = sequenceOf( TargetWatcherParameter( visibleRect = Rect(0f, 0f, 100f, 300f), targetRect = Rect(90f, 80f, 110f, 120f) ), TargetWatcherParameter( visibleRect = Rect(0f, 0f, 300f, 100f), targetRect = Rect(80f, 90f, 120f, 110f) ) ) } @Preview @Composable private fun PreviewTargetWatcher( @PreviewParameter(TargetWatcherParameterProvider::class) params: TargetWatcherParameterProvider.TargetWatcherParameter ) { KtcAdventCalendar2024Theme { TargetWatcher(params.visibleRect, params.targetRect) } } The above example demonstrates how to use PreviewParameterProvider to supply multiple parameters to a single preview function and display them in the preview. In the layout XML file of the Android View, you can’t achieve this using the tools:??? attribute to configure the preview. However, can display a preview without using PreviewParameterProvider —just call a Composable function inside a function annotated with @Preview and @Composable . My recommendation is to create a preview function as soon as you start working on a new Composable. Just the advantage of having powerful preview capabilities from the start when creating a new Composable is reason enough to switch to Jetpack Compose. Being able to check the display of debug information with the preview function is another advantage of using Composable for debugging. Recently, using preview functions for UI testing with libraries like roborazzi has been gaining attention. From a testing efficiency standpoint, creating preview functions is definitely worthwhile. 5.2. Try running the Preview function As explained here (Running the Preview) , you can run the Preview function on an actual Android device or emulator by clicking the Run '...' icon on the left side of Android Studio. This is similar to the previous functionality for executing a specific Activity, but the Preview function is more powerful. It is easier to write and it can run in a simplified environment without needing elements like intents. Callbacks for UI actions, such as button taps, are also executed, allowing you to use the Preview function for testing as if it were a simple app. However, it’s not recommended to overload a Composable with too many features just to make it more testable in the Preview function. To keep Composable purely as a declarative UI, it’s best to separate business logic into other classes or functions, such as a ViewModel or a Presenter in Circuit, and focus on writing only UI-related code. 6. Conclusion I hope this article will encourage more developers to take the leap from Android View to Jetpack Compose. Transitioning to a UI system with a different approach isn’t always straightforward, and the fear of setbacks is understandable. My hope is that your first steps feel as smooth as possible and that the risk of wasted effort is kept to a minimum. 7. References Android API reference [Android] Functions for Retrieving a View’s Position In-Depth Guide to Jetpack Compose Modifier What can Advanced / Lesser Known Modifiers do for your UI? — A Comprehensive Exploration in Jetpack Compose: A Journey Through Advanced and Notes The Android robot is reproduced or modified from work created and shared by Google and used under the terms of the Creative Commons Attribution 3.0 License. ^2
1.start-(概要) 初めまして! KINTOテクノロジーズのCloud Infrastructure GでInfrastructure Architectを担当している劉(YOU)です。 今年の1月に入社して、techblogには初執筆なのでこれからよろしくお願いします! AWSの認定は23年10月のSAAを始じめ、25年2月のMLAを最後に、1年4ヶ月でやっとAWS認定12冠を取りました。せっかくなので、AWS認定12冠を達成しながら感じた個人的な意見と情報を共有します。 先に話しますと、AWS認定12冠とは、AWS社が主催する資格認定を全て取得したことを指します。その基準は毎年変更されるんですけど、 https://aws.amazon.com/jp/blogs/psa/2024-japan-aws-all-certifications-engineers/ AWS JAPAN APN ブログ からその詳細を前もって発表して、選出された方々を表彰します。2024年は1,222名が正式に「AWS All Certifications Engineers」として認められています。公式記事では、 ”AWS 認定資格を全て取得し・維持する事” は AWS のテクノロジーを正しく理解し、お客様に信頼性の高い最新の技術をご案内できる基盤をお持ちだという証になります。 と述べています。 AWS以外でも、AzureとかGCPとかクラウドサービスを提供している会社は多いですが、クラウドサービスの量と質、高い占有率からできる汎用性、圧倒的なアップデートスピード、どう考えてもAWSは クラウド業界の標準だと言い切れます。 そして、最近AIの注目が高くなることと伴い、クラウドの重要性も上がっています。「クラウドとかAIとか、私と関係ないだろう」と思う方がいらっしゃるかも知れませんが、現時点でほぼ全ての業務にパソコンを使うようになった事と同じく、もうじきAIを日常で使うことになる時代がやって来ると思います。 AIその物になるモデルとモデルを動かすためのコンピュータ、それが簡単に提供されるプラットフォームがクラウドであり、時代を追いつくためにもクラウドを習得する必要があると言うことです。それで、AWS及びクラウドを勉強するために資格はなぜ必要なのか? その答えを次から説明して行きます。 2.status-(現状) 残念ながら、資格はあってもなくてもクラウド活用に大きい影響を与えません。例を挙げると、今からクラウドを英語だと見なして考えてください。英語を活用するためにTOEICを準備して、高得点をしました。それが本当の意味で英語を上達することだと思いますか? どれだけ試験のテクニックが良くても、単語と文法などを暗記しても、実際に英語を要求される時に使いこなせなかったら無駄です。 しかし、TOEICが英語の実力向上に役立たないって言ったらそれは違うと断言できます。意味が無かったら数々の大学・企業からTOEICの点数を基準として評価する訳がないです。ビジネス英語力を点数に換算する試験がTOEICだからこそ、ただの点数ではなくて能力として認められてることになります。 そう言う意味で、AWS認定12冠はクラウドに対して分かりやすい天井です。 実体化されていない知識の塊を資格という形で見えるようにします。こう言う見える化によって得られる効果を整理しますと、 明確な目標設定(Goal):AWS社が証明してくれるロードマップを従った認定なので、階段式でスケジューリング可能 努力の源(Motivation):受験日を決めて勉強することで、頑張れる環境が作られる 知識の保証(Knowledge):資格を取るための最低限の知識が保証される 振り返り(Remind):そもそもクラウドに詳しい人であっても、資格で要求される知識を点検することができる キャッチアップ(Discovery):試験はアップデートを沿って変わっていくので、触れる機会がない情報を勉強することができる になります。 逆に英語で置換して見ても、違和感なく受け入れられる内容ではないでしょうか?結局、資格を取ったらクラウド力が上がる、クラウドやりたいから資格取ろうではなく、鍛えること、それ自体に意味があると思います。 AWS認定のこれから 続いて、私が一年ちょっと超える時間をAWS認定準備しながら感じた「これからAWS認定はどうなる?」を突っ込んで行きます。 :::message あくまでも、 根拠のない自分勝手の推測なので、AWSで公式の何かがあることではありません。 引用の時にはご注意お願いします。 ::: 私がAWS認定の準備し始めた時期は、22年ChatGPTが流行ってAIに対する関心が大きくなってる状況でした。AWSもAIを中心にするサービスをどんどん出して、24年から資格の構成を大きく変更させました。 既存のスペシャル資格三つを24年4月で削除し、 AWS Certified Data Analytics – Specialty (DAS) AWS Certified Database – Specialty (DBS) AWS Certified: SAP on AWS – Specialty (PAS) DASとDBSを代行するために資格が24年3月に登場、 AWS Certified Data Engineer – Associate (DEA) その後、Amazon QとかAmazon Bedrockなど新しく出たAIサービスと、Amazon Sagemaker周りの強化されたAIサービスのロードマップを提示するために24年10月、 AWS Certified AI Practitioner (AIF) AWS Certified Machine Learning Engineer – Associate (MLA) 結構、大変革だったので試験を準備している個人の立場でもちょっと困りました。勉強してた内容が大幅に変更されることと同然だったので、先に計画してた試験日程も変えるしか無かったです。これからも、今後の技術トレンドであるAIを中心に変わっていくことは確かです。 あくまでも推測で過ぎませんが、変更が起きる可能性が高い資格は AWS Certified Machine Learning Engineer – Specialty (MLS) だと思います。 MLSが最後にアップデートされたのは22年7月のなのでAIF, MLAに比べたら内容が古くなっています。現状のSpecialtyのままアップデートされることもありますが、新しいprofessional資格に改編される可能性が高いです。 その理由としては、既存のパスがPractitioner→Associate→Professionalに繋がる仕組みだからです。 ^1 同様にAIF → MLA資格の次に来るProfessional資格が必要になります。単純にスペシャリティがプロフェッショナルになって最新化される…ことはAWS側が決める所ですが、そうなったらDEAの上位資格も想定するしかないです。 (仮)AWS Certified Machine Learning Engineer – Professional (MLP) (仮)AWS Certified Data Engineer – Professional (DEP) これが普通に考えられる予測ですが、これはこれで問題が生じます。AWS社の認定は12冠を象徴として維持してるそうなので、二つが増えれば資格の数が13個を超えてしまうことです。 それを回避する方法があって、まずは上記の資格が増やされる分、曖昧になったSpecialtyを減らすことです。 (仮)AWS Certified Security – Specialty (SCS) AWS Certified Advanced Networking – Specialty (ANS) SCSとANSはもう消えている他のスペシャリティとは違って、プロフェッショナルから携わった知識を深く振り込む内容で構成された認定です。両方とも出る内容が60%以上がプロフェッショナルと重複しつつ、SCSは組織全体のセキュリティを重視し、ANSはオンプレミスとのネットワーキングが主になっています。その中、現状のままではやむを得ない欠陥があります。 SCSはAIのアップデートされなかったので、AIに対するセキュリティの内容が含まれていません。AIの発展が早くなっていながら、AIに対するセキュリティーやコンプライアンスも大事にされてるので、SCSにAIの内容が入れるか、各プロフェッショナルに溶かすかの問題です。すでにAIのトレンドに合わせて統廃合されたスペシャリティ認定が多いので後者がなり得ると思ってます。 ANSの場合、SCSと似ているポジションですが、ネットワークはAIができてもAWS内部に限ってはそんなに差がありません。OpenAIを使うにはAzureを、Geminiを使うにはGCPを、その他のクラウドベンダが運営しているAIを使うにはマルチクラウドが確かに必要になりますが、自社製品じゃないとAWS社が積極的に支援しないスタンスなので、マルチクラウドに関係する認定のアップデートは今の所はないです。代わりに、アンチクラウドの流れからハイブリッドクラウドが浮上していますので、ANS認定の仕組みは維持すると見込んでいます。 とにかく、資格の数を減らしたら12冠は担保できるのでこう言うやり方もあることと、二つのプロフェッショナルを増やさずにDevOps Engineerみたいに一つに納める事もあります。 (仮)AWS Certified MLOps Engineer – Professional (MOP) AWSで紹介している MLOps は、「ML アプリケーション開発 (Dev) と ML システムのデプロイおよび運用 (Ops) を統合する ML カルチャーとプラクティスです」だと記述してます。 ^2 これはMLに関する全体作業を意味してるので、まさにDEAで扱うデータエンジニアリング、データ分析を通して、AIF→MLA→MLSで使われる機械学習の全体を活用できますので、新しいプロフェッショナルがただ一つだけ必要だったらこれで通用できる仕方だと思ってます。 AWS認定の類型 そして、認定の種類だけではなく、試験の類型にも変化が起きています。 既存の試験はSOAのラボ試験が中止になってから、選択肢で正解を選ぶ形式のみが評価方式でした。客観的に定量評価ができる要素は長所ですが、実装とは関係が深くない知識になることも否定できません。AWS社もこれを意識しているかなと思いまして、AIFとMLAから新しい出題形式が出ました。 AIF試験ガイド を見ると、 並べ替え: 指定されたタスクを完了することを目的とした 3~5 つの答えのリストが提示される。設問に対する点数を得るには、正解を選択し、正しい順序に並べる必要がある。 内容一致: 3~7 つのプロンプトのリストと一致する答えのリストが提示される。設問に対する点数を得るは、すべてのペアを正しく一致させる必要がある。 ケーススタディ : 1 つのシナリオに、そのシナリオに関する設問が 2 つ以上含まれている。ケーススタディの各設問のシナリオは同じである。ケーススタディの各設問は個別に採点される。ケーススタディでは正解した設問ごとに点数が得られる。 上記の三つの類型は私の試験にも多い数ではありませんが、試験ガイドで述べている事と同じく出題されました。問題のレベルとしては選択肢の問題と同様で、AWS試験の秘密保証のため問題の詳しい形式は言えませんが、私が感じた出題形式の評価はこうなります。 並べ替えと内容一致は、選択肢の類似性から正解を類推することが出来なくなりました。本当に実施するべきの手順だったり、提示される単語や説明を結び付く、問題から要求する内容を熟知しないと解けない形です。 ケーススタディの基本は設問=選択肢なんですが、単一のケースで複数の問題を提示するやり方です。ここはケースを多角的に接近することもできるし、長問・多問からは知識の応用より読解力を求められる状況があるから、それが解消されます。我らの現実世界もケースがあったら一問一答で絶えず、ケースごとにシミュレーションすることが一般的なので、このケーススタディは受験者としていい類型だと思います。 AWS社は認定に関してこれからも出題形式もそうですし、SOAのラボみたいに「本当に実装できますか?」を目指して変化して行くと考えられます。こういう変更は単発で起きることではなく、連続的に他の認定にも反映されるので、AWS試験を準備していらっしやる方々はキャッチアップして備えて行きましょう! 3.stance-(心構え) 私も周りの人達と話す時に職種不問でよく出る話題なんですが、 「AWSとは関係ない仕事してるけど、これ勉強して本当に使える?」 「AWS資格取るとしたらどこから始まる?」 「何をネタに勉強してる?」 と質問を貰います。私はクラウドエンジニアとして資格を取っていまして、クラウドで業務をするための知識が元々必要になります。実務で使っているからこそ、クラウドに関わる頻度が他の人と比べて非常に多いです。その為、資格を取得したらすぐにクラウドに関わる業務ができるようになるとは言えません。 今まで全然使っていなかったのに、資格を取ったらすぐ使えるようになることはあんまりないです。資格は言わば、何らかのクーポンのような物です。1万円以上に限って10%割引をしてくれるガソリンスタンドのクーポンができたとしても、車がそもそもないから無用、クーポンの店が遠いから行けない、10%割引額を達成するお金がない、クーポンは色んな理由で使えきれないケースがいっぱいです。こう見たらクーポンを使うための条件は明確です。 自分・知り合いが車を持っていたり、車を持つ計画がある。 クーポンを使えるガソリンスタンドとの距離が近い。 クーポンを使える余力がある。 自分自身にそのクーポンが欲しい理由が整えているかを確認してください。 言い換えますと、「資格を活かせるように動けますか?」という意味です。クーポンを手に入れたとして、車が自動で生成されたり、ガソリンスタンドが勝手に家の前にできたり、クーポンを使えるお金が急に現れたり、そんな出来事は現実では起こらないですよね?クラウドもAWSも同じです。クラウドが己の業務と当てはまらない方々、例えば ITと関係ないビジネス系 インフラは専門ではない開発系 オンプレに特化したインフラ系 が挙げられます。 車は高いから買えない人には何を推奨しますか?カーリース・サブスクは月額払いだけできれば車を使えることができます。これがITではクラウドです。私は「技術を借りる」がクラウドの本質だと思ってます。技術を教わることが高かったら借りればいい話です。領域によって詳しくは違うかも知れませんが、知っている事だけで技術の視界が覆ると確信してます。 ガソリンスタンドが遠くて行けないって言ったら、それはそれで十分です。無理矢理に行くことまではないです。ただし、通勤途中で寄り道に行けそうな距離だったらどうしますか?開発系の方にとって、クラウドはそんなに遠い所にある訳ではありません。むしろ、視線を少し横に移すだけで活用できることが溢れるているかもです。 最後に、どれだけお得になるクーポンを持っていても、使わずにいたらない事と同然です。車をすでに持っていて、ガソリンスタンドがすぐ前に出来ても、いつも通っていたガソリンスタンドだけ利用してたらクーポンは使えません。1万円を前払いできないとか、他社のガソリンカードがあるとか、検証できない店は行きたくないとか、それぞれ抱えた理由は千差万別だと思ってます。しかし、否定できない事実はインフラ系の方々は、他の誰よりもクラウドを始めることに特化していることです。オンプレだけやってたら、クラウドはIaaS(Infra as a Service)とかPaaS(Platform as a Service)として提供していて馴染みがないと思われます。それでも、基本構造はインフラの知識の上で作られているから、ビジネス系と開発系に比べれば極めて簡単です。「クラウドまでやれる余力がない」ではなく、「クラウドまでやる余力を作ってみよう」はどうですか? 私もキャリアを開発系でスタートしていましたが、個人的に勉強したクラウドの知識があってクラウド業務も任されました。その後は資格を取りながらクラウド職に転向もできました。多分、私がやろうとしてる事とやっている仕事に限って集中していたらできなかったと思います。AWS認定12冠の達成もチャンスを増やす感覚であります。資格取得で得た知識のうち、KTCに合流してから実践で使える知識は50%前後程度です。しかし、使ってない50%の知識もこれから活かせるように頑張っています。KTCはAIファーストを今年の目標に決めているので、私もAI活動を盛り上げて行くつもりです。 AIファーストと繋ぐKTCの今年目標に興味がある方は、弊社副社長の景山が記事を掲載しているのでぜひ読んでみてください。 https://blog.kinto-technologies.com/posts/2024-12-25-LookBack2024/ 公式から推薦する AWS認定パス もありますので、ここもご参考お願いします! 4.strategy-(攻略法) 勉強法は私以外にもたくさんの方が推薦してくれていると思いますので、観点を変えてAWS認定を効率よく攻略する戦法をテーマにお話しします。 正攻法 真面目に勉強する方法は極簡単です。前述してた AIF試験ガイド で出ている内容をゼロから習得することです。ここはベース知識がなくて誠実に勉強して行きたい方や、受験日程に焦らずにやりたい方にお勧めします。 全部5段階でやってます。 情報収集:検索、SNS、YouTube、Blogなどを参考して己が好むソースを探す。 ソース決定:下記のソースの中で、最も自分に適することを決めます。 AWS公式 AWS社で提供してくれるドキュメントは最新反映されてて、信用度も高いし、内容も上品です。私も他のやり方で勉強しても公式はいつも参照します。一部無料のAWS Traning Centerの活用もすごく助けになりますので、ぜひ活用してください。有料の物は使っていませんが、下で紹介する外部学習サイトと似てる効果だと感じます。 YouTube 無料情報の量は一番多いですが、アップローダーさんによって質も量もバラバラですし、最新化も望めない短所があります。でも、映像や音声で学習できることと言語の縛りさえなければ短所も薄くなります。聞いてみていつでもお気軽にやめる事もできることが嬉しいです。 書籍 アナログ勉強法が好きだったら推しです。ターゲットを絞って情報の質を保証することが本の魅力です。買う前にある程度内容把握もできるし、一冊で求める内容が集約されていることが長所です。しかし、情報の最新化には弱いことは変化が早いAWS認定には向いてないことを注意してください。即時、勉強に入って認定の更新前に試験を受ける事じゃなかったら避けた方がいいです。 外部学習サイト Udemyみたいに有料で提供するソースを記述します。お金を払う分、YouTubeを超える質を持ちながら最新化も早いです。端的に言うと、YouTubeと書籍の長所を合体させた感じなので、個人的に愛用しています。気を付ける所はAWS認定の場合、資格同士に被る内容が結構ありますので目次をよく見て購入してください。 学習開始:試験によって学習時間は違いますが、2-3ヶ月を想定して勉強することが最適でした。 検証:AWS公式とかでやってる模擬試験で自己評価 試験:日程を予め取って受験します。オンラインでもオフラインでも受験できますが、一貫的な試験環境のためにもオフラインを推奨します。オフラインでの試験で何かの問題が起こったら、試験主催側が調整してくれるので安心ですし、試験結果もオフライン試験の方が早く届くのでお得です。 速攻法 逆に「私はAWS知っているから、初めから勉強するまでもない。知らない所だけ勉強したい!」と考えてる方や、「資格を早く取って勉強してもいいから、最速で取りたい!」と思ってる方もいます。私も幾つかの試験はそうだったので、コスパ最高の攻略法を話します。 :::message 生成AI:私はPerplexityを使ってましたが、どんな生成AIでも大丈夫です!そして、紹介するプロンプトは記事用の例であって、個人が向いてるプロンプトやり方でしてもOKです。 ::: キーワード整理 サービスの内容を要約してピンポイントだけ知ることができます。 :::details 回答 以下に、AWSの監視サービスであるCloudWatchを中心に、関連するサービス(CloudTrail、Config、WAF)との比較を整理します。また、これらに含まれない他の監視ツールも簡単に触れます。 CloudWatchの整理 概要 Amazon CloudWatchは、AWSリソースやアプリケーションをリアルタイムで監視するためのサービスです。主に次の機能を提供します: メトリクス収集:CPU使用率、ディスクI/O、ネットワークトラフィックなどのリソースデータを収集。 ログ監視:アプリケーションログやシステムログを収集し、分析可能。 アラーム設定:特定の条件(しきい値)に基づいて通知や自動アクションを実行。 イベント管理:特定のイベントをトリガーとして処理を自動化(例:インスタンス起動時の通知)。 主なユースケース リソースのパフォーマンス監視 異常検知とアラート通知 システム全体の稼働状況の可視化 CloudWatchと関連サービスの比較 サービス名 主な目的 特徴 主なユースケース CloudWatch メトリクス&ログ監視 AWSリソースやアプリケーションの稼働状況をリアルタイムで監視可能。 ほぼリアルタイムで通知。 リソース監視、パフォーマンス最適化、異常検知 CloudTrail 操作履歴の記録 誰がいつ何をしたか(APIコール)の履歴を記録。 コンプライアンスやセキュリティ監査向け。 セキュリティ監査、不正操作検出 AWS Config 設定変更の追跡&評価 リソース構成変更を記録し、ルール違反を検出可能。 設定状態をスナップショットで保存。 設定管理、コンプライアンス遵守、トラブルシューティング AWS WAF Webアプリケーション層のセキュリティ対策 SQLインジェクションやDDoS攻撃などからWebアプリケーションを保護。 CloudFrontと連携可能。 セキュリティ強化、攻撃遮断 比較ポイント 目的と対象範囲: CloudWatchはシステム全体の稼働状況を監視。 CloudTrailは操作履歴に特化。 Configは設定変更とルール評価。 WAFはセキュリティ対策専用。 リアルタイム性: CloudWatchはほぼリアルタイムで通知可能。 CloudTrailは遅延が発生する場合あり。 ユースケース重複: udTrailやConfigで記録したデータをCloudWatch Logsに送信し、統合的に管理することも可能。 他のAWS外部監視ツールとの比較 AWS以外でも利用可能な監視ツールには以下があります: ツール名 特徴 メリット デメリット Zabbix オープンソース。 高度なカスタマイズ性 無料で利用可能。 幅広い環境に対応 導入・運用が複雑 Mackerel SaaS型日本製ツール 簡単導入。 複数環境一元管理が可能 AWS専用機能が少ない Site24x7 マルチクラウド対応 ハイブリッドクラウド環境にも対応 AWS専用ツールより細かい設定が難しい 選択基準 AWS専用ならCloudWatchが最適。 マルチクラウド環境ではSite24x7やMackerelが有効。 高度なカスタマイズが必要ならZabbix。 試験準備に役立つポイント 各サービスの役割とユースケースを明確に理解する。 CloudWatchと他サービス(CloudTrail, Config, WAF)の連携例を把握する。 コスト面(従量課金制)やリアルタイム性なども試験問題に出る可能性あり。 必要なら追加質問してください! ::: 試験問題生成 キーワードの概念を分かったら、簡単な問題を生成して自己評価します。 :::details 回答 問題 1: あなたは、EC2インスタンスのCPU使用率が特定のしきい値を超えた場合に通知を受け取りたいと考えています。この目的を達成するためにどのAWSサービスを使用するべきですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch • C. AWS Config • D. AWS WAF 問題 2: セキュリティチームが、過去3か月間に発生したAWSリソースへのAPIコールを追跡し、特定のユーザーアクティビティを調査したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. Amazon CloudWatch • B. AWS CloudTrail • C. AWS Config • D. Amazon Athena 問題 3: ある企業が、AWSリソースの設定変更を追跡し、コンプライアンス違反を検出したいと考えています。この目的に最適なサービスはどれですか? 選択肢: • A. AWS CloudTrail • B. Amazon CloudWatch Logs • C. AWS Config • D. AWS WAF 問題 4:あなたは、WebアプリケーションをSQLインジェクションやクロスサイトスクリプティング(XSS)攻撃から保護したいと考えています。この目的に最適なAWSサービスはどれですか? 選択肢: • A. Amazon CloudWatch Logs • B. AWS WAF • C. AWS Shield • D. AWS Config ::: 速攻法は私が主に使ってる使用法二つだけ説明できなかったんですけど、使用者によって無限にカスタマイズできる生成AI学習法です。紹介した方法以外にも、細かい質問を数十個を投げてリマインドすることも結構やっています。生成AIが参考できる資料をInputに入れたりしたら、もっと豊かなキーワード整理や試験問題を生成してくれます。 私もAIFとかMLAは速攻方を70%割合で使って、1-2周くらいで取ったんで効果は保証します! (1st-art).最初から始まる美学 AWS認定12冠達成して、私が感じた色々を呟いてみました。 気づいてるか知れませんが、実はタイトルにも本文にもトリックを入れました。最初に戻ってみたらすぐ分かると思います。 1.start-(概要) → (1st-art).最初から始まる美学 こんな面倒臭い仕業を入れた理由は、私の1年4ヶ月の12冠の挑戦はスタート(アート)したから得られた一つの絵だと話かったです。どんな結果物が誕生するか、絵を描く時には知らないと思います。小学生の頃、「私の未来を描いて下さい」って聞いて、私が描いた未来は消防士でした。そして、中学生の頃は小説家でした。現在はクラウドエンジニアで全然違う仕事をしています。 そうだとして、私の幼い頃の絵が意味がなかったことでしょうか。私はその絵を描きながら、自分の夢に向き合ったことに意味があったと信じます。私は今、「AWS認定12冠」と言う絵を完成しました。次も新しい絵を描いて行くつもりです。ここtechblogに書いたこの記事も一つの絵になりますし、KTCでの仕事も別の絵になれると思ってます。 記事を読んでいただきありがとうございました!
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 I’m Nakanishi from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station Project was officially launched and later established as a team. As part of this initiative, we are also running an in-house podcast, and for this year’s Advent Calendar, we’re excited to share some of its episodes with you. What is the "Learning Roadside Station"? "Learning Roadside Station" is a project launched to enhance the accessibility and effectiveness of the study sessions that are frequently held within the company. This initiative aims to promote knowledge sharing by supporting study sessions led by dedicated volunteers within the company. Figma Study Session The KTC Learning Roadside Station Podcast features interviews with people who organize study sessions within the company. This segment is called “A Peek into the Study Session Next Door”. Today's podcast guests are T.M. and M.K., who are leading the Figma Study Session. First of all, could you tell us about the work you both do? Let's start with T.M.-san. Interview T.M.-san: I am a member of the Retailer Digital Transformation (DX) Planning Team of the DX Planning Promotion Group in the Mobility Product Development Division. As a designer, I primarily work on developing products related to digital transformation (DX) for our company's retail stores. Lately I’ve been focusing on designing a mobile app, which we plan to release in the fall. Since our team was formed only about a year ago, we are now entering the phase of fully commercializing the project. Akeda-san: Thank you. Now, let’s hear from M.K.-san. M.K.-san: I work as a designer in the Marketing Planning Division. My main responsibilities include visual design and UI/UX design, and recently, I have been working on the website design for a new vehicle subscription service. Akeda-san: Thank you. Now, regarding the Figma study session held last Friday, what was the reason for holding it? T.M.-san: It all started when Nakanishi-san casually dropped by and said, “Why don’t you host a Figma study session?” However, even before that, I had felt that there was room for improvement in how we were using Figma at KTC, so it was perfect timing. So, together with M.K., we discussed the best format, and held our first session last Friday. Akeda-san: Nakanishi-san, what made you reach out and suggest the study session? Nakanishi: Since T.M. joined the company, I felt that he had a strong desire to improve the design. We had already talked about how it would be great to hold a Figma study session, so I felt that now was the right time and reached out. Akeda-san: Thank you. What was the reaction of the participants when you actually held the study session? T.M.-san: The response was quite positive. It seemed like many of the participants wanted to learn more about Figma. There were a lot of questions during the Q&A session, and it was clear that everyone was interested. M.K.-san: I felt the same way. It was a great opportunity to see that many people are interested in Figma and improving UI/UX. Akeda-san: What kind of questions were asked? T.M.-san: The questions covered a wide range of topics, from technical aspects to discussions about UI/UX initiatives at KTC. There were especially many questions from engineers, which showed that they, too, have a strong interest in UX. Akeda-san: Please tell us about future plans for the study group. T.M.-san: In the short term, we aim to improve productivity and efficiency by providing the most relevant themes and content. In the long term, I want to use Figma as a way to increase the presence of designers and contribute to the success of businesses. M.K.-san: I completely agree. We also want to create more opportunities for engineers and designers to develop a common language and communicate with each other. Akeda-san: Thank you. Finally, do you have a message for everyone listening to this podcast? M.K.-san: Design is not just about visuals. It's important for everyone to have an opinion. Let’s communicate freely and openly. T.M.-san: To create a great product, everyone should think like a designer. Please feel free to reach out to us anytime! Akeda-san: Thank you very much. That wraps up today's podcast. T.M.-san and M.K.-san, thank you for joining us. Your passion for the Figma study session and design really came through. I’m excited to see how KTC's design continues to evolve. This time, we have provided details about the Figma study group, the background to its operation, and future prospects. Stay tuned for the next study session!
This article is the entry for day 18 in the KINTO Technologies Advent Calendar 2024 🎅🎄 I’m Nakanishi from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Learning Roadside Station Project was officially launched and later established as a team. As part of this initiative, we are also running an in-house podcast, and for this year’s Advent Calendar, we’re excited to share some of its episodes with you. What is the "Learning Roadside Station"? "Learning Roadside Station" is a project launched to enhance the accessibility and effectiveness of the study sessions that are frequently held within the company. This initiative aims to promote knowledge sharing by supporting study sessions led by dedicated volunteers within the company. Figma Study Session The KTC Learning Roadside Station Podcast features interviews with people who organize study sessions within the company. This segment is called “A Peek into the Study Session Next Door”. Today's podcast guests are T.M. and M.K., who are leading the Figma Study Session. First of all, could you tell us about the work you both do? Let's start with T.M.-san. Interview T.M.-san: I am a member of the Retailer Digital Transformation (DX) Planning Team of the DX Planning Promotion Group in the Mobility Product Development Division. As a designer, I primarily work on developing products related to digital transformation (DX) for our company's retail stores. Lately I’ve been focusing on designing a mobile app, which we plan to release in the fall. Since our team was formed only about a year ago, we are now entering the phase of fully commercializing the project. Akeda-san: Thank you. Now, let’s hear from M.K.-san. M.K.-san: I work as a designer in the Marketing Planning Division. My main responsibilities include visual design and UI/UX design, and recently, I have been working on the website design for a new vehicle subscription service. Akeda-san: Thank you. Now, regarding the Figma study session held last Friday, what was the reason for holding it? T.M.-san: It all started when Nakanishi-san casually dropped by and said, “Why don’t you host a Figma study session?” However, even before that, I had felt that there was room for improvement in how we were using Figma at KTC, so it was perfect timing. So, together with M.K., we discussed the best format, and held our first session last Friday. Akeda-san: Nakanishi-san, what made you reach out and suggest the study session? Nakanishi: Since T.M. joined the company, I felt that he had a strong desire to improve the design. We had already talked about how it would be great to hold a Figma study session, so I felt that now was the right time and reached out. Akeda-san: Thank you. What was the reaction of the participants when you actually held the study session? T.M.-san: The response was quite positive. It seemed like many of the participants wanted to learn more about Figma. There were a lot of questions during the Q&A session, and it was clear that everyone was interested. M.K.-san: I felt the same way. It was a great opportunity to see that many people are interested in Figma and improving UI/UX. Akeda-san: What kind of questions were asked? T.M.-san: The questions covered a wide range of topics, from technical aspects to discussions about UI/UX initiatives at KTC. There were especially many questions from engineers, which showed that they, too, have a strong interest in UX. Akeda-san: Please tell us about future plans for the study group. T.M.-san: In the short term, we aim to improve productivity and efficiency by providing the most relevant themes and content. In the long term, I want to use Figma as a way to increase the presence of designers and contribute to the success of businesses. M.K.-san: I completely agree. We also want to create more opportunities for engineers and designers to develop a common language and communicate with each other. Akeda-san: Thank you. Finally, do you have a message for everyone listening to this podcast? M.K.-san: Design is not just about visuals. It's important for everyone to have an opinion. Let’s communicate freely and openly. T.M.-san: To create a great product, everyone should think like a designer. Please feel free to reach out to us anytime! Akeda-san: Thank you very much. That wraps up today's podcast. T.M.-san and M.K.-san, thank you for joining us. Your passion for the Figma study session and design really came through. I’m excited to see how KTC's design continues to evolve. This time, we have provided details about the Figma study group, the background to its operation, and future prospects. Stay tuned for the next study session!
Introduction Hi, my name is Pann Nu Wai, and I am part of the QA Group at KINTO Technologies. As a Test Automation Specialist for the App team in the QA Group, I am responsible for building and maintaining the test automation environment for the KINTO Kantan Moushikomi App , as well as writing test specifications and test scripts. Previously, I wrote a technical article about DarkMode automation testing using Appium . I have been working with automated testing using Appium for three years. In my team, we follow a process where test specifications are defined first before writing the automated test source code. So, in this post, I’d like to share an approach to creating test specifications that has proven highly useful when writing Java source code for Appium. Before diving into the details, I’ll first explain why this method of creating test specifications is essential. Benefits of Creating Test Specifications Saves time when writing automated test source code. Easy to understand even for non-automation testers. Improves readability by organizing specifications by screen, source code class, and operation function. In this article, we will use the login scenario specification for the KINTO Kantan Moushikomi App as a sample test specification. Login steps Here, we will explain each step of logging in to the KINTO Kantan Moushikomi App. Step 1 Step 2 Step 3 Collecting Test Data for Login As the first step in creating a login test specification, it is essential to gather test data, including the test environment, test account, password, report name (automatically generated after test execution), and test execution file. Now, let’s begin with the iOS login scenario specification. Data Name Data Information Environment Stg4 Test Account ******@gmail.com Password ******** Report Name For iOS iOS.poc.login For Android android.poc.login Executable Source File For iOS iOS.poc.login.xml For Android android.poc.login.xml Login Scenario Specification for iOS Confirmation Function Screen Operation Points to Check Executable File (xml) Source File Method Log in Main Screen Click on the My Page logo The login button can be pressed iOS.poc.login.xml iOS.MainPage clickMyPagingLogo My Page Screen Click on "Application Details" Press "Log in here!" iOS.MyPagingPage clickApplyTab clickLoginHereButton Login Screen Enter the above test account in the "Email address (KINTO ID)" field. Enter the above password in the "Login Password" field. Click "Log in to My KINTO" iOS.LoginPage fillMailAddress fillPassword clickToMyKintoLoginButton Now let’s create a Java class for each screen in the scenario specification. First, click the My Page logo on the main screen (MainPage.java). MainPage.java public class MainPage extends Base { public static final String MY_PAGING_LOGO = "//XCUIElementTypeButton[@name="My Page"]"; /** * Test method for clicking the "My Page" logo on the main screen * * This method uses XPath to locate the "My Page" logo on the main page * and performs a click action. * */ @Test(groups = "MainPage") public void clickMyPagingLogo() { driver.findElementByXPath(MY_PAGING_LOGO).click(); } } Step 2: Click "Application Details" on the My Page screen, then click "Log in here." MyPagingPage.java public class MyPagingPage extends Base { public static final String APPLY_TAB = "//XCUIElementTypeButton[@name="Application Details"]"; public static final String LOGIN_HERE_BUTTON = "//XCUIElementTypeButton[@name="Log in to My KINTO"]"; /** * Test method to click "Application Details" on the My Page screen * * This method uses XPath to identify the "Application Details" on the My Page screen, * and performs a click action. * */ @Test(groups = "MyPagingPage", dependsOnGroups = "MainPage") public void clickApplyTab() { driver.findElementByXPath(APPLY_TAB).click(); } /** * A test method to click "Log in here" on the My Page screen * * This method uses XPath to identify "Log in here" on the My Page screen, * and performs a click action. * */ @Test(groups = "MyPagingPage", dependsOnMethods = "clickApplyTab") public void clickLoginHereButton() { driver.findElementByXPath(LOGIN_HERE_BUTTON).click(); } } Next, in step 3, enter your email address and password on the Login Screen, then press the Login button. LoginPage.java public class LoginPage extends Base { public static final String EMAIL_TEXT_FIELD = "//XCUIElementTypeApplication[@name="KINTO Easy Application"]/XCUIElementTypeOther[2]/XCUIElementTypeTextField"; public static final String PASSWORD_TEXT_FIELD = "//XCUIElementTypeApplication[@name="KINTO Easy Application"]/XCUIElementTypeOther[3]/XCUIElementTypeSecureTextField"; public static final String ENTER_KEY = "//XCUIElementTypeButton[@name="Return"]"; public static final String TO_MY_KINTO_LOGIN_BUTTON = "//XCUIElementTypeButton[@name="Log in to My KINTO"]"; /** * A test method to enter a test account in the "Email address (KINTO ID)" field on the login screen * * This method uses XPath to search for the "email address" on the login screen * and enter the test account (Parameter) in the xml file. * */ @Parameters("email") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillMailAddress(String email) { driver.findElementByXPath(EMAIL_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(email); }   /** * A test method to enter a password in the "Login Password" field on the login screen. * * This method uses XPath to get the login password from the login screen. * and enter the password (Parameter) for the xml file. * */ @Parameters("password") @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void fillPassword(String password) { driver.findElementByXPath(PASSWORD_TEXT_FIELD).click(); driver.getKeyboard().sendKeys(password); driver.findElementByXPath(ENTER_KEY).click(); } /** * A test method to click "Log in to My KINTO" on the login screen * * This method uses XPath to identify "Log in to My KINTO" on the login screen, * and performs a click action. * */ @Test(groups= "LoginPage", dependsOnGroups = "MyPagingPage") public void clickToMyKintoLoginButton() { driver.findElementByXPath(TO_MY_KINTO_LOGIN_BUTTON).click(); } } The xml file below is the test execution file for automated testing. Each function is written in sequence based on the test specification. <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd"> <suite name="iOS.poc.login"> <test verbose="2" name="iOS.poc.login"> <classes> <class name="iOS.MainPage"> <methods> <include name="clickMyPagingLogo"/> </methods> </class> <class name="iOS.MyPagingPage"> <methods> <include name="clickApplyTab"/> <include name="clickLoginHereButton"/> </methods> </class> <class name="iOS.LoginPage"> <methods> <parameter name="email" value="******.gmail.com"/> <parameter name="password" value="*********"/> <include name="fillMailAddress"/> <include name="fillPassword"/> <include name="clickToMyKintoLoginButton"/> </methods> </class> </classes> </test> </suite> Summary In this article, we've outlined how to create test specifications for Appium Java source code. However, we believe this approach will be valuable not only for those working with Appium but also for developing automated test source code across different frameworks.
こんにちは!KINTOテクノロジーズのクリエイティブ室でデザイナーをしているmayuです。 私は普段、アプリのUI/UXデザインをメインで担当していますが、今回は会社のイベントで社員に配布するノベルティの制作を手掛けました。 この記事では、ノベルティ制作の企画からデザインまでの舞台裏をお話しします。 ノベルティ制作に携わる方々のヒントになれば嬉しいです。 ノベルティの選定 テーマは「一体感を感じられるもの」 今回のイベントでは「一体感を感じられるもの」を大前提としたノベルティを目指しました。 そこで、以下のような条件をもとにアイデアを出しました。 普段話さない人とコミュニケーションを取るきっかけになる 団結意識を高める イノベーションを促進する 年齢・性別を問わず、誰でも嬉しいもの 複数人のニーズがある 誰でもすぐ利用できる手軽さ 予算は数百円〜千円程度/人 長く価値が続く さまざまな案が出ましたが、最終的に「 マグネットカードスタンド 」と「 オリジナルネームカード 」を制作することに決定しました。 「マグネットカードスタンド」と「ネームカード」を選んだ理由 マグネットカードスタンド: デスクに置いて使うことで、自然と声をかけやすくなりコミュニケーションの促進につながる。 KINTOテクノロジーズのロゴや車の形を取り入れることで、会社への愛着やモチベーション向上が期待できる。 シンプルなデザインで、日常的に使いやすく、誰でも活用できる。 ネームカード: 社員一人一人に名前を入れたネームカードを作ることで、初対面でも話しかけやすく、社内の交流を促進。 「見切れKTCデザイン」によって、会社全体としての一体感をデザインで表現。 当日は名札として、イベント後はデスクに置いて使い続けられる。 マグネットカードスタンドの制作 1. 業者選定と依頼 マグネットカードスタンドの制作は、オリジナルグッズ専門サイト「 MOKU 」に依頼しました。 MOKUはカスタマイズ性が高く、デザインデータを入稿するだけでオリジナルのマグネットカードスタンドが作れることが決め手でした。 2. プロトタイピング 紙を使って簡易的なプロトタイプを試作し、サイズ感や使い勝手を確認しました。 実際のデスクに置いてみて、視認性や実用性を検証します。 3. マグネットカードスタンドのデザイン作成 Adobe Illustratorを使って、規定のテンプレートにロゴを配置したデザインデータを作成。 KINTOテクノロジーズのロゴが際立つシンプルなデザインに仕上げました。 4. 取扱説明書のデザイン作成 使い方が分かりやすいように、オリジナルの取扱説明書も作成しました。 こちらもAdobe Illustratorを使って、規定のテンプレートに沿ってデザインデータを作成。 5. データ入稿&納品 デザインデータを入稿し、約3週間で納品されました!(数量は500個で発注) ネームカードの制作 1. ネームカードのデザイン作成 Figmaで名前、部署、そしてSlackで使っているアイコンを配置したオリジナルデザインを作成。 マグネットカードスタンドとサイズが合うよう、プロトタイプを作って確認しました。 こだわりポイントは、この「 見切れKTC 」。KTCは「KINTOテクノロジーズ」の略称です。 並んでいる小さな四角は社員を表現しており、「社員一人一人が集まってKTCになる」という意味を込めました。 黒を基調としたシンプルでスタイリッシュなデザインで、テックカンパニーらしさも演出しました。 2. データの自動生成 全員分のデータを手作業で作るのは大変なので、社内のエンジニアさんに協力してもらい HTMLでデータを自動生成しました。 CSVファイルから社員情報を取り込み、テンプレートに自動反映させる仕組みを構築しました。 3. 印刷・カット作業 会社のプリンターで印刷し、ひたすら手作業でカット。 大変でしたが、その分かなりのコストカットに成功しました!笑 プロジェクトの結果と学び ノベルティ配布後、社員の皆さんからは以下のような嬉しいフィードバックをいただきました。 「声をかけやすくなった!」 「デザインが可愛い!」 「Slackのアイコンが入っているので愛着がわく!」 ノベルティを通じて一体感が生まれ、私自身も大きなやりがいを感じました。 また、「ただデザインするだけではなく、どう使われるか?」を考えながら制作する大切さを改めて実感しました。 目的に沿ったデザインの力 を発揮できたと思います。 さいごに 今回のプロジェクトを通じて得た学びを、これからのデザイン業務にも活かしていきたいと思います。 もし「KINTOテクノロジーズって楽しそう!」と感じていただけたら、ぜひ 採用ページ もご覧ください!お話しできるのを楽しみにしています。 ご覧いただき、ありがとうございました!
Self-Introduction Hello. This is Koyama ( @_koyasoo ) from KINTO Technologies. Since the beginning of this year, I've been dedicated to promoting Agile practices and working full-time as a Scrum Master, supporting my team in growing stronger every day. Today, I’d like to share some of the things we’ve been working on. Speaking of retrospectives... Do you hold them regularly? When it comes to retrospectives, the KPT (Keep/Problem/Try) format often comes to mind. It’s practically synonymous with the practice. KPTs are widely used in many workplaces, but is it actually effective? Back in June, I attended Scrum Fest Osaka, and out of all the sessions I joined, one left a particularly strong impression on me. OODA!!!!!! (If you know, you know) Yep, it’s Ikuo ( @dora_e_m ) san. Ikuo san’s session on retrospectives really left an impression on me. Until then, I had only used KPT for retrospectives, so when I heard the words, "To keep retrospectives meaningful, we need to avoid falling into a rut," they really hit home. That made me think, "I do retrospectives all the time, I should be able to apply this right away." Or so I thought. @ card As mentioned on page 22 of the session deck, it was eye-opening to see how simply switching from KPT to YWT (which is quite similar) led to a flood of opinions. It’s amazing how just shifting your perspective can bring out so many ideas... Quoted from page 22 of Ikuo san’s document So in this article, I’ll be sharing five retrospective techniques that I’ve actually tried out since then. Summary of How to Choose the Right Retrospective Method As Ikuo san mentioned in his session, switching up between different methods to fit the situation can really help a team grow and improve. Below is a summary of each method’s key features; feel free to use it as a reference. When to Use It! Things to Keep in Mind KPT A versatile method that works anytime. Just be careful not to rely on it too much. Hot Air Balloon Great for thinking about your team’s future. Puts more focus on current challenges rather than reflecting on the past. LeanCoffee Great for discussing various topics, not just retrospectives. Can be a bit tiring since discussions are held under time pressure. Celebration Grid Ideal for fact-based discussions. Hard to generate opinions when there’s little actual implementation and more personal impressions. FunDoneLearn When you want to reflect on the positives. Negatives often get overlooked. Elephants, dead fish, and vomit When the team seems to be building up frustration. Facilitating to keep the team from falling apart. Next, let’s dive into each retrospective method in detail. Let’s explore retrospectives! I hope this article encourages you (especially if you’ve only used KPT so far) to take the first step in trying a different approach! With that in mind, I’ll walk you through how to put it into practice with as much detail as possible. Feel free to use whatever works best for you. Just give it a try, you might be surprised. It’s actually not that different from KPTs! By the way, the examples in this article mainly use the online whiteboard tool Miro . @ card 1. Hot Air Balloon This reflection method involves replacing the "hot air balloon" in the center with your own product and thinking about what kind of "baggage" it carried, what "updrafts" helped it rise, and what "clouds" might become obstacles in the future. All you need is an image of a hot air balloon and three types of sticky notes to differentiate the categories. Here’s the hot air balloon our team came up with. Diagram of a hot air balloon Here’s how it goes: First, we spent 5 minutes writing about "updrafts", followed by an 8-minute discussion. Next, we did the same with "luggage"—writing for 5 minutes, then followed by an 8-minute discussion. Then, we repeated the process with "clouds"—5 minutes of writing, followed by an 8-minute discussion. Finally, we wrapped up with a 10-minute discussion on the question: "What’s important for making a hot air balloon fly higher?" The discussion took place from this perspective (10 minutes). This method naturally encourages discussions that reflect on the present and envision the future. Compared to KPT, it breaks down problems into current issues and anticipated challenges, making discussions more focused and effective. 2. LeanCoffee Lean Coffee is a method that starts with gathering topics, then breaks them into short time-boxed discussions to carry out various conversations about them. You can make this work by setting up an area where people can add and edit sticky notes for topics and another area to process selected notes one by one for discussion. Miro had a template for this, so I gave it a try. LeanCoffee Diagram Here’s how it works. First, participants will come up with a topic (8 minutes). Giving them a suggested theme can help spark ideas and make it easier to share opinions. Use features like polling to find out which topics interest the group the most. Discussions will follow the cycle below, starting with the topic that gets the most votes. Each discussion begins with 5 minutes, including time to introduce the topic. After 5 minutes, the conversation will pause. At that point, ask participants if they’d like to continue discussing the topic. You can use a poll to decide. If they want to continue, add 3 more minutes. If not, move on to the next topic. After those 3 minutes, pause again and check if they’d like to keep going. If they want to continue, add 1 more minute. If not, move on to the next topic. Once the final minute is up, that topic wraps up. If they want to keep the conversation going, set aside extra time for it and wrap up the discussion within that time. It also gives your insight into the current interests and trends among team members. You can also see trends in the interests of your team members at any given time. Plus, it might help reinforce awareness of timeboxing among the team. Facilitating discussions can make it tricky to step in and stop them, so try using a timer or pausing at natural breaks in the conversation. Be mindful that this method relies on sticking to the timebox—if not, it could fall apart. 3. Celebration Grid This method involves discussing completed actions by dividing them into six quadrants based on two axes: one for "success" and "failure", and another for "wrong ways", "experimental ways", and "known ways". As the name suggests, the focus is on keeping a positive mindset—celebrating every outcome, whether it’s a success or a failure. It seems that people often use the diagrams from this site as a guide. @ card CelebrationGrid template As shown in the diagram, each area varies in size based on the likelihood of an event occurring, and they carry the following meanings: The discussion followed these categories. Wrong way Experimental way Known Way Success Lucky! It was a great experience! You did the right thing! Failure It was inevitable. It’s ok, there was a lesson in it. Unlucky Celebration Grid in action Here’s how it works. Ask participants to specify a time period and list "what they have done" (5 minutes). Guide them to consider where each item belongs as they list them. Take a deeper dive into each one. Wrap up by celebrating the many insights gained. At first glance, this method may seem complicated, but it's actually quite simple. Since discussions are based on actual “events and facts", participants can stay grounded and discuss things without personal biases. However, because participants must first list "facts", some may find it harder to express their opinions. To make the process smoother, it’s best to include people who have been actively involved in the work. 4. FunDoneLearn As the name suggests, this method involves listing Fun (what was enjoyable), Done (what was accomplished), and Learn (what was learned.) Write down the elements that fit into each category where the circles overlap. You can use a template like a Venn diagram with overlapping circles to make organizing easier. Making the overlapping areas larger will give more space for sticky notes and make them easier to place. FunDoneLearn Diagram It’s not something that needs a detailed explanation, but here’s how to do it: Set a time limit and have participants put sticky notes (5 minutes). Discuss with each one. This method keeps the focus positive, incorporating an element of Fun. This approach helps create a generally positive and happy atmosphere for the review. On the other hand, since it focuses less on negative aspects like Problem, it may not be the best fit if there are many issues to address. 5. Elephants, dead fish and vomit This method helps identify issues from three different angles. Members are encouraged to openly discuss things they might normally hesitate to say, categorized as follows: Elephants – Issues that everyone is aware of. Dead Fish – Issues that could cause trouble if left unaddressed. Vomit – Issues that are on one's mind. You can facilitate this exercise using a simple diagram with an elephant, a fish, and vomit. However, to prevent personal conflicts among members, it’s a good idea to clearly outline the ground rules in a visible way. Drawing of an elephant, dead fish, and vomit Here’s how it works. First, start by explaining the rules. Make it clear that the goal of this method is not to create conflict within the team, but to come up with ways to address existing issues. This is a key point to keep in mind when using this method. Ask participants to write thoughts with opinions on sticky notes that fit each category (8 minutes). In my team, to keep the discussion from becoming too negative, we encouraged participants to add a pink sticky note if they wanted to reframe an issue into a positive perspective. With this method, seeing other’s notes while brainstorming might sometimes cause discomfort. To prevent this, we make sure that sticky notes in progress are not visible to others. If you are using Miro, enabling Private Mode is a good idea. Once ready, disable Private Mode and start discussing each topic. Since this method involves addressing negative aspects, it requires a bit more sensitivity compared to other approaches. That said, the atmosphere doesn’t tend to feel too negative. Comments like "Oh, so that’s what you were worried about!" or "I was thinking the same thing!" are likely to come up. This method is highly effective in aligning the team’s approach to problem-solving. What Applies to Any Retrospective Having conducted six retrospectives, including KPT, I’ve noticed that there are more commonalities than you might expect. The ultimate goal is always to "agree on the next action as a team". Writing the rules in large letters helps prevent confusion. It’s totally fine to share opinions during the session! Time guidelines: 5 minutes for basic topics, 8–10 minutes for deeper discussions. Discussion flow: First, explain the sticky notes, then either "share your own thoughts" or "randomly ask someone who might have an opinion". (Keeping it casual makes it easier for everyone to speak up. Haha!) If you’re also sharing your own thoughts, preparing your sticky notes in advance helps you stay focused on facilitating. Most importantly, as long as everyone is aligned on "agreeing on the next action", any method will work. Once that’s decided, the retrospective will be worthwhile. You can almost forget about everything else. Feedback from Participants I always feel a bit anxious after implementing a new retrospective method with new members. Would KPT have been just fine...? Did I give too many instructions, leaving little time for actual reflection...? If you ever feel this way (like I do), don’t hesitate to ask your team for feedback! You’ll probably hear nothing but positive responses. It was refreshing to do a different kind of retrospective. It was fun. (Hot Air Balloon) We were able to talk while being conscious of the time box, so we were able to talk about a variety of topics, which is good as we usually end up talking about the same thing. The issues became clear. (LeanCoffee) We made a lot of mistakes, but this helped us distinguish between good mistakes and bad ones. (Celebration Grid) It was great to understand what makes my team members enjoy their work. I liked that we could simply share fun experiences. (FunDoneLearn) I’m glad that the issues I had in mind became a shared understanding within the team. I appreciate how direct and open the discussion was. (Elephants, dead fish, vomit) Summary Rather than sticking to just one method, teams can grow stronger by choosing and applying the retrospective format that best fits the situation. I’ve only tried six so far, but I'm excited to explore even more! As mentioned at the beginning, if you’ve only used KPT, I highly encourage you to try others too. Start by following the steps outlined in this article. Once you’re comfortable, why not tweak and adapt approaches to better fit your team? I’d be happy if this article helps Scrum Masters who are looking for better ways to run retrospectives.
Integrating Native Features into Flutter Apps – Our Approach to Adding an Android-Specific Camera Analysis Library Hello. My name is Osugi, and I’m part of the Toyota Woven City Payment development group. Our team develops the payment system used in Woven by Toyota ’s Toyota Woven City , covering a wide range of payment-related functions, from backend to Web frontend and mobile applications. So far, we’ve been using Flutter to develop a mobile app for Proof of Concept (PoC). In this article, we have summarized the trial and error we went through to overcome the challenges we faced when developing new functions by incorporating a new camera analysis library that is only available natively on Android/iOS into the PoC app. Introduction Integrating native functions into a Flutter app doesn’t just add to the development workload—it also increases maintenance costs, making development more challenging. In our project, considering the development timeline and available resources, we chose not to integrate native functions directly into the Flutter app. Instead, we developed a separate PoC app and a native app for camera analysis, linking them together to carry out the PoC. After completing the PoC, when we considered integrating the Flutter app with the camera analysis app, we found that the information on design guidelines and implementation methods for Flutter's native linking function was fragmented, and we felt that there were few systematic guidelines, especially for Android's complex UI configuration. In this article, we’ll focus on Android and share design principles and practical methods for incorporating native UI into a Flutter app. Hopefully, this will be helpful for engineers facing similar challenges. :::message At the time of writing, the sample code was created using Flutter v3.24.3 / Dart v3.5.3 ::: App Overview For the purposes of this article, we’ve simplified the app developed during the actual PoC. The app follows these specifications: Specifications When you press the start button, the camera preview will be displayed. The camera analysis function runs on the preview image, and the analysis results are sent as notifications. In this article, I would like to talk about this app. Data Integration Between Flutter and Native Android We implemented data exchange between Flutter and Android native using MethodChannel and EventChannel , enabling camera control from Flutter and analysis result notifications from Android native. MethodChannel is used for commands like starting and stopping the camera, while EventChannel is used for sending analysis result notifications. The sequence diagram below illustrates this process: sequenceDiagram actor u as User participant f as Flutter participant mc as MethodChannel participant ec as EventChannel participant an as Android Native u ->> f: press start button activate f f ->> mc: start camera mc ->> an: set up camera an -->> mc: mc -->> f: result deactivate f loop an ->> ec: analyzed result ec ->> f: send analyzed data f ->> f: show data end u ->> f: press stop button activate f f ->> mc: stop camera mc ->> an: reset camera an -->> mc: mc -->> f: result deactivate f Next, I would like to talk about how to display the Android native camera preview UI on the Flutter side. How to display native Android UI in a Flutter app There are three main ways to display native Android UI in a Flutter app: Texture widget – Displays an image rendered on an Android native Surface within the Flutter Widget tree. PlatformView – Embeds, displays, and controls Android native UI inside the Flutter widget tree. Intent – Launches a new Activity. We’ll go over the characteristics of each method and how to implement them. Texture Widget The Texture widget displays an image rendered on an Android native Surface within the Flutter Widget tree. In other words, it allows Flutter to draw native UI images directly to the GPU. This approach works well for use cases where latency isn’t a major concern, such as camera previews and video playback. However, for UI animations requiring real-time performance, adjustments must be made on the native side. This means a solid understanding of both Flutter and Android native development is necessary. Additionally, the Texture widget itself does not detect user interactions like touch events, so this must be handled on the Flutter side using GestureDetector or similar. That said, if it aligns with your requirements, it can be implemented relatively easily using the approach shown below. Implementation Steps First, obtain TextureRegistry . For Flutter apps, use FlutterEngine.FlutterRenderer ,which implements TextureRegistry . For Flutter plugins, retrieve it from FlutterPluginBinding. // For Flutter apps val textureRegistry = this.flutterEngine.renderer // For Flutter plugin val textureRegistry = this.flutterPluginBinding.textureRegistry Next, create a textureEntry , which is a SurfaceTexture , from the textureRegistry , then set up a SurfaceProvider to provide a Surface to the CameraX preview instance. Once this is done, you’re all set. This Surface acts as the drawing buffer mentioned earlier. val textureEntry = textureRegistry.createSurfaceTexture() val surfaceProvider = Preview.SurfaceProvider { request -> val texture = textureEntry?.surfaceTexture() texture?.setDefaultBufferSize( request.resolution.width, request.resolution.height ) val surface = Surface(texture) request.provideSurface(surface, cameraExecutor) { } } val preview = Preview.Builder().build().apply { setSurfaceProvider(surfaceProvider) } // To meet the requirements for camera analysis mentioned at the beginning of the article, // this can be achieved by setting up a cameraProvider and configuring the Preview and analysis processing for the camera. try { camera = cameraProvider?.bindToLifecycle( this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis, // Set the camera image analysis process here ) } catch(e: Exception) { Log.e(TAG, "Exception!!!", e) } Then, simply return the ID of the TextureEntry associated with the Surface to Flutter as the return value of MethodChannel . fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { result.success(textureEntry.id()) } "stopCamera" -> { stopCamera() } else -> result.notImplemented() } } To render a native SurfaceTexture on the Flutter side, simply set the textureId obtained from MethodChannel to the Texture widget, and the camera preview will appear in the Flutter app. static const platform = MethodChannel('com.example.camera_preview_texture/method'); int? _textureId; Future<void> onPressed() async { try { final result = await platform.invokeMethod<int>('startCamera'); if (result != null) { setState(() { _textureId = result; }); } } on PlatformException catch (e) { print(e.message); } } Widget build(BuildContext context) { if (_textureId == null) { return const SizedBox(); } return SizedBox.fromSize( size: MediaQuery.of(context).size, child: Texture( textureId: _textureId!, ), ); } For an implementation using the Texture widget, the mobile_scanner serves as a great reference. PlatformView PlatformView allows embedding Android native UI into Flutter’s widget tree, making it possible to display and control it. There are three rendering modes for PlatformView : Virtual Display ( VD ), Hybrid Composition ( HC ), and TextureLayerHybridComposition ( TLHC )[^1]. When using the PlatformView API, TLHC is selected by default. However, if the Android native UI tree contains SurfaceView , it will fall back to VD or HC [^2]. In addition, Texture improves frame rate synchronization between Flutter and Android native, which was not possible with the Texture widget. It also allows user interaction control and supports displaying UI elements beyond just camera previews and videos. Implementation Steps In this sample code using PlatformView , the camera preview screen is implemented with Jetpack Compose. To use Jetpack Compose in a Flutter app, add the following dependencies and configuration to app/build.gradle : android { ~ ~ buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.4.8" } } dependencies { implementation("androidx.activity:activity-compose:1.9.3") implementation(platform("androidx.compose:compose-bom:2024.04.01")) implementation("androidx.compose.material3:material3") } Now, let’s dive into the details of the implementation. Implementing PlatformView requires the following three steps: Implement NativeView that inherits PlatformView Implement NativeViewFactory that inherits PlatformViewFactory Register PlatformViewFactory to FlutterEngine 1. Implementing NativeView For a general implementation, please refer to Official . One key difference from the official approach is that this implementation uses Jetpack Compose. Here, the CameraPreview (built with Jetpack Compose) is embedded into the Android native View tree using ComposeView . class NativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?, methodChannel: MethodChannel, eventChannel: EventChannel) : PlatformView { private var nativeView: ComposeView? = null override fun getView(): View { return nativeView!! } override fun dispose() {} init { nativeView = ComposeView(context).apply { setContent { CameraPreview(methodChannel, eventChannel) } } } } In the Jetpack Compose implementation, PreviewView from CameraX, which is a View , is Composed using AndroidView . As a side note, AndroidView can also be used for Fragment . @Composable fun CameraPreview(methodChannel: MethodChannel, eventChannel: EventChannel) { val context = LocalContext.current val preview = Preview.Builder().build() val previewView = remember { PreviewView(context) } suspend fun startCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() // To meet the requirements for camera analysis mentioned at the beginning of the article, // this can be achieved by setting up a cameraProvider and configuring the Preview and analysis processing for the camera. cameraProvider.bindToLifecycle( LocalLifecycleOwner.current, CameraSelector.Builder().requireLensFacing(CameraSelector.LENS_FACING_BACK).build(), preview, analysis, // Set the camera image analysis process here ) preview.surfaceProvider = previewView.surfaceProvider } suspend fun stopCamera(context: Context) { val cameraProvider = context.getCameraProvider() cameraProvider.unbindAll() } LaunchedEffect(Unit) { fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when(call.method) { "startCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { startCamera(context) } } } result.success("ok") } "stopCamera" -> { runBlocking { CoroutineScope(Dispatchers.Default).launch { withContext(Dispatchers.Main) { stopCamera(context) } } } } else -> result.notImplemented() } } methodChannel.setMethodCallHandler(::onMethodCall) } AndroidView(factory = { previewView }, modifier = Modifier.fillMaxSize()) } Next, 2. implement NativeViewFactory and 3. register it to FlutterEngine as follows. class MainActivity: FlutterFragmentActivity() { ~ ~ override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) val methodChannel = MethodChannel( flutterEngine.dartExecutor.binaryMessenger, METHOD_CHANNEL ) val eventChannel = EventChannel( flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL ) flutterEngine .platformViewsController .registry .registerViewFactory(VIEW_TYPE, NativeViewFactory(methodChannel, eventChannel)) } } class NativeViewFactory( private val methodChannel: MethodChannel, private val eventChannel: EventChannel ) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { override fun create(context: Context, viewId: Int, args: Any?): PlatformView { val creationParams = args as Map<String?, Any?>? return NativeView( context, viewId, creationParams, methodChannel, eventChannel ) } } Finally, here is the implementation on the Flutter side. PlatformViewsService.initSurfaceAndroidView() is an API for using either TLHC or HC . PlatformViewsService.initAndroidView() allows you to use either TLHC or VD . PlatformViewsService.initExpensiveAndroidView() forces the use of HC . class CameraPreviewView extends StatelessWidget { final String viewType = 'camera_preview_compose'; final Map<String, dynamic> creationParams = <String, dynamic>{}; CameraPreviewView({super.key}); @override Widget build(BuildContext context) { return PlatformViewLink( viewType: viewType, surfaceFactory: (context, controller) { return AndroidViewSurface( controller: controller as AndroidViewController, hitTestBehavior: PlatformViewHitTestBehavior.opaque, gestureRecognizers: const <Factory<OneSequenceGestureRecognizer>>{}, ); }, onCreatePlatformView: (params) { return PlatformViewsService.initSurfaceAndroidView( id: params.id, viewType: viewType, layoutDirection: TextDirection.ltr, creationParams: creationParams, creationParamsCodec: const StandardMessageCodec(), onFocus: () { params.onFocusChanged(true); }, ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) ..create(); }, ); } } By using PlatformView this way, you can integrate Android native UI into your Flutter app. Intent Intent is an Android feature (not specific to Flutter) that allows launching an Activity separate from the MainActivity where Flutter runs. With Intent, you can navigate to another screen within your app, launch external apps, and exchange data between Activities. The two methods mentioned above (Texture widget and PlatformView) have been reported to have performance issues [^3]. To resolve these issues, a deep understanding of both Flutter and Android native is essential. In some cases, building a separate Android app might actually help keep development costs down. However, this poses a different challenge. If your team only has Flutter engineers, you will need to catch up on Android development. If the app is developed as an external application, the interface between apps must include security measures and be designed with lifecycle considerations in mind. For instance, the following measures may be necessary: Validate the data exchanged between activities. Restrict access so that only a specific app can call it. Ensure the called app functions correctly even if the calling app has been killed. Now let’s take a look at how to use Intent in Flutter. First, we’ll go over how to call another Activity from a Flutter app. Calling Activity (MainActivity where the Flutter app runs) override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { if (call.method!!.contentEquals("startCamera")) { val dummyData = call.argument<String>("dummy_data") ?: return result.error( "ERROR", "data is invalid", null ) // In case of screen transition val intent = Intent(this, SubActivity::class.java) // For external apps val packageName = "com.example.camera_preview_intent" val intent = activity.packageManager.getLaunchIntentForPackage(packageName) ?: return result.error( "ERROR", "unexpected error", null ) intent.setClassName(packageName, ".SubActivity") // Store the sending data intent.putExtra("EXTRA_DUMMY_DATA", dummyData) intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) activity.startActivityForResult(intent, REQUEST_CODE) } } override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) { eventSink = sink } override fun onCancel(arguments: Any?) { eventSink = null } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean { if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { val result = data.getStringExtra("RESULT_DATA") ?: "", eventSink?.success(result) return true } return false } Next, let’s implement the Activity that gets called from the Flutter app. Once a specific operation is completed, you can use Intent to return data, as shown below. Target Activity val intent = Intent() intent.putExtra("RESULT_DATA", resultData) activity.setResult(Activity.RESULT_OK, intent) finish() By using Intent this way, you can avoid dealing with complex UI control on both the Flutter and native Android sides while enabling data exchange between Flutter and native Android Activities. However, security and data integrity must be carefully considered in this approach. Summary In this article, we've discussed how to incorporate native functionality into Flutter apps, with a focus on Android. Data communication between Flutter and Android native was achieved using MethodChannel and EventChannel . Here’s how to incorporate Android native UI into Flutter: Texture widget Great for camera previews and video displays, and relatively easy to implement. However, it requires handling user interactions and may have some performance issues. PlatformView Lets you integrate native UI into Flutter’s widget tree while enabling user interaction control. Supports embedding View, Fragment, and Jetpack Compose. Performance can also be an issue. Intent Allows seamless screen transitions and launching of other apps, making it possible to directly display Android’s UI and exchange data. However, security and data handling require careful attention. As mentioned above, each method comes with its own strengths and limitations when integrating Android native features into a Flutter app. The best choice depends on your project’s specific needs. Notes The thumbnail of the Droid is reproduced or modified from work created and shared by Google and used according to terms described in the Creative Commons Attribution 3.0 License. [^1]: Hosting native Android views in your Flutter app with Platform Views [^2]: Android Platform Views [^3]: Performance
This article is the entry for day 16 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hi, I’m Nakanishi from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. This year, the Manabi-no-Michi-no-Eki project was officially launched and later established as a team. As part of this initiative, we're also hosting an in-house podcast, and for this year’s Advent Calendar, we’d love to share more episodes with you. What is the Manabi-no-Michi-no-Eki (Learning Roadside Station)? It’s a project aimed at making the frequently held in-house study sessions more accessible and effective. The initiative is led by passionate volunteers within the company, with the goal of supporting study sessions and fostering a culture of knowledge sharing across the organization. 10X Innovation Culture Program The Learning Roadside Station Podcast features interviews with employees who organize study sessions within the company. This segment is called “A Peek into the Study Session Next Door”. For today’s podcast, we’re joined by Awata-san and HOKA-san, who are working on the 10X Innovation Culture Program being provided by Google. Usually, HOKA-san conducts the interviews, but today, Akeda-san and I will be taking on that role. So, without further delay, let's jump right into the interview. Interview Awata-san: Thank you for having me. My regular work focuses on platform engineering, ensuring that database-based operations are accessible to everyone. Besides that, I’m interested in corporate culture, so I’m doing a variety of activities. HOKA-san: I usually work in the Human Resources Group’s Organizational Human Resources Team. We plan and conduct training by identifying needs and challenges through interviews with both new and existing employees. Akeda-san: Please tell us what prompted you to hold these study sessions. Awata-san: As part of our Google Cloud Enterprise User Group ( Jagu’e’r ) I took part in a subcommittee for thinking about corporate culture and innovation. As part of our activities, we decided to try the 10X Innovation Culture Program, gathered around 15 volunteers, and went ahead with it. HOKA-san was also among them, and things took off from there. HOKA-san: Yes, that’s right. When we held our first event at the Google office in Shibuya, the reaction from the participants was extremely good. Collaborating in a workshop with people we usually had no interaction with opened up new opportunities for communication. Akeda-san: Next, please tell us some details about the events. How did they expand after the initial session? Awata-san: We initially held them at the Google office, then subsequently shifted to holding them in-house. The in-house events also drew a large number of participants and got an extremely positive reaction. HOKA-san: Seeing KTC employees engage so positively, the Google team also expressed high praise. We hope to go on spreading this program further both inside and outside the company. Akeda-san: What are the prospects for the future? Awata-san: In the future, we’d like to become certified facilitators, and get to spread 10X to other companies as well. HOKA-san: First, we plan to roll it out to other in-house groups and cultivate a culture of innovation across KTC. Akeda-san: What kind of organization do you envision for KTC? Awata-san: I want to make it a vibrant hive of thinking outside the box, flexible communication, and collaboration. HOKA-san: I want to create a culture where people can take on challenges without fearing failure. To achieve this, I plan to utilize the 10X methods. Akeda-san: Finally, could you share a message with all our listeners? Awata-san: Culture isn’t something that can be imposed; it naturally emerges from people's actions. If you're interested, we’d love for you to join us. HOKA-san: If you're interested, feel free to start by just taking a look—don’t hesitate to reach out. Through the 10X Innovation Culture Program, we aim to make KTC a more collaborative and supportive organization to work in. If you’re interested, please contact Awata-san or HOKA-san. In this article, we shared insights into the 10X Innovation Culture Program, its background, and what the future may hold for it. We hope you’re looking forward to the next study session as well!