TECH PLAY

KINTOテクノロジーズ

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

936

This article is the entry for Day 5 of the KINTO Technologies Advent Calendar 2024 Hi, I'm Ryomm, also known as "The Phantom Bot Craftsman", and I develop the My Route iOS app at KINTO Technologies. Here’s a quick tip from Manabyi . Although this article focuses on Slack CLI, it’s important to note that it operates using the Slack API internally. Consequently, the same issue can arise when interacting directly with the Slack API. Background While using Slack CLI, I encountered an issue where Block Kit messages failed to send under the following conditions: A form input contains a value in rich_text format. The value is stored in the DataStore as rich_text. Multiple rich_text entries are retrieved from the DataStore, concatenated, and formatted. A block is created and sent using postMessage However, the process failed with the error: parameter_validation_failed . ![Error](/assets/blog/authors/ryomm/2024-12-04-2/02.png =600x) Cause The error was triggered due to an invalid parameter during message transmission. Upon investigation, I found that the block_id values were duplicated in the block I was trying to send, as shown below. [ { "type": "rich_text", "block_id": "xdrwH", // <- this "elements": [ /* ... */ ] }, { "type": "rich_text", "block_id": "xdrwH", // <- this "elements": [ /* ... */ ] } ] It appears that the message can't be sent to Slack because the block_id in the message you're trying to send conflicts with an existing one. block_id block_id is a unique identifier for a block. The official documentation explains it as follows: A unique identifier for a block. If not specified, a block_id will be generated. You can use this block_id when you receive an interaction payload to identify the source of the action. Maximum length for this field is 255 characters. block_id should be unique for each message and each iteration of a message. If a message is updated, use a new block_id. https://api.slack.com/reference/block-kit/blocks If you create a block without specifying a block_id , a block_id will be automatically generated. This is mainly used for interactive elements, such as identifying which block a button was pressed on during user interactions. A block_id must be unique within a single message or across a sequence of repeated messages (i.e., a series of bidirectional interactions). Additionally, when a message is updated, a new block_id must be used. In this case, the block_id contained in the received rich_text was automatically generated. Additionally, since the rich_text input was received as separate messages during the initial process, block_id duplication was likely to occur. As a result, this issue occurred due to a conflict with the auto-generated block_id Here is a Haiku for this occasion. Thought it was safe, Auto-generated failed me, Block_id conflict This haiku reflects my shock and disbelief—I had believed that an auto-generated block_id, like a UUID, wouldn’t easily collide, yet it did. This is just my speculation, but it seems that Slack generates block_id based on the block’s content. To test this, I inputted the exact same text multiple times, and each time, the same block_id was generated. ![Error](/assets/blog/authors/ryomm/2024-12-04-2/03.png =600x) Type hoge in rich_text Each time, the generated block_id was RlmLN . { "type": "rich_text", "block_id": "RlmLN", "elements": [ { "type": "rich_text_section", "elements": [ { "text": "hoge", "type": "text" } ] } ] } Therefore, if there’s a possibility of identical inputs, it’s safe to assume that block_id collisions are just as likely. Solution When merging multiple blocks into a single message for Slack, each block_id within the message must be unique. The simplest solution is to delete the block_id , especially if interactivity isn’t needed. Since Slack will automatically generate a new block_id if one isn’t specified, you can remove it and proceed with sending the message. Here’s part of the formatting method: The delete operator is used to remove the block_id property from the object. // The items obtained from client.apps.datastore.query are included in the event argument // Reference: https://api.slack.com/methods/apps.datastore.query#examples function eventMessage(event) { // ... event.description.forEach((description) => { if (description.block_id) { delete description.block_id // 🐈❗️ } message.push(description) }) // ... } Now, you can send messages without worrying about block_id conflicts! However, removing block_id from the object is not the best approach if you need interactive functionality. In that case, it’s best to generate and assign a block_id on the application side before sending the message. Conclusion This was a story about block_id conflicts preventing messages from being sent!
アバター
This aricle is the entry for day 19 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Hello, I am GOSEO, an iOS engineer in the Mobile App Development Group at KINTO Technologies. Currently, I’m working on the app Unlimited. Interestingly, 66% of the iOS developers working on Unlimited are from overseas, and I enjoy chatting with them in English every day. I am also a fan of Paradox games and am currently hooked on Crusader Kings Ⅲ. This article is the entry for day 19 in the [KINTO Technologies Advent Calendar 2024] Evaluating the migration from Google Maps to MapKit in the Unlimited App In recent years, improvements in Apple Map’s performance have sparked interest in transitioning from Google Maps to MapKit. This shift is expected to reduce usage fees and enhance app performance and user experience. In this article, I will walk you through the implementation process, the challenges we encountered, and the outcomes of migrating from Google Maps to MapKit in the Unlimited app. Evaluating the migration from Google Maps to MapKit and its Process 1. Rendering maps and creating gradient lines In the Unlimited app, gradient lines are rendered on Google Maps. To replicate this functionality in MapKit, we tested the use of MKGradientPolylineRenderer . By setting colors and specifying the start and end points using locations , we examined whether this implementation would work effectively. Additionally, I considered that this feature could be used in the future to dynamically change the line's color based on the user's speed exceeding the limit. func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { if let polyline = overlay as? MKPolyline { let gradientRenderer = MKGradientPolylineRenderer(polyline: polyline) gradientRenderer.setColors( [Asset.Colors.primary.color, Asset.Colors.cdtRouteGradientLight.color], locations: [0.0, 1.0] ) gradientRenderer.lineWidth = 2.0 return gradientRenderer } return MKOverlayRenderer(overlay: overlay) } 2. Differences in tap detection In Google Maps, detecting taps on the map or markers is straightforward. However, MapKit does not offer a built-in API for this functionality. To detect taps on the map itself, we used UITapGestureRecognizer . For marker taps, we handled them using the didSelect and didDeselect methods. It was a bit challenging to figure out whether a tap was on the map or a marker, but we resolved this by checking if there was a marker at the tapped location. Challenge: Setting up custom gestures required extra effort, but we were able to confirm that it works as intended. let tapGestureRecognizer = UITapGestureRecognizer(target: context.coordinator, action: #selector(context.coordinator.handleMapTap(_:))) tapGestureRecognizer.delegate = context.coordinator mapView.addGestureRecognizer(tapGestureRecognizer) 3. Adding and managing markers On the Unlimited map, multiple types of markers often overlap. To handle this, we implemented a system using zPriority to display markers in order of importance. By reusing instances of the same marker image, we avoided generating separate instances for each marker, which improved performance. Challenge: The default tap animation wouldn’t go away... After much trial and error, we found a solution. Instead of adding an image directly to MKAnnotationView, we added a UIView as a subview of the annotation view, then added a UIImageView to the UIView, and finally set the image on the UIImageView . This effectively disabled the default animation. The solution was truly a stroke of genius from one of my teammates! 4. Slow rendering When we tested the map using driving data from Oita to Fukuoka, we found a bug where the line rendering couldn’t keep up during repeated zooming in and out after the map was displayed. The dataset contained 23,000 coordinate points, and rendering occurred every time the map view changed. This caused significant memory and CPU resource consumption during UI updates. Challenge: Rendering couldn’t keep up with large numbers of coordinate points. We tackled this by using the Ramer-Douglas-Peucker algorithm to simplify and reduce similar coordinate points. This allowed us to consolidate the data into a single polyline by simplifying and segmenting the polylines. // A function to interpolate UIColor. Returns the color interpolated between two colors based on the fraction value func interpolateColor(fraction: CGFloat) -> UIColor { // Retrieve the RGBA components of the start and end colors let fromComponents = Asset.Colors.primary.color.cgColor.components ?? [0, 0, 0, 1] let toComponents = Asset.Colors.cdtRouteGradientLight.color.cgColor.components ?? [0, 0, 0, 1] // Interpolate colors based on the fraction let red = fromComponents[0] + (toComponents[0] - fromComponents[0]) * fraction let green = fromComponents[1] + (toComponents[1] - fromComponents[1]) * fraction let blue = fromComponents[2] + (toComponents[2] - fromComponents[2]) * fraction return UIColor(red: red, green: green, blue: blue, alpha: 1) } // A function to generate polyline information from an array of coordinates func makePolylines(_ coordinates: [CLLocationCoordinate2D]) -> [PolylineInfo] { // Return an empty array If the coordinates array is empty guard !coordinates.isEmpty else { return [] } // Calculate the chunk size (at least the entire set as one chunk) let chunkSize = coordinates.count / 20 > 0 ? coordinates.count / 20 : coordinates.count var cumulativeDistance = 0.0 Let totalDistance = coordinates.totalDistance() // Calculate the total distance var previousEndColor: UIColor = Asset.Colors.primary.color var previousEndCoordinate: CLLocationCoordinate2D? var polylines: [PolylineInfo] = [] // Divide the coordinates into chunks and process each chunk let chunks = stride(from: 0, to: coordinates.count, by: chunkSize) .map { startIndex -> [CLLocationCoordinate2D] in // Retrieve the coordinates of the chunk and add the last coordinates from the previous chunk var chunk = Array(coordinates[startIndex..<min(startIndex + chunkSize, coordinates.count)]) if let lastCoordinate = previousEndCoordinate { chunk.insert(lastCoordinate, at: 0) } previousEndCoordinate = chunk.last return chunk } for chunk in chunks { Let chunkDistance = chunk.totalDistance() // Calculate the distance of the chunk Let startFraction = cumulativeDistance / totalDistance // Calculate the fraction for the start point cumulativeDistance += chunkDistance Let endFraction = cumulativeDistance / totalDistance // Calculate the fraction for the end point let startColor = previousEndColor let endColor = interpolateColor(fraction: CGFloat(endFraction)) // Calculate end color using interpolation previousEndColor = endColor // Simplify the polyline (reduce points while maintaining high accuracy) let simplified = PolylineSimplifier.simplifyPolyline(chunk, tolerance: 0.00001) let polyline = MKPolyline(coordinates: simplified, count: simplified.count) // Add the polyline information to the list polylines.append(PolylineInfo( polyline: polyline, startFraction: startFraction, endFraction: endFraction, startColor: startColor, endColor: endColor )) } return polylines } // A function to simplify coordinates (implements the Ramer-Douglas-Peucker algorithm) static func simplifyPolyline(_ coordinates: [CLLocationCoordinate2D], tolerance: Double) -> [CLLocationCoordinate2D] { // Return the coordinates as is if there are 2 or fewer points, or if the tolerance is less than 0 guard coordinates.count > 2 else { return coordinates } guard tolerance >= 0 else { return coordinates } var result: [CLLocationCoordinate2D] = [] var stack: [(startIndex: Int, endIndex: Int)] = [(0, coordinates.count - 1)] var include: [Bool] = Array(repeating: false, count: coordinates.count) include[0] = true include[coordinates.count - 1] = true // Process recursively using a stack while !stack.isEmpty { let (startIndex, endIndex) = stack.removeLast() let start = coordinates[startIndex] let end = coordinates[endIndex] var maxDistance: Double = 0 var currentIndex: Int? // Find the farthest point from the current line for index in (startIndex + 1)..<endIndex { let distance = perpendicularDistance(point: coordinates[index], lineStart: start, lineEnd: end) if distance > maxDistance { maxDistance = distance currentIndex = index } } // If the farthest point exceeds the tolerance, include it and subdivide further if let currentIndex, maxDistance > tolerance { include[currentIndex] = true stack.append((startIndex, currentIndex)) stack.append((currentIndex, endIndex)) } } // Add only the coordinates where include is true to the result for (index, shouldInclude) in include.enumerated() where shouldInclude { result.append(coordinates[index]) } return result } // A function to calculate the vertical distance between a point and a line private static func perpendicularDistance(point: CLLocationCoordinate2D, lineStart: CLLocationCoordinate2D, lineEnd: CLLocationCoordinate2D) -> Double { let x0 = point.latitude let y0 = point.longitude let x1 = lineStart.latitude let y1 = lineStart.longitude let x2 = lineEnd.latitude let y2 = lineEnd.longitude // Distance formula (distance between a point and a line in a 2D plane) let numerator = abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) let denominator = sqrt(pow(y2 - y1, 2) + pow(x2 - x1, 2)) // If the length of the line is 0, set the distance to 0 return denominator != 0 ? numerator / denominator : 0 } 5. Verification Results and Conclusions We confirmed that similar functionality implemented with Google Maps in the Unlimited app can also be achieved using MapKit. This makes migration from Google Maps to Mapkit feasible. Additionally, this migration is likely to lower usage fees. Through this research, the Unlimited iOS team as a whole has gained a deeper understanding of MapKit’s capabilities. Conclusion Looking ahead, we plan to continue development using MapKit for this project. We will keep striving for further improvements to deliver even better services!
アバター
This article is the entry for Day 12 of 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 structured as an organization. As part of our initiatives, we also run an in-house podcast, and for this year’s Advent Calendar, we’d like to share more about it. What is Manabi-no-Michi-no-Eki (The Learning Roadside Station) It's a project aimed at making the in-house study sessions, which are frequently held, 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. Joint Study Group For the first episode of the Learning Roadside Station Podcast, we interviewed the organizing members Asahi-san, Kiyuno-san, and Rina-san about a joint study session held within the company. In this podcast, we discussed in detail the background and purpose of the study sessions, key aspects of their management, and future plans. Interview HOKA-san (interviewer): First, could you tell us how the Learning Roadside Station project started? HOKA-san: This project was launched to further promote a positive learning culture within the company. It all started when Kageyama-san and several others came together to create a system to support in-house study sessions. HOKA-san: Can you tell us more about the background and purpose of the Joint Study Group? Asahi-san: During the new employee orientation, I realized that there were few opportunities to learn about information related to other systems. That’s when I felt the need for a dedicated space to catch up on the latest information, which led to the launch of the Joint Study Group. HOKA-san: What are some of the key aspects you focus on when organizing study sessions? Rina-san: We aim to share the latest information about other systems and products to provide a broader range of knowledge. Additionally, to increase participation, we make sure to thoroughly prepare in advance, and the organizing team members themselves are actively involved. HOKA-san: What were the results and feedback from the first study session? Kiyuno-san: Initially, we expected around 34 participants, but in the end, we had about 80 attendees, including those who joined via Zoom. We received a lot of positive feedback, such as being able to connect with colleagues they don’t usually interact with and getting access to the latest updates on other products. HOKA-san: How do you plan to continue the study sessions, and what are your future initiatives? Asahi-san: Moving forward, we plan to create thematic discussion sessions and networking opportunities based on job roles, so that more employees can participate easily. Rina-san: We have already decided on the next speaker, and we are committed to continuing these sessions regularly. Additionally, we will work on enhancing information sharing both internally and externally. HOKA-san: What is the purpose of promoting the Learning Roadside Station Project outside the company? Rina-san: By sharing information externally, we hope to attract more participants both inside and outside the company. Through this, we aim to foster a stronger learning culture within the company. Hoka-san: Lastly, how do you feel after this interview? Asahi-san: I gained a new perspective by experiencing both the participant’s and the organizer’s viewpoints. Being on the organizing side made me realize the importance and significance of these events, and it strengthened my motivation to make them even better. Kiyuno-san: Until now, I had only attended via Zoom, but by taking on a role in the organizing team, I realized how valuable audience reactions and comments are. I would like to continue actively participating in these sessions in the future. Summary The goal of the Learning Roadside Station project is not just to provide a space for study sessions, but to enrich the company culture through interaction and knowledge sharing among employees. By continuously holding study sessions and enhancing communication efforts, we aim to create an environment where more employees can participate, learn from each other, and grow together. This will contribute to ultimately improve overall skills and foster a strong sense of unity within the company. In this article, we shared the details of the Joint Study Group, the background of its operations, and future plans. Please look forward to the next study session!
アバター
はじめに こんにちは、9月入社のMizukiです! 本記事では2024年8、9月入社のみなさまに、入社直後の感想をお伺いし、まとめてみました。 KINTOテクノロジーズに興味のある方、そして、今回参加下さったメンバーへの振り返りとして有益なコンテンツになればいいなと思います! K.W 自己紹介 グループコアシステム部  共有サービス開発G JP会員PFチーム所属のPdMです。 新卒で大手webサービス会社でPdMとして入社し、KINTOテクノロジーズ(KTC)は2社目となります。 所属チームの体制は? JP会員PFチームは、日本のKINTOのWEBサービスやモバイルアプリで利用される会員機能の開発をしております。 チームリーダー1名、PdM1名、エンジニア6名(協力会社さん含む)の、計8名 KTCへ入社したときの第一印象?ギャップはあった? 中途入社のみの構成で毎月のように新しい人が入ってくるため、新参者を受け入れてくれる雰囲気をすごく感じました。 ギャップとしては、自分自身初めての転職だったので入社前は貢献できるか不安でしたが、メンバーのサポートもあり思ったよりも溶け込めているかなと感じてます。 現場の雰囲気はどんな感じ? KTC自体まだ新しい会社なので、ルールや仕組みを作りながらの段階に思いますが、 その分、各メンバーは主体的で新しい開発手法や取り組みを積極的に取り入れる文化があると感じました。 ブログを書くことになってどう思った? ブログなどを通して発信することがほとんどなかったので、過去の入社エントリーも見ながらじっくり書きました。 MKさんからの質問:オフィスで使っているおすすめのグッズを教えてください! オフィスではなく、自宅で使用しているものですが昇降デスクはオススメです。 こちらを使ってます。 https://www.amazon.co.jp/dp/B08CXSV3RX Shiori ![alt text](/assets/blog/authors/mizuki/20250123/suda.png =250x) 自己紹介 IT/IS部、生成AI活用PJTでGenerative AI Engineerをしています。 メイン業務は、生成AIの活用推進です。業務としては、生成AI研修の実施、ユースケース開発、生成AIの技術支援をしております。 所属チームの体制は? 自分を含め5人のメンバーで、名古屋3名、神保町1名、室町に1名(自分)という体制です。 KTCへ入社したときの第一印象?ギャップはあった? 毎月テーマに沿った勉強会、定期的なトヨタグループの技術勉強会、社内研修を開催していたり、業務に関連する外部のセミナーに参加できたり、技術をキャッチアップしてアウトプットする環境が整備されていると感じました。 Tech Blogや登壇など会社が発信することに積極的で、後押しをしてくれる環境が整っており、入社4ヶ月目で社外向けのセミナーで、社内での生成AI活用の取り組みを登壇する機会を得ることができました。 入社して「面着」という言葉を知りました。これはトヨタ用語で「直接会ってコミュニケーションを取る」というニュアンスの言葉なのですが、対面とオンラインのコミュニケーションをバランスよく取り入れる面着文化が、コミュニケーション齟齬を低減し、迅速な意思決定や価値創出の促進に寄与していると感じました。 トヨタの内製開発組織ということで、KINTOなどのモビリティ製品だけではなく、ファイナンス系、MaaS系、車のシステム系技術支援、販売店のDX、生成AIの技術支援など業務領域が多岐に渡っており、様々な領域について知見を広げる機会があります。 意思決定が民主的だと感じることが多いです。例えば、オフィスのドリンクのフレーバーは春夏と秋冬で入れ替えをしているのですが、入れ替え時に「好みのフレーバーアンケート」が実施され、きめ細かい体制にとても驚きました。)現在の秋冬フレーバーはアンケートの結果に基づくフレーバーです ) 現場の雰囲気はどんな感じ? Slackでもオフラインでも「ちょっと困った」を聞いた時に、丁寧にフォローしていただいており、とても温かい環境だと感じています。 ランチなども気軽にお誘いしやすく、部署以外のメンバーとも仲良くなる機会があり、とても楽しいです。 ブログを書くことになってどう思った?  ✌️😆✌️ K.Wからの質問:座右の銘は何かありますか。 座右の銘と言えるかはわかりませんが、2024年・2025年の努力テーマはあります。"Pivot" & "思い立ったが吉日"の2つです。生成AIの領域は、技術や情報の更新スピードがとても早いです。大々的な技術発表が定期的に発生し、「今日から昨日とは違った世界観が当たり前になる」ことが多々あります。そのような状況では、うまくいくorいかないかがわからない中、思い立ったことをすぐに検証して効果測定して、軌道修正して実行するというようなことを、素早いサイクルで回すことが必要になってきます。そのため、業務のちょっとしたことから日常生活まで、「思い立つ→行動する」までの時間を短縮できるよう心がけています。 ゆっき〜 ![alt text](/assets/blog/authors/mizuki/20250123/yukki-.jpeg =250x) 自己紹介 はじめまして、ゆっき〜です。IT/IS部 コーポレートITグループに所属しています。 社内情報システムをはじめ、ネットワークインフラやセキュリティなど幅広い分野で課題を探して解消に向けて動く業務をしております。 所属チームの体制は? チームはBoostチームに所属しており、メンバーは2名です。 コーポレートITグループの課題解決や後方支援を目的としたチームで、それぞれのメンバーが得意領域において課題解決に日々励んでおります。 現在はKINTOの各種システム入れ替えに伴うインフラ設計などに携わり、守破離を意識しつつ改善活動を進めております。 ※守破離とは、まずは既存の方法を守り(守)、次にそれを破り(破)、最終的には新しい方法を離れて創造する(離)プロセスです。 KTCへ入社したときの第一印象?ギャップはあった? 入社前はスタートアップとしてスピード感もって動いているとはいえ、大企業の傘下のため、ある程度制約があるものだと思っていました。 入社してみると、かなりの自由度や裁量が高く、ベンチャー気質のあるスピード感を持った会社だと感じました。 一方で、守るべき箇所はしっかりとガバナンスが効いており、スピード感と安定性が両立できているなと感じました。 また、在宅については、基本原則として週2回と定められてはいるものの、家庭の事情などによりやむを得ない場合などは柔軟に対応してくださることは感謝しています。 現場の雰囲気はどんな感じ? 思ったよりも在宅勤務している方も多く、同じ座席列に自分しか居ないという日もあり驚きました。 周りが在宅勤務を積極的に活用しているため、在宅勤務しやすい環境だなと感じました。 とはいえ、常設されたビデオ会議システムやSlack等のIT設備が整っているため、気軽に話しかけられる環境は整っており、安心できる環境です。 上長は声を掛けるとすぐに相談にのってくださるのも非常に助かっています。 また、グループの取り組みではあるものの、週1回30分の雑談タイムがあり、東名阪と物理的に距離があるメンバーとも気軽に雑談できる機会があるのもとても良いと感じています。 ブログを書くことになってどう思った? 同期入社とはいえ、物理的な距離があったりとなかなか話す機会がなかったですが、この入社者エントリーをきっかけに同期入社の多拠点の方たちとも接点を持てる良い機会だと思いました。 また、KTC入社前までは役割的にあまり積極的に情報発信ができない立場だったのですが、今後は少しずつアウトプットの機会を増やせたらなと思います。 Shioriさんからの質問:カメラが趣味と伺いましたが、上手くなる・撮影のコツがあれば教えていただきたいです。 まだまだ練習中の身ではあるのですが、最近意識していることは、遠景・中景・近景のバランスや画面内の要素の配置バランスなどを意識して取り組んでいます。あとは季節ごとにいろんな被写体を探しに行くようにして、撮影機会を増やすようにしています。 Mizuki 自己紹介 開発支援部、人事G 採用チームに所属しております。 エンジニア採用や、大阪での採用イベント運営などもしております! 所属チームの体制は? 採用チームは6名です KTCへ入社したときの第一印象?ギャップはあった? ギャップは特になかったです。 もともと、採用エージェントとして企業担当をさせていただいていたので、雰囲気やカルチャーなどについても理解ができておりました!ですが、想像以上に社員一人一人に裁量を任されているんだなと感じております 現場の雰囲気はどんな感じ? 人事チームとしてとても大切にしているバリューの一つでもある「全てにリスペクトを」という点を、体現できているチームだなと感じています。 12月までOsaka tech labで働いていたのですが、拠点として”かなり”アットホームな雰囲気で衝撃でした。Osaka tech labがこのままの雰囲気でずっといれたらいいなと思っています。 ブログを書くことになってどう思った? 私が拠点が離れていることもあり、オリエンテーションもほとんどの方と顔を合わせるだけだったので、今回のブログで皆さんの趣味とか仕事内容が知ることができていい機会だなと思いました。 ゆっき〜さんからの質問:一番おすすめのつけ麺屋はどこですか? 拠点ごとに書いてみました!ぜひ食べに行ってみてください! 東京だと道玄坂マンモス(渋谷) https://tabelog.com/tokyo/A1303/A130301/13122700/ 大阪だと宮田麺児(心斎橋) https://miyata-menji.com 名古屋だと半蔵(藤が丘) https://www.menya-hanzo.com/hanzo.html R.K 自己紹介 モビリティプロダクト開発部 マーケティングプロダクトG に所属しています。 今は「Prism Japan」というお出かけアプリとwebサイトのPdMをしています。 1社目は人材系の会社でアプリ・webサイトのグロースを担当し、KTCが2社目です。 所属チームの体制は? チームリーダー兼エンジニア1名、エンジニア2名、マーケター1名、ライター1名、PdM1名です。 KTCへ入社したときの第一印象?ギャップはあった? 仲の良いチームとお伺いしていましたが、実際に入社してみてもその通りで、私自身初めての転職で力になれるか不安でしたが、チームの皆さんから手厚いサポートをいただき、なんとか頑張れています! 現場の雰囲気はどんな感じ? 成長期のプロダクトを担当しているため、チームメンバー全員が自分の役割からにじみ出て和気あいあいと仕事をしています! ブログを書くことになってどう思った? あまりブログを書く機会がなかったので、他の皆さんのコメントを参考にさせていただきました! Mizukiさんからの質問:今まで一番行ってよかった海外はどこですか? 定番を除くと、ニュージーランドがよかったです!南島をロードトリップしていろいろなスポットを巡ったのですが、自然が豊かで壮大で、ごはんがおいしくて、街がきれいで、時間がゆったりしていて、期待以上な旅になり、いつかまた行きたいなと思っています! 陳 自己紹介 中国厦門(アモイ)出身で、中国でインターネット業界のエンジニアとして約10年間働いてきました。KTC は初めて勤めた日本企業です。 現在はプラットフォーム開発部DBREに所属し、DB周りの運用ツールを開発しています。 MBTIタイプはINFPです。 趣味はとにかく新しいものにチャレンジすることです。 たった3ヶ月で神保町周辺ほぼ全ての飲食店に行きました(半分はカレーですが)。 東京の美術館、特に現代アートを巡るのが好きです(理解できないのは面白さ)。 新発売に弱い。 所属チームの体制は? 自分を含め5名が神保町に勤務しており、最近はSREと合併したため、合計7名です。 KTCへ入社したときの第一印象?ギャップはあった? 第一印象はオフィス見た目派手ではないが、伝統な日本企業でもない感じでした(外資系な雰囲気はある)。 オリエンでは、使う内部ツールや初期設定の情報量が多く、IT経験者である自分も慣れるのに時間がかかりました。 神保町オフィスは人が少ないから、静かで集中できる環境です。 多くが在宅勤務ですからと聞いて良いと感じました(中国では在宅勤務の会社が少ないため)。 現場の雰囲気はどんな感じ? チームメンバーはとても親切、真面目で、社内のクライアント(プロダクト側の方)やチームメンバーに細かく対応する態度は珍しいですが、DBREではそれが実際にあります。 オンボーディングでは、バックグラウンド(業務に当たっての知識)が不足していることで無視されることはなく、「質問があれば遠慮なく言ってください」と常に言われています。 また、新人期間の「お手並み拝見」をされたこともありません。 具体的な課題は、みんなで相談した後に開発に入ります。これまで独りで問題解決にしていたので最初は驚きましたが、慣れるとこのペースが心地よく感じます。他のメンバーの分析から学ぶことが多く、「全員合意した」意識を持つなら取り組む自信が倍増になります。 ブログを書くことになってどう思った? 自分のアウトプット能力を向上させたいと思っているので、これからも挑戦してみようと思います。 チームメンバーのテックブログを拝見し、自分が欠けているのはそのような整理能力だと感じました。 R.Kさんからの質問:神保町周辺のほぼすべての飲食店に行かれたとのことで、その中でもお気に入りのランチを教えてください! ありがとうございます!参考として、個人の好みは東南アジア料理です(酸味と甘みがあって、食材も豊富の方が)。 【タイ料理】 バンコック コスモ食堂が一番のおすすめです。夜でも価格がリーズナブルで、ボリュームも満点。どのメニューを選んでも間違いないです。 【ラーメン】 正直、「ラーメン二郎」は毎回3時間以上待ちで並べないんです...😅 その代わり「用心棒」がおすすめです。豚山よりも個人的に好きですね。あと「海老丸らーめん」も外せません。スープの旨みが本当に素晴らしいんです。 【韓国料理】 「京城園」食べた後で「孤独のグルメ」でも紹介された店だと知って驚きました。コンタンスープの定食が美味しくで、値段もリーズナブルです。 【インド料理】 1階にある「ナマステインディア」で十分美味しいと思います(実は個人的にインド料理店はあまり味の違いを感じないんです...😂) 最近、銀座で「グルガオン」というお店を見つけました。そこのチーズグルチャが美味しくて、わざわざ行く価値があると思います 【カレー】 以下の3店がおすすめです: パンチマハル(ラッシーが絶品です) タケウチ 神保町本店(注意:かなり辛いメニューもあります) チャントーヤ ココナッツカリー(ココナッツベースのスープカレーが印象的) T.A. ![alt text](/assets/blog/authors/mizuki/20250123/ki2.jpeg =250x) 自己紹介 IT/IS部 セキュリティプライバシーG所属 情報セキュリティの担当をしています。 趣味はソロ活動です。 所属チームの体制は? チームリーダー、委託先の方を含めて3名です。 ちょうど今が繁忙期なので、ヒーヒー言っております。 KTCへ入社したときの第一印象?ギャップはあった? 柔軟性とスピード感がある会社だなぁと思いました。 国籍やバックグラウンドが異なる方が多いので、雑談するだけでインプットできることが多いです。 現場の雰囲気はどんな感じ? 自日々改善という雰囲気があり、実際に毎週何かがアップグレードされている感覚もあるので、好奇心が刺激されます。 いかに効率的にやるかを考えることが多いので、自分自身もブラッシュアップされている感じがします。 ブログを書くことになってどう思った? 誤字脱字しないように気を付けなければ…。 陳さんからの質問:ソロ活動について具体的にどのようなことをしていますか 一人飲みや一人映画、ソロキャンプなどはよくやります。最近は一人でクリスマスマーケットに行きました。イルミネーションがきれいでした。 MK 自己紹介 モビリティプロダクト開発部 DX開発G 所属 フロントエンドエンジニア 販売店様の業務DX化をサポートするプロダクトの開発を行っています。 営業 → 事務 → フロントエンジニアという職歴です。 生ハムが好きです。 所属チームの体制は? 9人のチームで複数プロダクトを構築しています。 様々な技術を扱えるエンジニアばかりなので、領域を定めずプロジェクトに合わせてコーディングやプランニングを行っています。 KTCへ入社したときの第一印象?ギャップはあった? 入る前は正直お堅いのかな…?と思っていたのですが、入ってみるとかなり柔軟な会社でした。 TOYOTAの安定感と、まだ新しい会社の良いとこ取りという印象です。 現場の雰囲気はどんな感じ? 技術への想いがアツいです。 プロダクトへの想いもアツいです。 コミュニケーションも活発で、活気があります。 ブログを書くことになってどう思った? 実際に開発しているメンバーの声は外部の方にとって良い情報源になりそうだな、と思いました! エントリーの皆さんの会社の印象を読めるのも楽しみです。 T.A.さんからの質問:子供のころに信じていた(騙されていた)迷信や嘘はありますか? 鳩の首の紫の部分に毒がある さいごに みなさま、入社後の感想を教えてくださり、ありがとうございました! KINTO テクノロジーズでは日々、新たなメンバーが増えています! 今後もいろんな部署のいろんな方々の入社エントリが増えていきますので、楽しみにしていただけましたら幸いです。 そして、KINTO テクノロジーズでは、まだまださまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは こちら からご確認ください!
アバター
This article is the entry for day 13 of the KINTO Technologies Advent Calendar 2024 🎅🎄 Hello! My name is fabt and I am developing an app called Unlimited (its Android version) at KINTO Technologies. I have recently implemented a screenshot test, which has been much talked about, so I will introduce the steps of it, the stumbling blocks I encountered and how to solve them. I hope this will be helpful for those who are considering implementing a screenshot test in the near future. What is Screenshot Testing? Screenshot testing is a method where the app takes screenshots based on the current source code in development and compares them to previous versions to confirm and verify changes. Roughly speaking, it can even detect a 1dp difference that is hard for the human eye to notice, making it easier to ensure no unintended UI modifications slip through. Many conference sessions, including one at DroidKaigi 2024, have addressed this topic, and I sense it's gaining significant attention lately. Screenshot Test Library Selection I decided to use Roborazzi , a tool featured in numerous conference presentations and already implemented in several Android apps within our company. Trying it out Following the official setup guide and information on the Internet, I'll try to run it in a local environment. Since I'm not doing anything special at this stage, I'll move forward quickly. First, install the necessary libraries for running the screenshot test. Version Catalog (libs.versions.toml) [versions] robolectric = "4.13" roborazzi = "1.29.0" [libraries] androidx-compose-ui-test-junit4 = { module = "androidx.compose.ui:ui-test-junit4" } androidx-compose-ui-test-manifest = { module = "androidx.compose.ui:ui-test-manifest" } robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } roborazzi = { module = "io.github.takahirom.roborazzi:roborazzi", version.ref = "roborazzi" } roborazzi-compose = { module = "io.github.takahirom.roborazzi:roborazzi-compose", version.ref = "roborazzi" } roborazzi-junit-rule = { module = "io.github.takahirom.roborazzi:roborazzi-junit-rule", version.ref = "roborazzi" } [plugins] roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } root build.gradle.kts file plugins { alias(libs.plugins.roborazzi) version libs.versions.roborazzi apply false } module build.gradle.kts file plugins { alias(libs.plugins.roborazzi) } android { testOptions { unitTests { isIncludeAndroidResources = true all { it.systemProperties["robolectric.pixelCopyRenderMode"] = "hardware" } } } } dependencies { // robolectric testImplementation(libs.androidx.compose.ui.test.junit4) debugImplementation(libs.androidx.compose.ui.test.manifest) testImplementation(libs.robolectric) // roborazzi testImplementation(libs.roborazzi) testImplementation(libs.roborazzi.compose) testImplementation(libs.roborazzi.junit.rule) } You should now have successfully added the necessary libraries. Now, let's create a test class like the one below and run it locally! import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import androidx.test.ext.junit.runners.AndroidJUnit4 import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH import com.github.takahirom.roborazzi.captureRoboImage import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.annotation.GraphicsMode @RunWith(AndroidJUnit4::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) class ScreenShotTestSample { @get:Rule val composeTestRule = createComposeRule() @Test fun sample() { composeTestRule.apply { setContent { MaterialTheme { Surface { Text(text = "screen shot test sample") } } } onRoot().captureRoboImage( filePath = "$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/sample.png" ) } } } By running the following screenshot capture command in the Android Studio terminal, the Text(text = XXXX) component's screenshot should be output as a PNG file. ./gradlew recordRoborazziDebug ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_empty_roborazzi.png =750x) Surprisingly, there was no output. This was an unexpected result. Test Gets Skipped Although the command execution was successful, for some reason the screenshot result was not output. While investigating, I ran the unit test locally and noticed that the following was displayed: ![](/assets/blog/authors/f.tsuji/2024-12-13/Test_events_were_not_received.png =750x) For some reason, the test events were not received, causing the test to be skipped. If that is the case, it certainly makes sense that the command was successful but there was no output. After further investigation, it seemed that the problem was a conflict in JUnit versions. Our app typically uses Kotest for unit testing, which runs on JUnit 5, while Roborazzi (or more specifically, Robolectric) uses JUnit 4 as its test runner. Surprisingly, there were similar issues in Roborazzi's github issues. The solution was to use the junit-vintage-engine , a library as mentioned in the above issue. Briefly speaking, this library allows JUnit 4 and JUnit 5 to coexist and is sometimes used for migration. Now, let's add junit-vintage-engine , run the command again to see if it works. First, add dependencies (the parts introduced in the previous section are omitted) Version Catalog (libs.versions.toml) [versions] junit-vintage-engine = "5.11.2" [libraries] junit-vintage-engine = { module = "org.junit.vintage:junit-vintage-engine", version.ref = "junit-vintage-engine" } module build.gradle.kts file dependencies { testImplementation(libs.junit.vintage.engine) } Run the screenshot save command again. This time, the Text(text = XXXX) screenshot should finally be output as a PNG file. ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure.png =750x) Execution result :::message alert After using junit-vintage-engine to allow JUnit versions to coexist, the test was able to run, but the result was a failure. ::: Initialization Failure It seems that the execution failed. Before adding the library, it couldn't even be executed, so let's take it as a positive step forward and aim to solve it honestly. It is a process of trial and error. As the first step in troubleshooting, I checked the logs and found the following output. ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure_log.png =750x) It seems like something failed during initialization. When I check the output logs, it seemed that it was trying to initialize our app's Application class. After a closer look at the Robolectric configuration , I found the following statement: Robolectric will attempt to create an instance of your Application class as specified in the AndroidManifest. Robolectric appears to create an instance of the Application class specified in AndroidManifest.xml . This matches what I saw in the logs! To prevent initialization failures, I'll follow the official documentation and use the @Config annotation to specify a simple Application class. import android.app.Application import org.robolectric.annotation.Config @RunWith(AndroidJUnit4::class) @Config(application = Application::class) // Add @GraphicsMode(GraphicsMode.Mode.NATIVE) class ScreenShotTestSample { With high expectations, run the screenshot save command (omitted) from the Android Studio terminal. Surely, this time the screenshot of Text(text = XXXX) should be output as a png file. ![](/assets/blog/authors/f.tsuji/2024-12-13/build_success.png =250x) ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_roborazzi.png =750x) ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample.png =250x) A screenshot successfully generated! :::message After investigating Robolectric Configuration and specifying the Application class using the @Config annotation, the screenshot test finally succeeded. ::: To try out the comparison (screenshot test), I made a slight change to the test class and ran the command. // Text(text = "screen shot test sample") Text(text = "compare sample") // Change the text ./gradlew compareRoborazziDebug ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_compare_roborazzi.png =750x) The comparison result has been successfully generated! ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample_actual.png =250x) Comparison image ![](/assets/blog/authors/f.tsuji/2024-12-13/result_sample_compare.png =750x) Comparison result Summary of Success So Far I created a simple test class and was able to run a screenshot test without any problems. While adjusting the details of unit tests and property specifications can be challenging depending on the project, the ability to visually confirm changes makes it clear and fast. Therefore, if possible, implementing screenshot testing can help maintain higher app quality by making unintended UI changes easier to detect. Side Note At this time, we also implemented CI for our app. Using the GitHub Actions Workflow, we achieved the point of saving, comparing, and commenting screenshots on a pull request. I'll omit the details since we didn't do anything particularly technical, but we have chosen a method that stores the results using the companion branch approach as described in official documentation . (We have integrated yml files, etc., but only at a general level of optimization for smoother implementation.) :::message When running ./gradlew recordRoborazziDebug , any regular unit tests within the same module are also executed. To address this in our app, we define our own properties and separate the Gradle tasks for running unit tests and screenshot testing tasks using Roborazzi. Reference issues https://github.com/android/nowinandroid/issues/911 https://github.com/takahirom/roborazzi/issues/36 ::: Let's get back to the topic. Including Preview Functions in Testing When implementing Composable functions, it's common to create corresponding Preview functions as well. If you implement tests manually as in the first half of this article, Composable Implementation Preview implementation Test Implementation It's quite a hassle... So, let's make the Preview function the target of screenshot testing! That's exactly what this section is about! This is another technique that has been a hot topic. Anyway, try it out The procedure is generally as follows: Collect Preview functions Run screenshot tests on the collected functions It's simple. There are several ways to collect Preview functions, but this time, I'll use ComposablePreviewScanner . The main reason I chose this is that it has well-documented setup steps, making investigation easier. Additionally, it seems likely to be officially integrated and supported in the future, though it's still experimental at the time of writing . Now, let's install the necessary libraries. Version Catalog (libs.versions.toml) [versions] composable-preview-scanner = "0.3.2" [libraries] composable-preview-scanner = { module = "io.github.sergio-sastre.ComposablePreviewScanner:android", version.ref = "composable-preview-scanner" } module build.gradle.kts file dependencies { // screenshot testing(Composable Preview) testImplementation(libs.composable.preview.scanner) } Next, let's create a test class by referring to the README of composable-preview-scanner and insights from those who have used it before. import android.app.Application import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onRoot import com.github.takahirom.roborazzi.DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH import com.github.takahirom.roborazzi.captureRoboImage import com.kinto.unlimited.ui.compose.preview.annotation.DialogPreview import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.robolectric.ParameterizedRobolectricTestRunner import org.robolectric.annotation.Config import org.robolectric.annotation.GraphicsMode import sergio.sastre.composable.preview.scanner.android.AndroidComposablePreviewScanner import sergio.sastre.composable.preview.scanner.android.AndroidPreviewInfo import sergio.sastre.composable.preview.scanner.android.screenshotid.AndroidPreviewScreenshotIdBuilder import sergio.sastre.composable.preview.scanner.core.preview.ComposablePreview @RunWith(ParameterizedRobolectricTestRunner::class) class ComposePreviewTest( private val preview: ComposablePreview<AndroidPreviewInfo> ) { @get:Rule val composeTestRule = createComposeRule() @Config(application = Application::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Test fun snapshot() { val fileName = AndroidPreviewScreenshotIdBuilder(preview).ignoreClassName().build() val filePath = "$DEFAULT_ROBORAZZI_OUTPUT_DIR_PATH/$fileName.png" // Preview function name.png composeTestRule.apply { setContent { preview() } onRoot().captureRoboImage(filePath = filePath) } } companion object { private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy { AndroidComposablePreviewScanner() .scanPackageTrees( include = listOf("XXXX"), // Set up a package to look for Preview functions exclude = listOf() ) .includePrivatePreviews() // Include private Preview functions .getPreviews() } @JvmStatic @ParameterizedRobolectricTestRunner.Parameters fun values(): List<ComposablePreview<AndroidPreviewInfo>> = cachedPreviews } } Now, execute the screenshot save command. ...However, it took a lot of time and even after an hour, it showed no signs of finishing. With fewer than 200 Preview functions, I didn't expect it to take this long. As expected, it wasn't going to be that easy. :::message alert When I included Preview functions in the screenshot test, the test execution time became significantly longer. ::: Test Execution Taking Too Long After doing some research, I found similar cases. https://github.com/takahirom/roborazzi/issues/388 A single test took about 5 minutes to complete, but after removing CircularProgressIndicator , the execution speed significantly improved. Digging deeper into the issue discussion, it seems that it takes a long time to test a Composable that includes infinite animation. As a solution, one suggested setting mainClock.autoAdvance = false to stop automatic synchronization with Compose UI and manually advancing time instead. By manually controlling time, we can capture screenshots at any time, avoiding the impact of infinite animations. Since our app also uses CircularProgressIndicator , this is definitely worth trying! I'll implement it immediately. I added mainClock. XXXX to the test class. Pause time. Advance by 1,000 milliseconds (1 second). Capture the screenshot. Resume time. That's the flow. composeTestRule.apply { mainClock.autoAdvance = false // 1 setContent { preview() } mainClock.advanceTimeBy(1_000) // 2 onRoot().captureRoboImage(filePath = filePath) // 3 mainClock.autoAdvance = true // 4 } (On a separate note, there's a library called coil for loading images asynchronously, which may not be tested correctly either. ]( https://github.com/takahirom/roborazzi/issues/274)But since the solution seems to be the same, we should be able to handle these together.) Alright, execution time! ![](/assets/blog/authors/f.tsuji/2024-12-13/build_failure_time.png =500x) The test execution became much faster, but it failed. :::message alert While infinite animations were a bottleneck, manually controlling time during testing resolved the issue. Now the test runs quickly, but the result was still a failure. ::: Failure When Dialogs Are Included Although the execution speed has increased, there is no point if it fails. In situations like this, we can always rely on logs. ComposePreviewTest > [17] > snapshot[17] FAILED java.lang.AssertionError: fail to captureRoboImage Reason: Expected exactly '1' node but found '2' nodes that satisfy: (isRoot) Nodes found: 1) Node #534 at (l=0.0, t=0.0, r=0.0, b=0.0)px 2) Node #535 at (l=0.0, t=0.0, r=320.0, b=253.0)px Has 1 child at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrThrow(SemanticsNodeInteraction.kt:178) at androidx.compose.ui.test.SemanticsNodeInteraction.fetchOneOrThrow$default(SemanticsNodeInteraction.kt:150) at androidx.compose.ui.test.SemanticsNodeInteraction.fetchSemanticsNode(SemanticsNodeInteraction.kt:84) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage(Roborazzi.kt:278) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage(Roborazzi.kt:268) at com.github.takahirom.roborazzi.RoborazziKt.captureRoboImage$default(Roborazzi.kt:263) at com.kinto.unlimited.ui.compose.ComposePreviewTest.snapshot(ComposePreviewTest.kt:49) There were multiple logs like the one above. It said there were two nodes. Searching based on the log information, I found an interesting issue . As it's related to compose-multiplatform , the cause may be different, but it seems that the failure is occurring in sections where Dialog()Composable is being used. When I was wondering if I had to give up, I discovered that an experimental function has been added to capture images that include dialogs! Since it's experimental, using it for all tests might not be ideal, but if I can determine whether the test target is dialogs, it may succeed. So, I decided to create a custom annotation called DialogPreview() , applied it to the Previews that include dialogs, and modified the test class to retrieve and determine the information. annotation class DialogPreview() @OptIn(ExperimentalRoborazziApi::class) // Add @Config(application = Application::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) @Test fun snapshot() { val isDialog = preview.getAnnotation<DialogPreview>() != null // Add composeTestRule.apply { mainClock.autoAdvance = false setContent { preview() } mainClock.advanceTimeBy(1_000) if (isDialog) { // Add captureScreenRoboImage(filePath = filePath) } else { onRoot().captureRoboImage(filePath = filePath) } mainClock.autoAdvance = true } } companion object { private val cachedPreviews: List<ComposablePreview<AndroidPreviewInfo>> by lazy { AndroidComposablePreviewScanner() .scanPackageTrees( include = listOf("・・・"), exclude = listOf() ) .includePrivatePreviews() .includeAnnotationInfoForAllOf(DialogPreview::class.java) // Add .getPreviews() } } Determine if the Preview has a DialogPreview annotation (not null) and use captureScreenRoboImage() for dialogs. Let's run it. ![](/assets/blog/authors/f.tsuji/2024-12-13/build_success.png =250x) ![](/assets/blog/authors/f.tsuji/2024-12-13/folder_roborazzi_preview.png =750x) Due to the nature of the Preview function, images and file names are masked. After loading the Preview functions, I successfully saved their screenshots! :::message Composable functions that include dialogs contain multiple nodes, making it difficult to correctly determine the root, which led to test failures. Although it's experimental, using a function that can capture dialogs allowed the tests to run successfully. ::: The screenshot comparison was already confirmed earlier, so everything seems to be working fine as it is. Thank you for sticking with me through this long process! Conclusion In this article, I walked through the process of implementing screenshot testing, highlighting the challenges I encountered, and how I solved them. At first, I thought it would be as simple as adding a library and running the tests, but things didn't work out. After a lot of trial and error, I finally got screenshot testing to work. The solutions, such as adding libraries or applying annotations, were relatively simple. However, finding the right information to reach those solutions turned out to be more challenging than I expected. I hope this article will be of some help to you.
アバター
Introduction Hello! I'm Nakatani ( @tksnakatani ) from the Common Service Development Group, where we plan and develop essential functions across web and mobile app services, including membership and payment platforms. In this article, I'll share a case of an Aurora MySQL deadlock that occurred in a production environment at a payment platform—one of those "close-call incidents" that many of us have likely encountered at least once. The Incident Leading to the Deadlock One day in 2024, we received an incident notification from our log monitoring system. When I reviewed the notification, I found that the following error log recorded during a credit card payment process. Deadlock found when trying to get lock; try restarting transaction Additionally, Slack alerted us to an inquiry from a product manager reporting that their credit card payment had failed. At that moment, I immediately sensed the gravity of the situation, and I still vividly remember breaking out into a cold sweat. Cause investigation Logic check The deadlock itself was resolved naturally after about 30 minutes. Upon further investigation, we discovered that a popular product had been released around the time the error occurred, leading to a high volume of purchase requests. A deadlock generally occurs when multiple transactions hold resources required by each other, causing a standstill. Given that load testing had been conducted to simulate such scenarios, it was puzzling what the deadlock still occurred. Initially, we couldn't pinpoint any specific processes that would cause resource contention, so it was theoretically difficult to identify the rootcause of the deadlock. Reproduction check Next, we tried to reproduce the requests to the API, both before and after the deadlock occurred, in a local environment. Using the same request parameters that caused the problem in the production environment, we sent the following two requests almost simultaneously using the curl command. As in the production environment, one request was successful, but the other resulted in a system error. Here's an example of the curl command we used: curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000001" } curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000002" } ::: The error log contained the following message: Deadlock found when trying to get lock; try restarting transaction was output. Being able to reproduce the problem provided a crucial clue for our investigation, and for the moment, we felt a sense of relief. SHOW ENGINE INNODB STATUS Additionally, we used the MySQL SHOW ENGINE INNODB STATUS command to check the status of the InnoDB storage engine. The SHOW ENGINE INNODB STATUS command provides comprehensive information about the state of the InnoDB storage engine. This information can provide clues to examine the lock and transaction details and identify the specific cause of the deadlock. mysql> set GLOBAL innodb_status_output=ON; mysql> set GLOBAL innodb_status_output_locks=ON; ・・・Again, sent two requests, using the curl command. mysql> SHOW ENGINE INNODB STATUS; The results back then are as follows: *Some portions have been excerpted and masked. ===================================== 2024-xx-xx 10:05:27 0x7fe300290700 INNODB MONITOR OUTPUT ===================================== Per second averages calculated from the last 2 seconds ----------------- BACKGROUND THREAD ----------------- srv_master_thread loops: 463 srv_active, 0 srv_shutdown, 7176 srv_idle srv_master_thread log flush and writes: 0 ---------- SEMAPHORES ---------- OS WAIT ARRAY INFO: reservation count 318 OS WAIT ARRAY INFO: signal count 440 RW-shared spins 290, rounds 306, OS waits 16 RW-excl spins 1768, rounds 5746, OS waits 48 RW-sx spins 0, rounds 0, OS waits 0 Spin rounds per wait: 1.06 RW-shared, 3.25 RW-excl, 0.00 RW-sx ------------------------ LATEST DETECTED DEADLOCK ------------------------ 2024-04-18 10:04:02 0x7fe3059a4700 *** (1) TRANSACTION: TRANSACTION 12085, ACTIVE 6 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 7 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3 MySQL thread id 70, OS thread handle 140612935517952, query id 28138 192.168.65.1 user update insert into payments (.... *** (1) HOLDS THE LOCK(S): RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12085 lock_mode X locks gap before rec Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12085 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (2) TRANSACTION: TRANSACTION 12084, ACTIVE 7 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 7 lock struct(s), heap size 1136, 3 row lock(s), undo log entries 3 MySQL thread id 69, OS thread handle 140612935812864, query id 28139 192.168.65.1 user update insert into payments (.... *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12084 lock_mode X locks gap before rec Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 297 page no 5 n bits 248 index uq_payments_01 of table `payment`.`payments` trx id 12084 lock_mode X locks gap before rec insert intention waiting Record lock, heap no 56 PHYSICAL RECORD: n_fields 4; compact format; info bits 0 0: len 11; hex 6d65726368616e745f3031; asc merchant_01;; 1: len 7; hex 5041594d454e54; asc PAYMENT;; 2: len 30; hex 6276346c6178316736367175737868757676647963356737656c616a6466; asc bv4lax1g66qusxhuvvdyc5g7elajdf; (total 32 bytes); 3: len 30; hex 70615f666f79706161656c6a71666f663378746332366b6d61756c38676e; asc pa_foypaaeljqfof3xtc26kmaul8gn; (total 35 bytes); *** WE ROLL BACK TRANSACTION (2) ---------------------------- END OF INNODB MONITOR OUTPUT ============================ The above indicates the following: TRANSACTION 12085 and TRANSACTION 12084 exist. Both TRANSACTION 12085 and TRANSACTION 12084 acquired the same "gap lock.” TRANSACTION 12085 attempted to acquire an "insert intention gap lock" before insertion, but this conflicted with the gap lock of TRANSACTION 12084, causing it to wait. TRANSACTION 12084 attempted to acquire an "insert intention gap lock" before insertion, but this conflicted with the gap lock of TRANSACTION 12085, causing it to wait. MySQL detected a deadlock and rolled back TRANSACTION 12084. What are gap lock and insertion intention gap lock? Gap lock A gap lock is a lock on the gap between index records, or a lock on the gap before the first index record or after the last index record. For example, SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; prevents other transactions from inserting a value of 15 into column t.c1, because the gaps between all existing values in the range are locked, regardless of whether such a value already exists in the column. https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-gap-locks Insertion intention gap lock An insertion intention lock is a type of gap lock set by an INSERT operation before a row is inserted. This lock indicates that the insertion is done intentionally so that multiple transactions inserted into the same index gap do not have to wait for each other unless they are inserted into the same place within that gap. Let's assume that there are index records whose values are 4 and 7. Separate transactions attempting to insert the values 5 and 6, respectively, use an insertion intention lock to lock the gap between 4 and 7 before acquiring an exclusive lock on the inserted rows, but do not block each other because the rows do not conflict. https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html#innodb-insert-intention-locks Since it was discovered that the gap lock was the cause of the problem in question, we proceeded to identify the portion of the credit-card payment processing where the gap lock was acquired. The overall flow of the payment processing is divided into the following three steps: Checking whether a payment has been made with the same request ID Requesting payment from the payment service provider Writing the results from the payment service provider into the database and returning a response. We set breakpoints around the areas where SQL is issued and performed debugging in a local environment, confirming that a gap lock was acquired immediately after executing the following query: SELECT * FROM PAYMENTS where request_id = '' FOR UPDATE; The data from performance_schema.data_locks at that time is as follows: Cause All the information has been gathered, and the cause has been identified. In the payment platform, the request_id was received from the request source to check for duplicate requests, and this value was assigned a unique index for use in subsequent references. Meanwhile, on the product side, the request_id was generated according to the following rule: Product ID-YYYYMMDD-Sequential Number When the deadlock occurred, a popular product had been released, and purchase requests for the same product were concentrated and sent in large numbers over a short period of time. As a result, a large number of requests were sent, in which the sequential number portion of request_id was counted up rapidly. An example of a curl command curl --location 'http://localhost:8080/payments/cards' \ --header 'Content-Type: application/json' \ --data '{ "amount": 10, "capture": false, "request_id": "ITEM-20240101-0000001" } ::: As mentioned above, the main flow of credit card payment processing is as follows. Checking whether a payment has been made with the same request ID Requesting payment from the payment service provider Writing the results from the payment service provider into the database and returning a response. The problem occurred in the first step: SELECT * FROM PAYMENTS where request_id = '' FOR UPDATE; This query is typically executed under the assumption that requests with the same request_id will not be received. However, since the data for the request_id had not been inserted yet, the query resulted in no action, and a gap lock was acquired. Subsequently, in the third step, INSERT processing to write the results occurred, and it attempted to acquire an insertion intention gap lock. However, it conflicted with the gap lock that had already been acquired, resulting in a wait. As a result, MySQL detected a deadlock, and one transaction was rolled back. Resolution Through our investigation, we identified that the SELECT FROM ... FOR UPDATE query, which was used to check for duplicate payments, was causing the deadlock. To resolve this, we decided to discontinue this query and revised the design. Now, the data is temporarily registered when a request is received, and the transaction is committed immediately. Although there were concerns that increasing the frequency of commits might lead to performance degradation, load testing confirmed that the necessary performance could be maintained, so we released it with this specification. Reflection This incident highlighted our insufficient understanding of gap locks. We didn't fully understand that a gap lock is acquired when there are no results for a SELECT FROM ... FOR UPDATE query. While I believed I carefully reviewed manuals and incorporated the ideas into our designs, I now realize that I had assumed I knew everything. https://dev.mysql.com/doc/refman/8.0/ja/innodb-locking.html I also regret that there were aspects we could have noticed during testing. In our load testing, we used random values (UUIDs) for the request_id , primarily to avoid performance degradation from index fragmentation and rebuilding. As a result, no deadlock occurred during the tests, and they completed successfully. Conclusion A deep understanding of transaction behavior and lock mechanisms is essential when working with MySQL and the InnoDB storage engine. This incident reinforced the importance of regularly reviewing documentation and specifications, and seeking expert advice when necessary. Additionally, I learned that investigating the actual parameters used in the production environment, and conducting tests with request values based on those parameters, can help identify issues early and improve overall quality.
アバター
Introduction Hello! I am Akeda, a member of both the Corporate IT Group and the Technology Public Relations Group, and of the Manabi-no-Michi-no-Eki Team as well. (“Manabi-no-Michi-no-Eki” literally means “roadside station of learning.”) As a corporate engineer, I am usually doing things like on/offboarding related for IT equipment, and improving the processes and work within the groups. In this article, I would like to share the story of how I, a corporate engineer, happened to get involved in the Manabi-no-Michi-no-Eki project, and creating a portal that spontaneously collects videos from actively held in-house study sessions. The Trigger: Is There No Way to Watch Internal Study Sessions Later? As we have mentioned in several articles below, our company frequently holds in-house study sessions. https://blog.kinto-technologies.com/posts/2024-04-23_%E5%AD%A6%E3%81%B3%E3%81%AE%E9%81%93%E3%81%AE%E9%A7%85%E3%81%AF%E3%81%98%E3%82%81%E3%81%BE%E3%81%97%E3%81%9F/ https://blog.kinto-technologies.com/posts/2024-05-21-%E5%AD%A6%E3%81%B3%E3%81%AE%E9%81%93%E3%81%AE%E9%A7%85-iOS%E3%83%81%E3%83%BC%E3%83%A0%E3%81%AE%E5%8B%89%E5%BC%B7%E4%BC%9A%E3%81%AB%E7%AA%81%E6%92%83/ After the sessions, the video recordings of them get posted on the in-house Slack channel. However, as a corporate engineer, I felt that the following points were internal issues. There are places in the company like Confluence, SharePoint, and Box for sharing document files, but no fixed place to store video content. In particular, although the study sessions could be beneficial to everyone in company, the fact that the video recordings of them only got posted on the in-house Slack channel made them difficult to find later. The company-wide information-sharing channel is the main place that things get posted to and other work-related information and so on flows through it as well, so they were impossible to find through simple scrolling alone. Members who joined the company after study sessions had been held had no way of knowing that they had been held in the first place, let alone that there were videos and files from them. Wondering what could be done to solve these problems, I came up with the idea of building a video platform and creating a place where the videos of the study sessions could all be gathered together. The Beginning: I Tried Laying My Idea on the Manabi-no-Michi-no-Eki Project Members When I had the idea of creating a place for gathering together videos of the study sessions, I immediately thought of the Manabi-no-Michi-no-Eki project. Manabi-no-Michi-no-Eki is a “michi-no-eki” (roadside station) is an activity that "supports in-house activation centered on study groups where internal 'study groups' intersect." I selfishly thought that it would be nice if we could work together if what I want to do and their activities clashed, or if they were considering it. As the saying goes, "strike while the iron is hot," I started by approaching Kin-chan , who is in the same groups as me, and is also involved in Manabi-no-Michi-no-Eki. The response I got was an enthusiastic thumbs-up! (I remember being overjoyed about that.) After that, once the Corporate IT Group had gotten the green light to go ahead with the idea, I immediately went to discuss it with the other project members as well. I think the back-and-forth on Slack at the time conveys how enthusiastic the Manabi-no-Michi-no-Eki members were about it, so I will share it some of it with you here. First, here is the discussion message from me. Did you notice the amazing emoji reaction? Here is an answer I got with the same momentum. Things moved quickly! After that, on the same day that we all got together, I was warmly invited to join Manabi-Michi-no-Eki. And so, we ended up all proceeding with the video platform together as members of the Manabi-Michi-no-Eki team. Main Story: We Built a Video Platform and Rolled It Out Internally I imagine the internal rolling out of a platform containing collected videos of company-wide meetings and in-group study sessions is something some of this blog’s readers would like to do themselves, so now, I will talk about what we did to achieve it, and pick out a few things to talk about why we did them. What We Did Building a video platform Utilizing the SharePoint site that was being used in the company, we gave (among other things) the video posting method a makeover. Deciding the video collection method Collect videos of meetings and internal study sessions that are OK to publish within the company Get the meeting/session organizers to upload video files and publishable files to the documents section of the SharePoint site On the top page of the SharePoint site, use the “highlighted content” feature, and set a filter using the study session name. Once the setup has been done, if a video with the same study session name is uploaded, it will get posted on the top page automatically. Spreading the word, and running the system We announced the video platform and asked people to collect videos for it in the all-company meeting at the end of August Q. Why Did You Choose SharePoint? A. Because our company uses Microsoft365 for groupware. There are various video platforms to choose from, YouTube, Vimeo, and Brightcove being prominent examples. However, using one of those would entail signing a new contract, and in the first place, one of the requirements was that we wanted collecting the videos of the sessions to be done by their organizers. Consequently, we opted for SharePoint, which everyone in the company is already familiar with. Q. Why Did You Decide to Get the Organizers Themselves to Upload the Videos? A. We wanted the organizers themselves to handle things like editing out the unnecessary parts. We thought it would be best if they decided for themselves whether to upload things to the video platform. As third parties, we Manabi-no-Michi-no-Eki members could have done the work for them, but we thought the organizers themselves were the most suitable people to decide whether the videos should also include things like the casual chit-chat before the study sessions started and things that would only make sense if you had actually seen them at the time, and to decide whether the videos should be uploaded to the platform in the first place. Q. Why Did You Decide to Use the “Highlighted Content” Feature A. Because it was simple and easy. When we thought about how to get people to watch the uploaded videos, we presumed that the reach would be higher if people saw ones they were interested in via images rather than text, and could watch them on the top page rather than somewhere a few clicks away. After that, we created several patterns for how to access the videos, and got Manabi-no-Michi-no-Eki members to watch them. When we did this, we found that the format that would attract viewers’ interest the most was one where we could display thumbnails, and the videos would be grouped by study session and listed on the SharePoint site’s top page. The “highlighted content” feature lets you do all that, so that is what we decided to use. Example of using it Reference: https://support.microsoft.com/ja-jp/office/強調表示されたコンテンツの-web-パーツを使用する-e34199b0-ff1a-47fb-8f4d-dbcaed329efd A cautionary note about using this is that filtering by study session name (= video file name) might not catch videos that have only just been uploaded. When that happens, please wait a little while, then try again. Q. Did You Only Spread the Word about It That One Time at the All-Company Meeting? A. We tell people about it every month in the orientation for new employees. We tell people about it in the monthly Manabi-no-Michi-no-Eki and Technology Public Relations Group sections of the monthly all-company meetings. As I said at the beginning of the article, there was an issue in that members who joined the company after study sessions had been held had no way of knowing that they had been held in the first place, let alone that there were videos and files from them. Of course, we addressed that here. By having these two sections, we are successfully making its presence felt! In Conclusion: We Will Continue to Support Internal Learning in a Variety of Ways! Since the all-company meeting, the number of accesses to the platform has not decreased significantly and study session videos are being collected. This reinforces my belief that the platform is addressing the issues I initially identified. So far, I have talked about the following three themes: the internal issues I perceived; how I found a strong supporter within the company in the form of Manabi-no-Michi-no-Eki; and the main theme, how we actually built the platform. One of our company’s good points is that it is tolerant about people taking on new challenges. I feel that this initiative really embodies that, and although I talked at length before getting to the main theme, I do believe (perhaps presumptuously) that I have gotten that message across to everyone who has stuck with this article up to here. Currently, Manabi-no-Michi-no-Eki is working as a single team in the Technology Public Relations Group based on a single project. We will continue to serve as a “roadside station” where internal study sessions come together and activities are conducted to support internal energization centered on them, and to tell you about those activities through things like the Tech Blog. So, please do stay tuned!
アバター
技術広報Gの中西です。この度新たなコミュニティを立ち上げることになったのでこちらで告知致します。 モバイルアプリの市場は年々拡大し、サービスの差別化には迅速かつ高品質なリリースが求められるようになりました。そんな状況下で注目を集めるのが、Seleniumに近い操作感でiOS/Androidアプリをテストできる Appium です。ただし、OSのバージョン差や環境構築の難しさなど、実際に導入・運用するうえで乗り越えるべきハードルも存在します。 こうした課題を共有し、解決策を学び合う場として、新たなコミュニティ「 Appium Meetup Tokyo 」が発足しました。 「Appium Meetup Tokyo」を立ち上げ理由 ニーズの高まり モバイルアプリのリリースサイクルは短期化が進み、手作業だけで行うテストには限界があります。Appiumはテスト自動化の効率化を後押しする重要な選択肢ですが、設定や実装の知見が十分に共有されていない状況です。 また弊社KINTOテクノロジーズにおいても 2025年の重要施策 として「技術力」「開発生産性」「リリーススピード」を掲げております。これらを実現するうえでもAppiumによるテスト自動化はとても重要な取り組みとなっています。 近年「開発生産性」という言葉を聞かない日は無いくらい様々な企業で語られていますが、今後ますます開発速度が高まる中必然的に自動テストの重要度が増してきています。 国内向けの情報不足 英語ドキュメントや海外事例は増えてきていますが、日本語でまとまったAppium情報はまだ限られています。実務で運用するにあたっては、身近な事例や成功・失敗談が大きな助けになります。 おそらく、他社様も同様にお困りのQAエンジニアの皆さんやモバイルアプリ開発者の方がいらっしゃると思います。日本語での情報共有やコミュニティを活発化させ、よりより自動テスト環境を構築していきたいと考えております。 Autify社との協力体制 モバイルアプリ自動テストプラットフォームを提供するAutify社とお話している中で、Appiumを学ぶ機会があると嬉しい。という話をしていく中で、この度、協力しこのコミュニティをまずは小さな勉強会から始めていくこととなりました。 Autify社では、サービスの裏側でAppiumを利用しており、様々な知見をお持ちのため、今まで外に出てきていないような様々な知見、自社のアプリ開発や自動テスト化だけでは得られないような知見やノウハウも多くお持ちなので、私個人としてはとても楽しみにしております。 また、Appiumだけでなく周辺ツールも含めた総合的なノウハウを共有することで、より幅広い課題解決に対応できるコミュニティを目指します。 今後この活動を広めていくための運営メンバーも絶賛大募集中です。 コミュニティの活動内容 定期勉強会 Appium基礎講座 実機・シミュレータを使ったハンズオン CI/CD連携方法の解説 ライトニングトーク(LT)やディスカッション 導入企業の事例紹介 現場ならではの課題やノウハウの共有 日々のアップデート情報やTips交換 オンライン情報発信 スライドや資料のアーカイブ コミュニティSlackやSNSでのQA ベストプラクティス集の作成 などなど、皆様とご一緒に様々な取り組みを出来ればと考えております。 初回イベントについて 開催日程 : 2025年2月20日(木) 19:00-21:30 開催場所 : Autify社 東京オフィス(ハイブリット開催) 主なテーマ : AutifyにおけるAppiumプラグインの活用事例(15分+質疑応答5分) 効率的なアプリ自動化のためのガイドラインと実践方法(15分+質疑応答5分) 【公募LT】発表3(15分+質疑応答5分) 【公募LT】発表4(15分+質疑応答5分) 詳細はconnpassをご覧ください https://autifyjapan.connpass.com/event/342867/ 登壇ご希望の方、参加ご希望の方は是非↑からお申し込みください。 今後の展開 継続的なイベント開催 勉強会やワークショップを通じて、初心者から上級者まで多様なニーズに応えていきます。 他ツールとの連携情報 SeleniumやCypressなどの他ツールとの比較や連携事例も積極的に取り上げ、実務の選択肢を広げます。 コミュニティドリブンな情報発信・共有 イベントを重ねる中で、Appiumに関する成功談・失敗談を含めた豊富な知見をコミュニティ全体で知見を共有し、参加者のスキル向上に貢献します。 参加を検討している方へ モバイルアプリの自動テストをこれから本格的に導入したい方 Appiumに興味があるが具体的な事例やノウハウが欲しい方 CI/CDと組み合わせた運用に関心があるエンジニアやQA担当の方 他社事例を参考に自社のテスト文化を改善したい方 上記に当てはまる方は、ぜひAppium Meetup Tokyoで最新の知見を共有し合いましょう。今後の告知や詳細情報は @AutifyJapan や @KintoTech_Dev でさせて頂きます。ご質問やご要望などがございましたら、お気軽にお寄せください。 Appium Meetup Tokyoでお会いできることを心より楽しみにしています。
アバター
This article is the entry for day 9 in the KINTO Technologies Advent Calendar 2024 . Introduction Hello, I'm Kameyama, an application engineer in the Digital Transformation Development Group. In recent years, generative AI has been applied across different fields, and our development team is also working on building systems that makes the most of its capabilities. Furthermore, since Java is widely used within our development team, we believed it would allow us to efficiently build an interface for generative AI while taking advantage of our existing knowledge and tools. With this in mind, this article will explain how to call generative AI using Java and process its results. This article will cover the basic implementation of calling Azure OpenAI using Java, with simple code examples. Compared to OpenAI, Azure OpenAI is considered a platform with greater scalability and reliability, making it better suited for large-scale business systems. Setting Up Azure Open AI First, sign up for an Azure subscription: https://azure.microsoft.com/en-us/pricing/purchase-options/azure-account?icid=ai-services&azure-portal=true Next, follow the instructions on the page below to obtain the endpoint and API key. https://learn.microsoft.com/ja-jp/azure/ai-services/openai/chatgpt-quickstart?tabs=command-line%2Cjavascript-keyless%2Ctypescript-keyless%2Cpython-new&pivots=programming-language-java Access the Azure console here . * This page requires logging in with the account you registered earlier. Setting Up the OpenAI Library To call Azure OpenAI, the Azure SDK library will be used. This SDK library allows for simple and efficient coding to call generative AI in Azure OpenAI. For Gradle: dependencies { implementation 'com.azure:azure-ai-openai:1.0.0-beta.12' implementation 'com.azure:azure-core-http-okhttp:1.7.8' } For Maven: <dependencies> <dependency> <groupId>com.azure</groupId> <artifactId>azure-ai-openai</artifactId> <version>1.0.0-beta.12</version> </dependency> <dependency> <groupId>com.azure</groupId> <artifactId>azure-core-http-okhttp</artifactId> <version>1.7.8</version> </dependency> </dependencies> The version used here is the latest available at the time of writing. Refer to the Official Documentation for the most up-to-date version. In particular, the Azure OpenAI client library for Java that we will use this time is currently in beta, so we recommend that you use the stable version when it is released in the future. Calling the Azure OpenAI Chat Model in Practice Reference: https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/openai/azure-ai-openai /src/main/resource/config.properties endpoint=https://{Resource Name}.openai.azure.com/ apiKey={API Key} Enter the obtained Azure OpenAI endpoint and API key here to manage them in a separate file. You can manage sensitive information according to your own or your team's policies. /src/main/java/com/sample/app/Main.java Package com.sample.app; // match your package name import com.azure.ai.openai.OpenAIClient; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.models.*; import com.azure.core.credential.AzureKeyCredential; import java.util.ArrayList; import java.util.List; public class Main { public static void main(String[] args) { // Load the properties file *Modify as needed if using a different method to manage key information. Properties = new Properties(); try (InputStream input = Main.class.getClassLoader().getResourceAsStream("config.properties")) { if (input == null) { System.out.println("config.properties File not found.") ; return; } properties.load(input); } catch (IOException ex) { System.out.println(ex.getMessage()); return; } // Retrieve configuration values from properties String endpoint = properties.getProperty("endpoint"); String apiKey = properties.getProperty("apiKey"); // Create OpenAI client var client = new OpenAIClientBuilder() .endpoint(endpoint) .credential(new AzureKeyCredential(apiKey)) .buildClient(); // Prepare the prompt List<ChatRequestMessage> messages = new ArrayList<>() .setTemperature(0.7) // Response randomness, the higher more diverse (0.0~2.0) .setMaxTokens(100) // Maximum number of response tokens .setFrequencyPenalty(0.0) // Penalty for frequently occurring words (-2.0~2.0) .setPresencePenalty(0.6); // Penalty for things relates to existing topics (-2.0~2.0) messages.add(new ChatRequestSystemMessage("You are an excellent AI assistant.")); messages.add(new ChatRequestUserMessage("For beginners, please explain the difference between classes and objects in Java.")); // Set the request option var options = new ChatCompletionsOptions(messages); var chatCompletions = client.getChatCompletions("gpt-4o", options); //Specify the deployment name or generative AI model name to use // Send the request and retrieve the result for (ChatChoice choice : chatCompletions.getChoices()) { ChatResponseMessage message = choice.getMessage(); System.out.printf("Index: %d, Chat Role: %s.%n", choice.getIndex(), message.getRole()); System.out.println("Message:"); System.out.println(message.getContent()); } } } When calling the generation AI, you can set various parameters. These are parameters that cannot currently be set in the ChatGPT application you normally use, and being able to adjust these parameters is one of the benefits of calling the generation AI from a program. In this case, we set four parameters (temperature, maxTokens, frequencyPenalty, and presencePenalty), but there are many other parameters. See here for more details. In addition, the following two types of messages were set in the messages section. The former ChatRequestSystemMessage can be executed without setting. ChatRequestSystemMessage This message is used to set the behavior and roles of the generative AI model, defining the tone of the conversation and response style. ChatRequestUserMessage This message is used to convey specific questions or instructions from the user to the AI, and the response is returned from OpenAI as the return value. For the first argument of getChatCompletions, enter a deployment name or a model name. The deployment name can be obtained from the Azure portal. If using OpenAI outside of Azure, enter a model name "gpt-4o" or "gpt-3.5-turbo," etc. (In the example above, the model name is entered.) .gitignore /src/main/resources/config.properties If managing it as explained in this article with /src/main/resource/config.properties, add the above line to .gitignore. Especially when managing it in a repository, be sure to handle sensitive information such as API keys with utmost care. Execution Result We were able to get the following response from OpenAI. (The actual response is in Markdown format.) Index: 0, Chat Role: assistant. Message: The difference between classes and objects in Java is an important concept for beginners learning programming. Here's an easy-to-understand explanation: Class Blueprint : You can think of a class as a blueprint or template for creating an object. It defines the attributes (fields) and behaviors (methods) of an object. Declaration : In Java, the class keyword is used to define a class. For example, a class representing a car can be defined as follows: public class Car { // Field (attribute) String color; int year; // Method (behavior) void drive() { System.out.println("The car is driving"); } } Object Instance : An object is an entity (instance) generated from a class. It holds specific data and can perform operations on that data. Creation : In Java, the new keyword is used to create an object from a class. For example, to create an object from the Car class: public class Main { public static void main(String[] args) { // Create an instance of the Car class Car myCar = new Car(); myCar.color = "Red"; myCar.year = 2020; // Call the object's method myCar.drive(); } } Summary A class is a template for creating objects, defining their attributes and behaviors. An object is an actual instance of the class, holding specific data and executing the defined behaviors. By understanding this basic relationship, you can begin to build more complex programs. Conclusion This article provided a basic introduction to using Azure OpenAI with Java. Since there is still limited information on integrating OpenAI with Java, I hope this guide will be useful to you. Next time, I'd like to explore more advanced methods, so stay tuned!
アバター
KTC の Engineering Office について 2025年1月、KINTOテクノロジーズ(以下 KTC )では新たに「Engineering Office」を設立しました。 この記事では社内周知も兼ねて Engineering Office の取り組みについて紹介します。 やりたい、やっていること 我々のシンプルな目的は「KINTO テクノロジーズの開発組織としての力を高めること」です。これを中長期で実現していけるよう、新しいケイパビリティやカルチャーを獲得するためのさまざまな働きかけをしています。 副社長 直轄グループとして経営課題と常に同期し、KTC に必要な取り組みを仮説立てながら、私を含む所属メンバーが それぞれの経験や専門性を活かして各種の横軸活動に取り組んでいる のが現状です。 スタンス、こころがけ どこの組織でも隣の青い芝を見て、無いものばかりに目がついてしまいます。しかし新たな可能性を開くためには、これまで培ってきた強みにもしっかりと目を向けて活かすことが必要です。 「これまでの強みにこれを足したり、引いたり、変えたりすれば、もっと強くなれる」、そういう発見をしないとなかなか前には進めません。現状を正しく理解し、コンテキストを汲んだ活動をすることが重要です。 KTC は既に350名を超える規模であり、既存の業務や体制が確立されているからこそ、変化を生み出すことも容易ではありません。誰も自分たちにとっての正解なんて分からない不確実で曖昧な状況だからこそ、安易に結論を急がず本当に必要なものを見極める粘り強さも心がけたいところです。 たぶん、やらないこと 事業施策、開発案件の企画立案、推進・実行 開発案件のプロジェクトマネジメントおよびプログラムマネジメント 採用や広報、社内活性化など Engineering Office は開発組織としての力を高めるための企画立案、推進を行いますが、他社の同名組織の事例によくある採用、育成、評価、広報などを直接担うことはありません。今は企画を進めるなかで、要所でそれらの専任グループと協力しあっています。 活動例① 注力テーマの取り組みを加速させる企画 現在の KTC は優れた能力をもつメンバーが集まりつつある一方で、内製開発部隊としてより高い成果を実現するためにはチームや組織としての成熟を進める必要があります。 副社長と議論を重ねる中で 2025年に我々が注力して取り組むべきテーマ の言語化に関わらせてもらいました。AIファーストは 専任プロジェクト があるので、Engineering Office ではそれ以外の3テーマ... リリースファースト ユーザーファースト 組織インテンシティ これらについて ①組織全体におけるリテラシー向上 と ②具体事例の創出と行動変容 の2軸を念頭に、社内の有志や各グループの取り組みに対する支援を企画しています。 活動例② 開発チームのプロセス/コミュニケーション支援 メンバーの Y.Naito さんの SPI ( Software Process Improvement ) に関する専門知識や経験を活かしながら、社内の開発チームに対して支援活動をしています。 具体的には、チームふりかえりのファシリテーションや開発インフラ (JIRAやFindy Team+など) の運用アシスト、プロセス改善に必要なナレッジの勉強会、メンタリングなどを展開しています。 チーム一丸となって協力し合うことで個々のメンバーが単独では成し得ない成果を創出するために、コミュニケーション、ツール、プロセス、そしてエンパワーメントの向上・改善をリードしていく取り組みです。 また、個人や特定のチームに蓄積されていたノウハウや優れたプラクティスを発掘し、他のチームへ横展開することで組織内の知識を拡大・深化させ、最終的には組織全体の技術力を高める活動を推進します。 活動例③ 個人の成長と貢献が両輪で回る環境づくり 副社長から技術の会社として 「エンジニアやデザイナー等の専門職がスペシャリストとして活躍することが、ちゃんと会社への貢献につながって、それが評価されるようにしたい」 とリクエストをもらっています。議論を始めたばかりで確からしいことはまだ何も書けないのですが、このリクエストを実現していくため人事と協力して各部長らと相談を始めています。 ソフトウェア領域の開発力・技術力でグループに貢献する会社で在り続けるためには、キャリアパスの整備やチャレンジ機会を増やす等の取り組みを通して、メンバーが成長を続けられる環境を整えることが重要だと考えています。 自分たちで自分たちの仕事を面白くしていきましょう!(求人) まだ業務を体系化できていないので表向きには求人をオープンしていませんが、自分のテーマや信念をもとに使命を開拓していくぞ!という心意気の方であればきっと楽しめると思います。 自分たちで自分たちの仕事を面白くしたり、意義のあるものにしたりしていきましょう💪 Requirements 高い自由度を活かして、自律的に動き回れる 手探りの状況でも、試行錯誤しながら前進できる 不確実な状況や曖昧な課題に、結論を焦らず粘り強く向き合える 周囲を巻き込みながら、行動の変容を生み出せる もしご興味を持たれた方は オープンポジション求人 や、個人的につながりのある方は各種 DM 等でお問い合わせください (・∋・)ノシ https://hrmos.co/pages/kinto-technologies/jobs/0000127 ほかにも KINTO テクノロジーズでは、さまざまな部署・職種で一緒に働ける仲間を募集しています! 詳しくは 採用情報 をご覧ください
アバター
This article is the entry for day 13 in the KINTO Technologies Advent Calendar 2024 🎅🎄 Introduction Hello. I am ITOYU, a front-end developer in the New Car Subscription Development Group of the KINTO ONE Development Division. Engineers, are you using GitHub? At KINTO Technologies we do too! We rely on GitHub's Pull Request feature to review and merge code. When merging, you have several options to choose from—did you know that? In this article, I'll explain the differences between these options and share some pitfalls I've encountered along the way. Topics Covered GitHub Pull Request merge options: Create a merge commit Squash and merge Rebase and merge Common Pitfalls: Rebase and merge is not the same as git rebase . Your last selected merge option becomes the default for the next merge. Prerequisites A develop branch and a feature branch exist. You create a feature branch by branching off from develop , make changes, and then submit a Pull Request to merge it back into develop . The commit histories for the develop and feature branches are as follows: Commit history of the develop branch Commit history of the feature branch The Pull Request Merge Options on GitHub. Create a merge commit The Create a merge commit option merges the commits from the feature branch into the develop branch while preserving their hash values. It also generates a new merge commit. After merging, the commit history will appear as follows: Commit history of the develop branch after the merge You can see that the hash values of the original commits are preserved and that a new merge commit is created, maintaining a clear record of the merge. Features The hash value of the commit history from the merge source is retained. A merge commit is created, preserving a record of the merge in the commit history. Use cases When you want to keep multiple commits as is When you want to keep a clear merge history Squash and merge The Squash and merge option combines all all commits from the feature branch into a single commit before merging it into the develop branch. After merging, the commit history will appear as follows: Commit history of develop branch after merge Although multiple commits existed in the feature branch, they are consolidated into a single commit in the develop branch. Features Combines all commits from the merge source into a single commit. Use cases When you want to keep the commit history simple. When you need to consolidate multiple small changes into one commit. Rebase and merge The Rebase and merge option moves the commits from the feature branch right after the latest commit in the develop branch, and merges them into the develop branch. Unlike squash, the commit history remains intact. Commit history of develop branch after merge As you can see, no merge commit is created. Features Preserves the commit history from the feature branch without modifications. No merge commit is created, keeping the history clean. Unlike the original commit hash values, new commits are created during the rebase. Use cases When you want to keep the commit history intact without creating a merge commit. When you prefer a structured commit history by rebasing. Common Pitfalls: I've explained each option above, but now I'll highlight some pitfalls I've encounteres along the way. Rebase and merge and git rebase are not the same In a medium-sized project, our team was collaborating on a development branch, and the develop branch of the source branch had been updated. I considered using git rebase to clean up the commit history of our project's branches. However, this approach would require a force push , which I wanted to avoid on the branches we were all working on. Instead, I thought the Rebase and merge option in GitHub’s Pull Request feature might be a safer alternative. I believed this method would keep the commit history clean without requiring additional local work. After merging the Pull Request from the feature branch into the develop branch using the Rebase and merge option, I checked for any differences or deltas. There were a lot of differences. At first glance, the commit histories of both the develop and feature branches seemed identical, but the hash values were different. This occurs because Rebase and merge generates a new commit hash rather than preserving the hash value from the source branch. I realized that Rebase and merge and git rebase behave differently, and I shouldn’t expect them to produce the same results. The option you selected last time will be set as the default for the next merge. This one is more of a careless mistake than a pitfall, but it’s important to highlight. My team typically uses Squash and merge to keep a clean commit history in the working branch. After my previous unsuccessful attempt with Rebase and merge , I returned to my usual workflow. Once the Pull Request I submitted was approved, I merged it as usual. However, something was off. For some reason, Rebase and merge was executed instead of Squash and merge … It turns out that the Pull Request merge options you select last are automatically set as the default for the next merge. This might seem obvious after you think about it, but I didn’t realize it at first. I learned to be more cautious when changing merge options, as the selected option will carry over to the next Pull Request, potentially affecting your workflow if you’re not paying attention. Conclusion When merging a GitHub Pull Request, selecting the appropriate option is crucial based on the characteristics of the merge and your specific needs. Here’s how I differentiate the options: Create a merge commit : I use this option when I want to retain the commit hash from the feature branch and maintain a clear record of the merge history. This makes it easier to trace which branches were merged and when. Squash and merge : I prefer this option when I want to combine my working commits into a single commit and maintain a clean, simple commit history. This helps combine smaller commits into one, making the commit history more concise and easier to read. Rebase and merge : I choose this when I want to keep my commit history linear without creating merge commits. This makes it easier to follow the flow of changes and keeps the history clean. Additionally, always double-check the currently selected merge option before merging a Pull Request to avoid any unexpected issues.
アバター
What are UUIDs and which version should you use? Recently, we had to respond to an incident: a service was down because of duplicate keys in its database. My team and I were scratching our heads because these were UUIDs - you know, those supposedly 'unique' identifiers. How could we have duplicates? Turns out the issues were caused by the service trying to add the same event twice, not that the same UUID was generated twice. This incident got me thinking about UUIDs. What they are? How are they generated? What are their use cases? And most importantly, which version should you use? What is a UUID? UUIDs are usually used to provide an ID for a resource. UUID stands for "Universally Unique IDentifier". Looking at the name, there seems to be strong expectations of uniqueness for the values being generated. That's with good reasons: even if we generated a huge amount of UUID, for instance a few quadrillions UUID (it's what comes after trillions), there is a 99,999% chance of them being unique. If you are interested in the math behind these odds, I recommend reading this really great article . UUIDs are 'practically unique' rather than 'guaranteed unique.' The probability of a collision is so small that for most applications, it's more likely that your hardware will fail or that a cosmic ray will flip a bit in your machine's memory than it is that you'll experience a UUID collision. However, it's worth noting that these probabilities assume proper random number generation. If your random number generator is flawed or predictable, the actual probability of collisions can be much higher. I'll explain bit more later in the article. If you work in software, you probably already know what a UUID looks like, but just in case: UUIDs are 128 bits wide and composed of 5 parts, separated by hyphens. They are usually represented using hexadecimal digits, looking something like this: ccba8c00-cbed-11ef-ad79-1da827afd7cd 74febad9-d652-4f6b-901a-0246562e13a8 1efcbedf-13bf-61e0-8fb8-fe3899c4f6f1 01943a0e-dd73-72fd-81ad-0af7ce19104b But wait! These UUID were actually generated using different version of UUID! In the list of UUID above, they are generated using the following versions in this order: version 1, version 4, version 6, version 7. Try to figure out where the version is indicated in the UUID. Hint: it's somewhere in the middle. Hopefully you noticed that the version of the UUID is indicated in the first character of the third part of the UUID, right in the middle of the UUID. There is also a variant being indicated in the first character of the fourth part. The version is used to indicate how the UUID was generated and the variant is used to indicate the layout of the UUID, but you probably won't need to worry about the variant, the version matters the most. So as we discussed, there are multiple versions of UUIDs. Aside the version indicator that we discovered earlier, what are the differences between each version? Are they all equally able to generate unique UUIDs? Also, why would you use one version over another? Obviously, you should use the latest and greatest version of UUID, right? Very good question! Let's take a look at the different versions of UUID. Version 1 and Version 6 Version 1 and 6 UUID are generated using the current time and the MAC address of the computer that generated the UUID. The timestamp part is located at the front of the UUID, and may include random bits or incremented counters depending on the computer's CPU. The MAC address part is located at the end, so if you use the same computer that part should never change. Interestingly because the MAC address can be retrieved from a UUID, there is a privacy risk when generating a UUID version 1 or 6. But that's also one of the pro of this version of UUID: two computers cannot generate the same UUID. That makes these versions useful in distributed systems where global uniqueness is needed. The difference between version 1 and 6 is the order in which parts of the timestamps is used in the UUID. Unlike version 1, version 6 UUID can be sorted chronologically, which can be useful for ordering in databases. As version 1 and 6 uses predictable elements (the time of generation and the mac address), it is possible to guess a UUID, which makes it unsuitable for uses that requires the UUIDs to remain secret. Version 2 Version 2 is similar to Version 1 in that both use a timestamp and the MAC address of the computer generating UUIDs. However, Version 2 also uses additional identifier data, namely the POSIX UID or GID. This makes Version 2 less random and use less of the timestamp than Version 1 and 6. As a consequence, there is a limited number of UUID v2 that can be generated at a given time, making it less desirable for most uses. It is rarely used and usually not supported by most libraries. It's also not documented in the UUID specification. Version 3 and 5 Version 3 and 5 are quite different from the other UUID. While the other versions aim to be random, Version 3 and Version 5 aim to be deterministic. What does that mean? They both use hashing algorithms to generate the UUID, making the UUID reproducible. There is no randomness or timestamp used to produce the UUID, a given input should always produce the same UUID. Version 3 uses the MD5 hashing algorithm while Version 5 uses SHA1. These versions are particularly useful when you need to generate the same UUID repeatedly from the same input data. For example, imagine you're creating UUIDs for users based on their email addresses - you'd want the same email to always generate the same UUID, even across different servers or times. Another good example would be when you need to generate a primary key based on some data to avoid duplicates, but using the data itself as the primary key is not a good option. When choosing between Version 3 or Version 5, you should keep in mind that SHA1 is a little more secure but also more compute intensive. If that is a concern for your use case, you might want to use Version 3 to reduce usage of compute resource but most of the time you should pick Version 5, as it is much more secure. It's also more likely that you will experience a collision with MD5 than with SHA1, but the probability is still very low. Version 4 Version 4 is the most widely used version of UUID. It uses random bits to generate the UUID, making them unique and unpredictable. It relies heavily on random number generation, but not all random number generators are actually capable of generating true random numbers. Shocking, I know. Many programming languages use what's called a Pseudo-Random Number Generator (PRNG), which is fine most of the time, but for UUID generation you'll want to ensure your system uses a Cryptographically Secure PRNG (CSPRNG). Why? A regular PRNG might be predictable if someone analyzes enough of its output. CSPRNGs, on the other hand are specifically designed to make predicting their output practically impossible, even if an attacker knows all previously generated values. Most modern UUID libraries use CSPRNGs by default but it's worth checking just to be sure. Like for the other version, the only predictable part is the version indicator, so you could try impressing your friends by guessing that part. They are great for most usage, generally when you need to generate a large amount of UUID and don't need to sort them or reproduce them later. They are often used as keys in databases. Version 7 Version 7 is designed to be a chronologically sortable variant to Version 4. Like Version 4, it uses random bits but includes a timestamp, making the UUID sortable and unique. They can be a great alternative to Version 4 where you want uniqueness, but want to be able to sort them by creation time. Version 7 also uses Epoch time for its timestamp, while Version 1 and 6 use the number of 100-nanosecond intervals since 15 October 1582. This makes Version 7 a little easier to work with. Version 8 Version 8 is a bit special, because it is custom. Vendors can implement it how they wish. You can implement it yourself, and you just need to respect the UUID version placed in the third part of the UUID. You probably will never need to use it. So, what should you use? For most people, it will be version 4. It has the greatest guarantee of uniqueness and is relatively secure (as long as the random number generator is not predictable). If you want to be able to sort you UUID by creation time, you can reach for version 7 or even version 6 as long as you don't have any privacy concern with leaking your MAC address. For some cases, version 3 and 5 are useful, but for most applications their use is limited. Database keys? Maybe you've seen discussion about using UUID for database key, and there are a few facts that you should keep in mind if you are thinking of using UUID for your database keys: UUIDs are large, they take up 128 bits. If you do not plan to store large amounts of data, the extra space taken for your UUID might be significant. Alternatively, a 32 bits auto incremented integer should give you about 2147483647 rows, and if that's not enough a 64 bits BIGINT goes up to 18446744073709551615. That should be enough for most use cases. For some databases, if you use UUID for your keys, insert performance may suffer. If insert performance is a concern, you might want to consider using auto incremented integer, or at least test the performance of your database with UUID. UUID make it easier to migrate data, as you will have collision when using an auto incrementing integer but probably won't have that issue with UUID. Even if some UUID are sortable, they are not easy to read. Looking at two UUID, it's quite hard to know which one came first. That's quite minor but it's something to keep in mind. Most database have some kind of module or function to generate UUID, so you can check the documentation of your database to see how to generate UUID. They will probably tell you there if there are some performance issue or special consideration to take into account when using UUID. Conclusion Hopefully you now understand UUIDs and their different versions a bit better than before reading this article. Version 4 UUIDs remain the go-to choice for most applications. They have strong uniqueness guarantees and unpredictability, which is probably what you want from UUIDs. They're mostly used for database keys, distributed systems, and any scenario where you need globally unique identifiers without coordination. Version 7 is a good alternative when chronological sorting is desirable, as it offers a good balance between randomness and sortability. Version 1 and 6 are useful in distributed systems where global uniqueness is needed, but they come with privacy concerns due to the inclusion of MAC addresses. Version 3 and 5 are useful when you need to reproduce the UUID from a given input, but keep in mind that MD5 is not as secure as SHA1. If you plan to use UUID in your systems, keep in mind these factors when choosing UUID version choice: Your uniqueness requirements Whether chronological sorting is needed Privacy concerns (especially if using versions that include MAC addresses) Storage space constraints (maybe you don't need 128 bits for your keys) While UUID collisions are theoretically possible, they're so improbable that they shouldn't be a primary concern in your system design - as long as you're using a proper implementation with a cryptographically secure random number generator. If you do encounter a UUID collision (congratulations on defying astronomical odds!), it's more likely due to an application logic issue, like duplicate event processing, rather than an actual UUID generation collision. In such cases, focus on investigating your application's handling of unique constraints rather than questioning the UUID generation itself.
アバター
Hello, this is HOKA from the Manabi-no-Michi-no-Eki (Learning Roadside Station) team. The Manabi-no-Michi-no-Eki team functioned a bit like a club activity, where we all worked on both our main jobs and this project during office hours. But this fall (September?), we officially joined the Developer Relations Group! For more details, check out this blog ↓↓ https://blog.kinto-technologies.com/posts/2024-12-03-the-next-goal/ Since we joined the Development Relations Group, we figured, why not contribute to the December Advent Calendar from our team as well? We had previously written a Tech Blog together, so we set up the MTG in a similarly casual manner this time as well, and Nakanishi, the founder of the KINTO Technologies Tech Blog, said with enthusiasm, "Let's write 15 articles." "Wait… was that really the plan? " I wasn’t so sure, so I asked him what the 15 topics were. Here’s the list: 10 about podcast episodes 1 about manabyi 1 about our portal One article reflecting on our past year Our future plans in the Development Relations (DevRel) Group Looking at this list, I couldn’t help but wonder… "Do we really have that much to write about? Is this even interesting?" "No, no, let's just keep writing. For example, you wrote a blog in the spring. You can write about the reaction within the company, the change in the atmosphere, etc." said Nakanishi-san, full of confidence. Since I have some PR experience, I figured I could write something, so I reluctantly said, "Okay, I’ll give it a shot". Just as I was about to wrap things up, Kin-chan spoke up. "HOKA-san, you’re not really convinced, are you? Are you forcing yourself?" He saw right through me. Since it's work, I thought it was natural that there would be things I wasn't fully happy with and things I would have to force myself to do, so I answered honestly, "YES." Then I asked, "If we don’t even have enough material to write about, why do this? " I thought our current activities are not enough to be shared, and honestly, I didn't find them all that interesting to read either (full disclosure here.) The conversation went like the above: Nakanishi: I want you to write as if you were talking to your past-self before joining KTC. If you learned the company had this kind of atmosphere, you might actually want to join, right? Hoka: Hmm. Yeah, nope. Doesn’t sound like something I’d want to read at all... Nakanishi: Honestly, a Tech Blog only needs to really hit home for one or two people a year. Hoka: "!?" Nakanishi: Honestly, there are articles on Tech Blog that describe common things that happen in any company. But without it, the people outside KTC wouldn’t have any idea what’s actually going on here. So it doesn’t have to be some amazing discovery. Just writing about what’s happening at the company is enough. Because when people read it, they’ll simply see, "Oh, so that’s what’s been going on". In other words, all you need to do is document what you’ve done." Hoka: "!?!?!?!?!?!?!?" Nakanishi: "Just write what you did, that’s it". That makes it easy, and anyone can. And even if each post is just a small part of what happens at the company, if everyone keeps doing it, all those pieces will come together to show what KTC is really like." HOKA: "Got it. That makes so much sense (imagining a patchwork diagram in my head). " Kin: That’s Nakanishi-san’s strategy, isn’t it? It's what differentiates our Tech Blog from others. Also HOKA-san's concerns are the same as those engineers who have not written Tech Blog articles yet. I feel relieved to have heard HOKA-sans concerns." I spent 10 years in corporate PR, where my writing goal was always to strip away personal feelings and concisely convey business performance and brand image. That’s because my readers were always time-strapped reporters and editors. But today was different—I got to experience firsthand how engineers communicate in a company built around engineers, and I learned a lot from it. Honestly, it was one of the most eye-opening moments since joining the company, so I had to put it into writing rightaway. ##Summary Our Tech Blog serves as a record of what happened. The Learning Road Station Team is an amazing team where people can speak honestly. They are wonderful teammates who support each other through uncertainty. Everyone involved is continuously learning.
アバター
Migrate from EncryptedSharedPreferences to Tink + DataStore 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. In this post, I've summarized the story of how I replaced an Android app that implemented EncryptedSharedPreferences, which has now been officially deprecated. Introduction EncryptedSharedPreferences has been deprecated since v1.1.0-alpha07, with an official recommendation to replace it with Android KeyStore . ![Updates of security-crypto](/assets/blog/authors/osugi/20250616/security-crypto.png =600x) Investigating Alternatives to EncryptedSharedPreferences With EncryptedSharedPreferences being deprecated, we began exploring options for both data persistence and encryption. Choosing a Data Persistence Method In our app's use case, EncryptedSharedPreferences had only been used to store configuration data, so using SharedPreferences alone would have been sufficient. However, since we had this opportunity to refactor, we decided to follow the official recommendation and adopted DataStore as our persistence means. Choosing an Encryption Library Following the official recommendation mentioned earlier, we initially planned to use Android KeyStore . However, we found that not only are there functional limitations depending on the API level, but achieving a high level of security using StrongBox also depends on the device specifications. This meant that simply implementing it in code might not guarantee the intended level of security. In our case, since the app was designed to run on devices managed via MDM, and we had already selected devices that support StrongBox, this limitation was not an issue. During our research on encryption libraries, we also came across Tink , a cryptographic library provided by Google. Looking at Tink’s repository , we found that it uses Android KeyStore to store its master key. To compare Android KeyStore and Tink in terms of maintainability and performance, we created a sample implementation. Comparing Encryption Library Implementations Below is a summary of sample code using Android KeyStore with StrongBox and TEE, as well as using Tink. We found that both were relatively easy to implement at a basic level. That said, Android KeyStore has some challenges: Key generation settings must be adjusted depending on the encryption algorithm Developers are responsible for managing initialization Vectors (IVs) There are very few sample implementations available Tink, on the other hand, wraps these aspects nicely, making implementation smoother. Sample Implementation of Encryption and Decryption Using Android KeyStore class AndroidKeyStoreClient( private val useStrongKeyBox: Boolean = false ) { private val keyStoreAlias = "key_store_alias" private val KEY_STORE_PROVIDER = "AndroidKeyStore" private val keyStore by lazy { KeyStore.getInstance(KEY_STORE_PROVIDER).apply { load(null) } } private val cipher by lazy { Cipher.getInstance("AES/GCM/NoPadding") } private fun generateSecretKey(): SecretKey { val keyStore = keyStore.getEntry(keyStoreAlias, null) if (keyStore != null) { return (keyStore as KeyStore.SecretKeyEntry).secretKey } return KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_PROVIDER) .apply { init( KeyGenParameterSpec.Builder( keyStoreAlias, KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT ).setBlockModes(KeyProperties.BLOCK_MODE_GCM) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) .setIsStrongBoxBacked(useStrongKeyBox) .setKeySize(256) .build() ) }.generateKey() } fun encrypt(inputByteArray: ByteArray): Result<String> { return runCatching { val secretKey = generateSecretKey().getOrThrow() cipher.init(Cipher.ENCRYPT_MODE, secretKey) val encryptedData = cipher.doFinal(inputByteArray) cipher.iv.joinToString("|") + ":iv:" + encryptedData.joinToString("|") } } fun decrypt(inputEncryptedString: String): Result<ByteArray> { return runCatching { val (ivString, encryptedString) = inputEncryptedString.split(":iv:", limit = 2) val iv = ivString.split("|").map { it.toByte() }.toByteArray() val encryptedData = encryptedString.split("|").map { it.toByte() }.toByteArray() val secretKey = generateSecretKey() val gcmParameterSpec = GCMParameterSpec(128, iv) cipher.init(Cipher.DECRYPT_MODE, secretKey, gcmParameterSpec) cipher.doFinal(encryptedData) } } } Sample Implementation of Encryption and Decryption Using Tink class TinkClient( context: Context ) { val keysetName = "key_set" val prefFileName = "pref_file" val packageName = context.packageName var aead: Aead init { AeadConfig.register() aead = buildAead(context) } private fun buildAead(context: Context): Aead { return AndroidKeysetManager.Builder() .withKeyTemplate(KeyTemplates.get("AES256_GCM")) .withSharedPref( context, "$packageName.$keysetName", "$packageName.$prefFileName" ) .withMasterKeyUri("android-keystore://tink_master_key") .build() .keysetHandle .getPrimitive(RegistryConfiguration.get(), Aead::class.java) } fun encrypt(inputByteArray: ByteArray): Result<String> { return runCatching { val encrypted = aead.encrypt(inputByteArray, null) Base64.getEncoder().encodeToString(encrypted) } } fun decrypt(inputEncryptedString: String): Result<ByteArray> { return runCatching { val encrypted = Base64.getDecoder().decode(inputEncryptedString) aead.decrypt(encrypted, null) } } } Performance Benchmarking of Encryption Libraries We measured the encryption processing time of Android KeyStore and Tink. For Android KeyStore, we evaluated two execution environments: StrongBox and TEE . In the test code, a common encryption algorithm (AES_GCM) was set and the process of repeatedly encrypting 10KB of data was measured using Microbenchmark . By using Microbenchmark, measurements were taken on an actual Google Pixel Tablet using a thread other than the UI thread. The test code is shown below: import androidx.benchmark.junit4.BenchmarkRule import androidx.benchmark.junit4.measureRepeated import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ExampleBenchmark { @get:Rule val benchmarkRule = BenchmarkRule() @Test fun benchmarkTinkEncrypt() { val context = InstrumentationRegistry.getInstrumentation().context val client = TinkClient(context) val plainText = ByteArray(1024 * 10) benchmarkRule.measureRepeated { client.encrypt(plainText).getOrThrow() } } @Test fun benchmarkStrongBoxEncrypt() { val context = InstrumentationRegistry.getInstrumentation().context val client = AndroidKeyStoreClient(context, true) val plainText = ByteArray(1024 * 10) benchmarkRule.measureRepeated { client.encrypt(plainText).getOrThrow() } } @Test fun benchmarkTeeEncrypt() { val context = InstrumentationRegistry.getInstrumentation().context val client = AndroidKeyStoreClient(context, false) val plainText = ByteArray(1024 * 10) benchmarkRule.measureRepeated { client.encrypt(plainText).getOrThrow() } } } Here are the benchmark results: Encryption Backend Average Encryption Time (ms) Number of Allocations Android KeyStore (StrongBox) 209 4646 Android KeyStore (TEE) 7.07 4786 Tink 0.573 38 Compared to Tink, which performs encryption in software, both Android KeyStore (StrongBox) and Android KeyStore (TEE) take significantly longer to process due to hardware access. Although the device we used in this test is relatively high-spec for an Android device, using Android KeyStore—particularly StrongBox—may require careful consideration of the user experience (UX). Notes Incidentally, the actual execution environment used for key generation with Android KeyStore can be determined using the code below: val secretKey = generateSecretKey() val kf = SecretKeyFactory.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEY_STORE_PROVIDER) val ki = kf.getKeySpec(secretKey, KeyInfo::class.java) as KeyInfo val securityLevelString = when (ki.securityLevel) { KeyProperties.SECURITY_LEVEL_STRONGBOX -> "STRONGBOX" KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> "TEE" KeyProperties.SECURITY_LEVEL_SOFTWARE -> "SOFTWARE" else -> "UNKNOWN" } Log.d("KeyStoreSecurityLevel", "Security Level: ${ki.securityLevel}") Conclusion Since EncryptedSharedPreferences has been deprecated, we evaluated technologies as potential replacements. Following the official recommendation, we adopted DataStore as our data persistence solution. For encryption, after comparing Android KeyStore and Tink, we found that Tink was easier to use as it abstracts the key generation and encryption processes. It also offered better performance and met our security requirements, making it our preferred choice. It's worth noting that using Android KeyStore requires accounting for device-specific behavior. As such, it's important to carefully weigh both performance and security needs when choosing an encryption approach.
アバター
This article is the entry for day 5 in the KINTO Technologies Advent Calendar 2024 🎅🎄 I'm Onuma, an Android engineer in the Mobile Development Group at KINTO Technologies. I primarily work on developing the My Route , a mobility service app. In this article, I'll guide you through the process of building Android Automotive OS and developing in-vehicle apps, including both Android Automotive and Android Auto applications. Running Android Automotive OS on a Raspberry Pi What is Android Automotive OS? Android Automotive is an in-vehicle platform built on Android and integrated into the AOSP framework. It supports pre-installed Android apps for IVI systems, as well as second- and third-party Android apps. For more details, refer to the official documentation →  https://developer.android.com/training/cars?hl=ja#automotive-os What is AOSP? AOSP stands for Android Open Source Project, and all the elements that make up the Android OS are open source and available to the public. Android Open Source Project The latest OS developed by Google is released as open source after a certain non-disclosure period. Device developers customize the released OS by adding features or making modifications to suit their specific needs before installing it on smartphones, tablets, and other devices. What to prepare to build Android Automotive OS PC *It is necessary to meet the hardware requirements mentioned later. Display *A touch monitor is better. RaspberryPi 4B MicroSD 16GB should be enough. MicroHDMI-HDMI cable Hardware Requirements for Building OS: Ubuntu 22.04 Intel Gold 6226R (16 cores, 32 threads) At least 16 GB of RAM HD: 1TB *Note: Building on Windows or Mac OS is not supported. I tried to create an environment on AWS EC2 to build, but I gave up since the Free Tier couldn't meet the required specifications. Set Up the Build Environment Install the necessary tools for building. sudo apt-get install git-core gnupg flex bison build-essential zip curl zlib1g-dev libc6-dev-i386 libncurses5 lib32ncurses5-dev x11proto-core-dev libx11-dev lib32z1-dev libgl1-mesa-dev libxml2-utils xsltproc unzip fontconfig Add Repo and Local_Manifest Android OS consists of a large collection of source code. Repo is used to check out Android source code. The components are loosely coupled, with each one being managed and developed in an independent Git repository. Repo is a tool that manages these numerous Git repositories based on a management file called a Manifest file. ## Install the Repo launcher repo init -u [https://android.googlesource.​com/platform/manifest](https://android.googlesource.com/platform/manifest) -b android-13.0.0\_r35 --depth=1 ## Add the local_manifest git clone [https://github.com/grapeup/​aaos_local_manifest.git](https://github.com/grapeup/aaos_local_manifest.git) .repo/local\_manifests Add dav1d under line 46 <!-- FFmpeg --> of .repo/local_manifests/manifest_brcm_rpi4.xml Added missing dav1d library in the local manifest by jijith700 · Pull Request #5 · grapeup/aaos_local_manifest · GitHub ## Add missing dav1d library to local_manifest <!-- FFmpeg --> <project path="external/dav1d" name="raspberry-vanilla/android_external_dav1d" remote="github" revision="android-13.0" /> Compile . build/envsetup.sh lunch aosp_rpi4-userdebug make bootimage systemimage vendorimage -j$(nproc) Flashing and Deploying Images Clean the MicroSD card. sudo umount /dev/sdb* sudo wipefs -a /dev/sdb* sudo wipefs -a /dev/sdb Then create four partition tables and flash the images to the MicroSD card. There are three images to flash onto the microSD card: boot.img , system.img , and vendor.img . As I thought I could flash the images using the command sudo dd if=boot.img of=/dev/sdb1 bs=1M , I gave it a try, but the steps were too complicated. So, I used a partition editing tool called GParted . Boot Android Automotive OS Insert the microSD card into the Raspberry Pi to boot. It's convenient if you connect a touch monitor to the Raspberry Pi, you can operate it without a mouse. But I don't have a touch monitor, so I'm connecting a PC monitor instead ;-; Develop In-Vehicle Apps that Run on Android Auto and Android Automotive OS Next, I'll walk you through the basics of implementing and debugging in-vehicle apps on Android. While Android Auto works by connecting your smartphone to display apps on the car's screen, Android Automotive OS has Android built directly into the vehicle system, allowing apps to be installed directly. This time, I implemented a navigation app as a trial. The following development environment is a Mac. Supported app categories and corresponding Android APIs Category Description Corresponding Android API Media Apps for music, podcasts and audiobooks Use the MediaBrowserService to browse content and control playback. Use the MediaSession to notify the system of playback status and metadata. Navigation Turn-by-turn navigation with audio and visual guidance Use the NavigationManager from the CarAppLibrary to control navigation start, end, destination setting, and turn-by-turn guidance. Point of Interest (POI) Apps to find locations such as parking lots, EV charging spots, gas stations, etc. Use the PlaceClient to implement features such as finding locations, getting more information, and Place Autocomplete. Use the PlaceListMapTemplate from the CarAppLibrary to view POIs on a map. Messaging ( Android Auto only) Hands-free message replies with voice input Use the MessagingManager from the CarAppLibrary to control sending and receiving messages, voice input, and sending template messages. Game Apps for entertainment while parked Use the ScreenManager from the CarAppLibrary to view the game screen while parked. Use the InputManager to receive control inputs for your game. Browser & Video Browser integration and video playback features (specific to AAOS, often used while parked) Use the WebTemplate from the CarAppLibrary to display web content. Use the VideoTemplate to play video content. These templates are recommended for use only while parked. Supplement I've summarized the key points from the table in the Official Documentation . Since new categories are added every year, even if you can't widely release your app yet, there is a possibility it could be released in the future. CarAppLibrary is a Jetpack library for Android Auto and Android Automotive OS app development. PlaceClient is a client that uses the Google Places API. Desktop Head Unit (DHU) What is a DHU? DHU is a tool for emulating the Android Auto environment on a desktop. It allows you to simulate the in-vehicle experience without using an actual in-vehicle device. Why use DHU? You can test how the app operates and displays in an in-vehicle environment. It lets you debug and verify that your UI/UX complies with guidelines to avoid distracting drivers. Run the DHU The following are required to run the DHU: MacBook Android device Install the Android Auto Desktop Head Unit Emulator in the SDK Manager. Make sure there is a desktop-head-unit in Library/Android/sdk/extras/google/auto. Grant permissions to the desktop-head-unit. chmod +x ./desktop-head-unit Forward the socket connection to the same port number on the Android device. adb forward tcp:5277 tcp:5277 Open the Auto settings on your Android device. Tap [See all apps] > [Android Auto] > [Advanced] > [Additional settings in the app]. Tap the version and permission information about 10 times to enable developer mode. Run the DHU. ./desktop-head-unit --usb About Host When running an app created for Android Auto or Android Automotive in a compatible car, the app doesn't interact directly with the car. At this stage, the connection destination is the Android Auto app on the Android device. During the DHU installation process, connecting to the actual device via USB is necessary since it needs to communicate with the Android Auto app, which serves as the host. The Android Auto app is referred to as the host, and all Auto-compatible apps interact with this host. In the case of an Android Automotive-compatible car, the OS is built into the vehicle system itself, so Android Automotive acts as the host. Libraries CarAppLibrary is a Jetpack library for Android Auto and Android Automotive OS app development. Apps built using the CarAppLibrary run through the host app rather than running directly on Auto or Automotive. Declare the version of CarAppLibrary in the project-level build.gradle. buildscript { ext { car_app_library_version = '1.4.0' } } dependencies { ... implementation "androidx.car.app:app:$car_app_library_version" ... } Add services and sessions Add a class that inherits from CarAppService. You need to extend the CarAppService bound by the host. In the intent filter, you need to declare androidx.car.app.category.POI as the category for the car app. <service android:name="com.example.places.carappservice.PlacesCarAppService" android:exported="true"> <intent-filter> <action android:name="androidx.car.app.CarAppService" /> <category android:name="androidx.car.app.category.POI" /> </intent-filter> </service> The CarAppService abstract class cannot be overridden, such as onBind or onUnbind . The library takes care of proper interaction with the host app for you. You only need to implement createHostValidator and onCreateSession . The HostValidator returned by createHostValidator is referenced when the CarAppService is bound to verify that the host is trusted, and that the bind fails if it does not match the parameters defined by the host. ALLOW_ALL_HOSTS_VALIDATOR is a HostValidator that can only be used for validation. class PlacesCarAppService : CarAppService() { override fun createHostValidator(): HostValidator { return HostValidator.ALLOW_ALL_HOSTS_VALIDATOR } override fun onCreateSession(): Session { return PlacesSession() } } Add the PlacesSession class. class PlacesSession : Session() { override fun onCreateScreen(intent: Intent): Screen { return MainScreen(carContext) } } Template You need to choose from predefined templates and implement them according to the guidelines. The UI and UX of car apps are restricted because the UI needs to be optimal for drivers. Reference: Official template documentation In addition, add the necessary permissions to access templates for displaying maps. <uses-permission android:name="androidx.car.app.MAP_TEMPLATES" /> List Location Information Once launched, locations will be listed. The UI can be implemented using Composable. Add a MainScreen that inherits Screen from CarAppLibrary. To display the list of locations and a map, return a PlaceListMapTemplate using onGetTemplate . Templates are implemented using the Builder Design Pattern. Pass the items to be listed using setItemList and build the template to return. Use ItemListBuilder to build the items to be listed. class MainScreen( carContext: CarContext, ) : Screen(carContext) { override fun onGetTemplate(): Template { val placesRepository = PlacesRepository() val itemListBuilder = ItemList.Builder() .setNoItemsMessage("No data") placesRepository.getPlaces() .forEach { itemListBuilder.addItem( Row.Builder() .setTitle(it.name) // Each item in the list must have a DistanceSpan added to the title or text line. .addText( SpannableString(" ").apply { setSpan( DistanceSpan.create( Distance.create(Math.random() * 100, Distance.UNIT_KILOMETERS), ), 0, 1, Spannable.SPAN_INCLUSIVE_INCLUSIVE, ) }, ) .setOnClickListener { screenManager.push(DetailScreen(carContext = carContext, placeId = it.id)) } .setMetadata( Metadata.Builder() .setPlace( Place.Builder(CarLocation.create(it.latitude, it.longitude)) .setMarker(PlaceMarker.Builder().build()) .build(), ) .build(), ).build(), ) } return PlaceListMapTemplate.Builder() .setTitle("Places") .setItemList(itemListBuilder.build()) .build() } } View detailed location information Use PaneTemplate to implement the detail screen. class DetailScreen(carContext: CarContext, private val placeId: Int) : Screen(carContext) { private var isFavorite = false override fun onGetTemplate(): Template { val place = PlacesRepository().getPlace(placeId) ?: return MessageTemplate.Builder("Place not found") .setHeaderAction(Action.BACK) .build() val navigateAction = Action.Builder() .setTitle("Navigate") .setIcon( CarIcon.Builder( IconCompat.createWithResource( carContext, R.drawable.baseline_navigation_24 ) ).build() ) .setOnClickListener { carContext.startCarApp(place.toIntent(CarContext.ACTION_NAVIGATE)) } .build() val actionStrip = ActionStrip.Builder() .addAction( Action.Builder() .setIcon( CarIcon.Builder( IconCompat.createWithResource( carContext, R.drawable.baseline_favorite_24 ) ).setTint( if (isFavorite) CarColor.RED else CarColor.createCustom( Color.LTGRAY, Color.DKGRAY ) ).build() ) .setOnClickListener { isFavorite = !isFavorite // To capture updates to the screen state, call invalidate() to call `onGetTemplate` again. invalidate() }.build() ) .build() return PaneTemplate.Builder( Pane.Builder() .addAction(navigateAction) .addRow( Row.Builder() .setTitle("Coordinates") .addText("${place.latitude}, ${place.longitude}") .build() ).addRow( Row.Builder() .setTitle("Description") .addText(place.description) .build() ).build() ) .setTitle(place.name) .setHeaderAction(Action.BACK) .setActionStrip(actionStrip) .build() } } Launch the app Possible Errors When Trying to Launch Other Apps Caused by: androidx.car.app.HostException: Remote startCarApp call failed An error may occur when attempting to start navigation (where startCarApp is called). This is likely because a navigation app is not installed. You can easily find a navigation app by searching in the Play Store on the emulator. Vehicle Properties Available in the App Although not yet verified, the following properties should be available. The setting values may be adjustable in the emulator. Reference Vehicle Speed The current speed of the vehicle can be obtained. It is typically provided in km/h and is used for actions based on speed limits and driver assistance features. Fuel Level For gasoline vehicles, you can obtain the remaining fuel level in the tank. This can be used for features like "low fuel" warnings or suggestions for the nearest fuel station. Battery Level For EVs and Hybrids, you can monitor the state of the vehicle's battery. It is used to display charging status or remaining battery levels. Door Status The open/closed status of each door (front, rear, trunk, and hood) can be obtained. You can set up notifications when a door is left open or alerts to prevent forgetting to close it. Light Status The on/off status of the vehicle lights (headlights, high beams, fog lights, etc.) can be obtained. This allows for night mode switching and providing feedback to the driver. Engine Status The on/off/idling status of the engine can be obtained. The application can restrict certain actions when the engine is off. Parking Brake Status The status of whether the parking brake is applied or released can be obtained. This can be used to control app functionality and interactions while parked. Gear Position The position of the shift lever (Park, Reverse, Neutral, Drive, etc.) can be obtained. This allows for automatic activation of the back camera and interface changes based on the gear selection. Tire Pressure The tire information such as tire pressure can be obtained. This allows notification of low pressure warnings and maintenance alerts. External Temperature The external temperature can be obtained, allowing it to be used for weather-based interfaces or driver notifications based on driving conditions. Seat Occupancy Status The presence of passengers in each seat and seatbelt usage can be obtained. This is used to display warnings when seat belts are not fastened for safety reasons. Window Status The open/closed state of each window can be obtained. For example, a notification can be sent if a window is left open when the vehicle is turned off. HVAC Status The settings and status of the vehicle's air-conditioning system (heating, cooling, fan speed, and airflow direction) can be obtained. This allows the app to manage a comfortable in-car environment. GPS Location The vehicle's current GPS location can be obtained. This enables navigation apps and location-based services. Wiper Status The operational state of the wipers can be obtained. This helps adjust the UI based on weather and visibility conditions. Conclusion Thank you for reading to the end. Android Automotive OS The quality of the Android open source is well-maintained, making it easy enough for a beginner to clone, build, and boot it. However, the required PC specs are quite high. One of our engineers pointed out that boards with Android Automotive OS pre-installed are available worldwide, and I couldn't help but think, "Why didn’t you tell me sooner?" Nonetheless, getting the OS up and running was a highly rewarding experience. Auto and Automotive App Development This article ended up providing a broad overview of in-vehicle app development, but we discovered that the implementation process involves just a few steps. That said, the concepts of host app and emulator setup can be somewhat challenging to grasp. Since automotive app development doesn't allow for much UI customization, the real challenge and fun lie in refining what the app can do. In the future, as autonomous driving becomes mainstream, more categories may emerge, allowing drivers to enjoy gaming and other experiences. Bonus Reflecting on my childhood memories of driving, I was inspired by Hyuga-san's article to create AI-generated music. Here's what I came up with. It turned out pretty good and atmospheric. I suppose only my colleagues would stick around this long. Looking forward to hearing your thoughts! https://soundcloud.com/numami-775711983/5qclozsqk1mz
アバター
Continuous Delivery of Kubernetes Applications Using Only GitHub Actions Hello. My name is Narazaki, and I work in the Toyota Woven City Payment Solution Development Group. Our team is responsible for developing the payment infrastructure application for Woven by Toyota at Toyota Woven City . We build cross-functional payment solutions, covering everything from the backend to the web front end and mobile applications. The payment backend runs on Kubernetes and is developed using various cloud-native tools. This time, while following GitOps—an approach where infrastructure configuration files are managed and modified using Git, key to building and maintaining stable Kubernetes applications—we aim to implement the continuous delivery (CD) process using only GitHub Actions, instead of the commonly used cloud-native CD tools. The CD process in this setup is limited to: Applying changes to Kubernetes configuration files Updating the container image While there are more advanced CD strategies like Blue/Green and Canary deployments, this approach starts small. This setup is designed for teams that already have a DevOps workflow and want to continuously and efficiently deliver Kubernetes applications with minimal developers and no additional tools—using only GitHub Actions, which they already use daily. The repository assumes that both the application code and Kubernetes configuration management files are maintained in the same repository. (Technically, it might be possible to run this across repositories depending on permission settings, but let’s not get into that here.) For GitLab users, there’s an excellent tool called Auto DevOps , so this isn’t a 'GitHub and GitHub Actions are the best!' kind of post. But don’t worry, I’m not making that claim! Cloud-Native CI/CD Tools for Kubernetes What tools come to mind when you think of CI/CD for Kubernetes applications? Argo CD Flux CD PipeCD Tekton And so on. Both tools are powerful and highly useful for leveraging Kubernetes to its full potential. They also allow for flexible and secure updates to Kubernetes configuration files and application images, enabling GitOps practices. On the other hand, they require tool-specific knowledge and expertise. For smaller teams without dedicated DevOps specialists, maintaining them continuously can be a challenge—wouldn’t you agree? Running a CD tool itself requires Kubernetes, and the tool also needs Kubernetes configuration files to manage those same configuration files. In this article, we‘ll explore how to set up the pipeline shown in the figure below using only GitHub Actions. Kubernetes runs on a generic cluster, not tied to any specific cloud provider. This setup requires a container registry. The configuration management file uses Kustomize as an example, but it can be applied to other tools like Helm, Terraform, and more. flowchart TD A[Change Code] -->| Run Build Pipeline | B[Build and Push Container Image] B -->| Trigger pipeline to update container images | C[Create pull request with updated container image references] C -->| Review pull request | D[Deploy new container image to Kubernetes] linkStyle default stroke-width:2px,color:blue,stroke-dasharray:0 Demo Consider a repository that includes folders for both Kubernetes configuration files and applications. The folder structure is as follows: This section omits specific code, Dockerfile contents, and application source code. ├── .github │   ├── actions │   │   └── image-tag-update │   │   └── action.yaml │   └── workflows │   ├── build-go.yaml │   ├── build-java.yaml │   ├── build-node.yaml │   └── kubectl.yaml ├── go-app │   ├── src/ │   └── Dockerfile ├── java-app │   ├── src/ │   └── Dockerfile ├── k8s │   ├── service-go.yaml │   ├── service-java.yaml │   ├── service-node.yaml │   └── kustomization.yaml └── node-app    ├── src/ └── Dockerfile Each application follows the structure below: apiVersion: apps/v1 kind: Deployment metadata: name: app spec: ... template: ... spec: containers: - name: some-server image: Go-placeholder # put the same string as in kcustomization as a placeholder All placeholders are centrally managed in kustomization.yaml: apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: techblog resources: - service-go.yaml - service-java.yaml - service-node.yaml images: - name: go-placeholder newName: go-app newTag: v1.1.1 - name: java-placeholder newName: java-app newTag: v2.7.9alpha - name: node-placeholder newName: node-app newTag: latest First, to apply the Kubernetes configuration file, configure the following GitHub Actions workflow. name: kubectl on: pull_request: branches: - "**" paths: - "K8s/**" the location of the #Kubernetes manifest file push: branches: - main paths: - "k8s/**" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: azure/setup-kubectl@v4 - env: KUBECONFIG_CONTENTS: ${ secrets.KUBECONFIG_contents }} # Put kubeconfig in GitHub secrets beforehand run: | echo "${KUBECONFIG_CONTENTS}" > $HOME/.kube/config chmod 600 $HOME/.kube/config - run: kubectl apply --dry-run=server -k ./k8s >> $GITHUB_STEP_SUMMARY - if: Github.ref == 'refs/heads/main # Changes are actually applied only on the main branch run: kubectl apply -k k8s/ This pipeline applies a standard Kubernetes configuration when using a kubeconfig with administrator privileges. Adjust the method of retrieving the kubeconfig based on the cluster’s configuration approach, such as for different cloud environments. Next, automatically create a pull request when pushing an application, or set up a composite action to update the container’s image tags. name: image-tag-update description: 'Task to update image tags in kustomization when container images are updated. inputs: target_app: description: 'Target applications, required: true tag_value: description: 'New Container Image Tag ' required: true token: description: 'Tokens with PR and content update privileges. required: true runs: using: 'composite' steps: - uses: actions/checkout@v4 id: check-branch-exists continue-on-error: true with: ref: "Image-tag-update" # Default branch name for tag updates - uses: Actions/checkout@v4 # Checkout cannot fall back to the default branch if the specified branch is missing if: steps.check-branch-exists.outcome == 'failure' with: ref: main - uses: Mikefarah/yq@master # Replace the value of the target placeholder tag with yq with: cmd: yq eval '(.images[] | select(.name == "'"${{ inputs.target_app }}-placeholder"'")).newTag = "'"${{ inputs.tag_value }}"'"' -i k8s/kustomization.yaml - uses: peter-evans/create-pull-request@v6 if: steps.check-branch-exists.outcome == 'failure' # Create a new pull request if no pull requests exist with: title: 'Update Container Image. body: | Update `${{ inputs.target_app }}' branch: "image-tag-update" - uses: stefanzweifel/git-auto-commit-action@v5 if: steps.check-branch-exists.outcome == 'success' # Add a commit to the existing branch if checkout succeeds with: commit_message: "Image update for ${{ inputs.target_app }}" The composite runs during image creation for each application. For multiple applications, it’s a good idea to add them after each image is created. ... - uses: docker/setup-buildx-action@v3 - uses: docker/build-push-action@v6 with: file: ./Dockerfile push: true tags: ${ env.tag }} # some tag - uses: ./.github/actions/image_update if: github.ref == 'refs/heads/main' with: target_app: go tag_value: ${{ env.tag }} token: ${ secrets.GITHUB_token }} # A github token with content and Pull Request editing privileges When the application runs,the container image updates automatically, letting you deploy a new one with a Pull Request! (Tag derivation is handled by your workflow.) The example below shows the minor version increment.) - name: go-placeholder newName: go-app - newTag: v1.1.1 + newTag: v1.1.2 Operational considerations Timing of deployment Image update Pull Request deploy immediately upon merging. If you want to release updates along with infrastructure changes, you can either add the fixes to the same branch or merge them when the timing is right. Add a new container application For example, if you add a Python application in the above setup while an image update Pull Request is still open, updating the Python image tag won’t take effect unless the Pull Request includes the latest changes. Cut back It’s easy to undo—just revert the commit. Timing of Reconcile While many GitOps Tools offer near real-time reconciliation to minimize drift, this method only works when the CD pipeline is running. It’s important to choose the right tool based on the number of teammates and their permissions to update the Kubernetes cluster. You’re interacting with the Container Registry indirectly. While some retrieve the latest container image directly from the container registry, this approach operates differently. It's advisable to include a verification step for each container registry to ensure the container exists. About permission settings for GitHub Actions You’ll need update permissions for contents and pull-requests . Set permissions in Actions settings, GitHub Apps, and more. Learn more here . Overwritten by a container image that was executed later The CD tool determines the newer version by checking the container image tag, following conventions like Semantic Versioning. The workflow above will overwrite the image tag in the later-executed pipeline, regardless of the tag’s value. If this behavior is an issue, consider checking the value before deciding whether to overwrite it. Summary With this approach, GitOps can be fully managed on GitHub, enabling a simple and efficient continuous delivery process for Kubernetes applications. Since CD tool errors can also be consolidated in GitHub Actions, it’s convenient to check execution results and errors just like in the usual CI process. Kubernetes offers a wide range of tools, making selection challenging. However, by choosing the right tools for my workflow, I aim to improve the productivity of Kubernetes application development.
アバター
KINTO ID Platformチームの Xu Huang です。数年前から複数の国にユーザー認証認可システム(略称UserPool)を提供し、Brokerモデルを採用して複数地域のUserpoolを繋ぎ、お互いに認証認可情報を共有できるアーキテクチャを構築して運用していました。昨年からコスト削減活動の一環としてアーキテクチャの設計を見直し、移行を行いましたので、その変更内容について紹介したいと思います。 最初はGlobal展開の戦略でAWS Aurora Global Database(略称Global DB)採用し、アクセス負荷とレイテンシーを減らすためにSlave DBをUserpoolに近いところに配置してBrokerサーバーもSlave DBと同じリージョンに配置した運用にしていました。 (Global DB制約上MasterDB一つだけ、Slave DB最大五つまで許容) 上記の図に青枠に囲んだ地域に順次Userpoolサービスを提供し、ユーザーを一元化管理するために必要なユニークIDは集約したところから発行し、各リージョンのサブDBに同期して管理する方針で設計しました Phase 1 Global DB → Normal DBに変更し、書込み専用アプリ廃棄 アクセス負荷を重要視した設計で複数のリージョンにサーバー配置しましたが実際運用上はまだスケールアップが必要なレベルまで至っておらずコストが余計に掛かっている状態でした。 適切な構成にする為に、検証評価したところGlobal DBの必要性がないと判断した為、直接Brokerから共通DBにR&W可能の設計に変更しました。 変更後のアーキテクチャの以下の図のイメージになります Phase 2 Broker一本化 Phase 1 の対応でかなりコスト削減できましたがさらにコスト下げる検討し続け、Brokerが一つに集約できないかを検討し始めました。但し集約するには一つ課題あって、IDプロバイダーとしては外部サードパーティにもリダイレクトURL提示していてそれらが変更してしまうとサードパーティ側も合わせて変更作業発生するのでドメインを変更しない前提で移行できないか考えてみました。インフラチームにも協力してもらい、Route53にDNS設定を変更して向き先を新しい統合サーバーと繋ぐCloudFrontにスイッチしておけばドメインを変えなくてもいいと考えていました。 上記の図のような設計に変更すると、物理的にサーバー間通信の距離が離れているためUserpoolから集約したBrokerの通信にはレイテンシーどれぐらい影響あるかも気になって測ってみました。 結果はUserPoolからBrokerの通信は約10%遅くなりましたが、BrokerがDBと同じリージョンに配置したため速くなり、アーキテクチャ変更前後の全体から見るとあんまり変わらないため、Phase2の移行計画も立て進めてきました。 成果: 上記2段階でビジネスの実態に即して構成の最適化を行いました 今後は機能的なところも見直し作業継続して定期的にコスト削減の活動を行なって参ります。
アバター
This article is part of day 6 of KINTO Technologies Advent Calendar 2024 . 🎅🎄 Introduction Merry Christmas 🔔. I am Romie ( @Romie_ktc ) from Osaka Tech Lab, where I work on Android-side development for the my route app in the Mobile App Development Group. In the my route Android team, we are currently switching the UI implementation from XML over to Jetpack Compose (hereinafter, Compose). However, since we cannot refactor everything all in one go, there will inevitably be situations where parts converted to Compose will be layered on top of XML. In this article, how we implemented it by overlaying Compose on top of the BottomSheet XML. What the finished result looks like :::message alert This article is about implementing things while in the process of refactoring. Implementation everything entirely in either XML or Compose is recommended. ::: Basics The implementation is done with the following classes, which inherit BottomSheetDialogFragment. class MixComposedBottomSheetDialog : BottomSheetDialogFragment() Set the Basic Behavior of the BottomSheet. Here, we set the behavior of the BottomSheet. The following code is put in onCreateView. dialog?.setOnShowListener { dialogInterface -> val bottomSheetDialog = dialogInterface as BottomSheetDialog val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) // If you want to set rounded corners and a background color, follow the steps below context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) } val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) } bottomSheetBehavior?.let { behavior -> // Set maxHeight and peekHeight to whatever heights you want. behavior.maxHeight = EXPANDED_HEIGHT // Set the height for when the BottomSheet is expanded as far as it will go behavior.peekHeight = COLLAPSED_HEIGHT // Set the height for when the BottomSheet is displayed in its initial state behavior.isHideable = false behavior.isDraggable = true } } Compose By returning ComposeView via onCreateView, you can put Compose on top of BottomSheetDialogFragment. return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { BottomSheetContents() } } If this is all you want to do, then it is easy enough to understand. However, if you want to add a button that will always be at the bottom of the bottom sheet, things get trickier. Developing Things Further The button itself is implemented with Compose. However, adding a button in this way means it will not be displayed unless you scroll through the contents. return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { BottomSheetContents() ButtonOnBottomSheet() } } To ensure that the button will always be anchored to the bottom of the bottom sheet and will not get pulled about even by scrolling through the contents, you need to implement something like the following: val button = ComposeView(context ?: return@setOnShowListener).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { ButtonOnBottomSheet() } } To anchor the button to the bottom of the bottom sheet, use the following code. Using this code enables you to directly retrieve the layout implemented with BottomSheetDialogFragment. Consequently, it enables you to manipulate views more flexibly. val containerLayout = dialogInterface.findViewById<FrameLayout>(com.google.android.material.R.id.container) val coordinatorLayout = dialogInterface.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator) clipChildren is a property of ViewGroup that specifies whether to clip the drawing of a child view if it will be drawn outside the boundary of the parent view. It will be used when something overlaps other elements of the bottom sheet. // The default value is true, and setting it to false lets you display child views as is even if they go outside the boundary of the parent. button.clipChildren = false button.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, ).apply { gravity = Gravity.BOTTOM } containerLayout?.addView(button) button.post { val layoutParams = coordinatorLayout?.layoutParams as? ViewGroup.MarginLayoutParams layoutParams?.apply { button.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), ) this.bottomMargin = button.measuredHeight containerLayout?.requestLayout() } } Summary Summarizing the implementation so far, we have the following: override fun onCreateView(): View { dialog?.setOnShowListener { dialogInterface -> val bottomSheetDialog = dialogInterface as BottomSheetDialog val containerLayout = dialogInterface.findViewById<FrameLayout>(com.google.android.material.R.id.container) val coordinatorLayout = dialogInterface.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator) val bottomSheet = bottomSheetDialog.findViewById<View>(com.google.android.material.R.id.design_bottom_sheet) context?.let { bottomSheet?.background = ContextCompat.getDrawable(it, R.drawable.background_map_bottom_sheet) } val button = ComposeView(context ?: return@setOnShowListener).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { ButtonOnBottomSheet() } } button.clipChildren = false button.layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT, ).apply { gravity = Gravity.BOTTOM } containerLayout?.addView(button) button.post { val layoutParams = coordinatorLayout?.layoutParams as? ViewGroup.MarginLayoutParams layoutParams?.apply { button.measure( View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), ) this.bottomMargin = button.measuredHeight containerLayout?.requestLayout() val bottomSheetBehavior = bottomSheet?.let { BottomSheetBehavior.from(it) } bottomSheetBehavior?.let { behavior -> // Set maxHeight and peekHeight to whatever heights you want. behavior.maxHeight = EXPANDED_HEIGHT // Set the height for when the BottomSheet is expanded as far as it will go behavior.peekHeight = COLLAPSED_HEIGHT // Set the height for when the bottom sheet is displayed in its initial state behavior.isHideable = false behavior.isDraggable = true } } } } return ComposeView(requireContext()).apply { setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) setContent { BottomSheetContents() } } } In other words, here's how we implemented it: Create a BottomSheet in xml. Adjust the layout (i.e., the heights and so on). In order to attach the button to the bottom of the BottomSheet, prepare some more layout details and combine everything together. It means that we overlaid the Compose content onto the BottomSheet. Thank you for reading all the way to the end!
アバター
こんにちは! KINTOテクノロジーズ(以下、KTC)の生成AI活用PJTで生成AIエバンジェリストをしている和田( @cognac_n )です。 Azure AI Foundryを用いてDeepSeek R1を試してみた 1. はじめに 最近、大規模言語モデル(LLM)の進化がめざましく、さまざまな企業が独自のLLMを発表しています。その中でも注目を集めているのが、中国のAI企業DeepSeekが開発した DeepSeek R1 です。 2025年1月30日、MicrosoftはDeepSeek R1モデルを Azure AI Foundry とGitHubで提供開始しました。そこで本記事では、Azure AI Foundryを使ってDeepSeek R1を実際に試してみた際の流れや感想をまとめてみます。 2. Azure AI Foundryとは Azure AI Foundry は、Microsoftが提供する 包括的なAI開発プラットフォーム です。開発者が生成AIソリューションやカスタムコパイロットを 構築・評価・展開 するためのツールとサービスを一元的に提供してくれます。 主な特徴 多様なAIモデルの活用: MicrosoftやOpenAI、Hugging Face、Meta、Mistralなど、パートナー企業から提供される幅広い最先端モデル・オープンソースモデルをサポート。今回のDeepSeek R1もこの一環として提供されました。 統合されたAIツールチェーン: 開発ライフサイクル全体を加速させるためのSDKやAPI、ポータルが用意されており、データ前処理からモデル推論、継続的モニタリングまで一貫した体験が得られます。 責任あるAIの実践: 評価機能や安全フィルター、セキュリティ制御を組み込み、AIの信頼性と透明性を高める仕組みを提供します。 エンタープライズ向けのスケーラビリティ: Azureのマネージドインフラ上で高い可用性と拡張性が確保されており、企業レベルのモニタリング・ガバナンスをサポートします。 @ card Azure AI Foundry ポータルでモデル カタログを通じてモデルを使用する場合、プロンプトや出力がMicrosoftやモデル プロバイダーと共有されることはないと記載されており、様々なモデルをセキュアに利用することが可能です。 Microsoft では、ホスティング インフラストラクチャと API エンドポイントを提供および管理します。 この "サービスとしてのモデル" (MaaS) シナリオでホストされるモデルは、Azure のデータ、プライバシー、セキュリティのコミットメントの対象です。 Azure AI Foundry に適用できる Azure コンプライアンス認証の詳細を確認してください。 Microsoft は、従量課金制推論用にデプロイされたモデル (MaaS) により送信および生成されるプロンプトと出力のデータ プロセッサとして機能します。 Microsoft では、これらのプロンプトと出力をモデル プロバイダーと共有しません。 また、Microsoft は、Microsoft、モデル プロバイダー、またはサード パーティのモデルをトレーニングまたは改善するために、これらのプロンプトと出力を使用しません。 @ card 3. Azure AI Foundry上でのDeepSeek R1環境構築 ここからは、実際にDeepSeek R1をAzure AI Foundry上で利用するための手順を紹介します。なお、細かい画面遷移や設定項目については現時点(2025/1/31)の仕様となります。 UIや操作の変更が頻繁に行われるため、可能な限り公式ドキュメントを引用する形とします。 前提準備・アカウント設定 Azureアカウントの用意 Azure AI Foundryへのアクセス @ card DeepSeek R1の導入手順 プロジェクトの作成: Azure AI Foundryポータル上で新規プロジェクトを作成します。DeepSeek-R1が使用可能なリージョンは[eastus2, westus3, northcentralus, eastus, southcentralus, westus]です。(2025/1/31時点) @ card DeepSeek R1モデルの選択: 「モデルカタログ」からDeepSeek R1を探し、モデルページに遷移します。 デプロイ名の設定と価格の確認: 任意のデプロイ名を設定します コンテンツフィルターのON/OFFを設定します 2025/1/31時点で、DeepSeek-R1は無料提供されています。料金は今後変更されることが予想されるため、最新の情報を逐次チェックしましょう。 DeepSeek R1 use is currently priced at $0, and use is subject to rate limits which may change at any time. Pricing may change, and your continued use will be subject to the new price. The model is in preview; a new deployment may be required for continued use. 4. デプロイとエンドポイント構築: Azure AI Foundryのワークスペース内で自動的にエンドポイントが生成され、モデルを呼び出すURIやAPIキーが発行されます。 4. DeepSeek R1を使った簡単なデモ ここでは、実際にDeepSeek R1の推論を試すためのコード例を紹介します。 必要なライブラリをインストールします。 pip install azure-ai-inference APIを利用するコードの例を以下に記載します。 Azure AI Foundryではコードサンプルも提供されるため、トライが非常に簡単です。 from azure.ai.inference import ChatCompletionsClient from azure.ai.inference.models import SystemMessage, UserMessage from azure.core.credentials import AzureKeyCredential client = ChatCompletionsClient( endpoint="<YOUR_TARGET_URI>", credential=AzureKeyCredential("<YOUR_KEY>"), ) deploy_name = "<YOUR_DEPLOY_NAME>" response = client.complete( messages=[ SystemMessage(content="You are a helpful assistant."), UserMessage( content="企業における生成AI利活用を推進する方法を考えてください。日本語で回答してください。" ), ], max_tokens=2048, model=deploy_name, ) print(response.choices[0].message.content) 実際の応答例 <think> まず、問題は何かを理解する必要があるね。企業が生成AIを使う推進方法について考えなきゃいけないんだ。 生成AIといってもいろんな用途があるから、まずは企業内での現状を把握しなきゃ。どの部門がAIを活用できそうか、どのような課題があるのかを調べる必要があるだろう。例えば、マーケティングや顧客サポート、業務自動化などが考えられるね。 次に、教育や研修が大事だと思う。従業員がAIの使い方を学べる機会を提供しないと、実際に活用するのは難しい。特に、AIに対する不安や理解不足を解消するため、ワークショップやトレーニングプログラムを実施した方がいいかも。 また、適切なツールの選定も必要。自社のニーズに合った生成AIのプラットフォームやツールを選ぶことが重要。場合によっては、カスタマイズが必要かもしれないし、セキュリティやデータプライバシーの問題にも対応しなきゃいけない。 管理層のサポートも不可欠だよね。経営陣が推進しないと予算やリソースが確保できない。AI導入のメリットを明確に伝えて、経営層の理解を得る必要がある。 さらに、成功事例を作ることが効果的。一部の部門で試験的に導入して成果を出せば、他の部門にも広がりやすい。結果を共有して、社内全体に広げていく戦略がいいかもしれない。 あと、倫理的なガイドラインやポリシーを整備することも重要。AIの誤用を防ぎ、責任ある活用を促すため、明確なルールを作る必要があるだろう。 継続的な評価と改善も忘れちゃいけない。導入後も効果を測定し、フィードバックを元に改善を重ねることで、持続的な推進が可能になる。 そういったポイントを整理して、具体的なステップを考えていく必要がありそうだな。 </think> 企業における生成AIの利活用を推進するためには、以下のステップを体系的に進めることが効果的です。 --- ### **1. 現状分析と目標設定** - **ニーズの特定**: 各部門(企画、営業、開発、サポートなど)の業務課題を洗い出し、生成AIが解決できる領域(ドキュメント作成、データ分析、顧客対応等)を明確化します。 - **優先順位付け**: 効果が高く実現可能性の高いユースケース(例:問い合わせ自動返信、報告書自動作成)から優先的に導入します。 --- ### **2. 体制構築と意識改革** - **推進チーム発足**: 経営層・IT部門・各部門の代表者が連携し、予算・リソースを確保します。 - **教育プログラムの実施**: - **基礎理解**: 生成AIの仕組み・メリット・倫理リスク(情報漏洩、バイアス等)を研修で共有。 - **実践トレーニング**: プロンプト設計や出力検証の方法をハンズオン形式で習得させます。 --- ### **3. ツール選定とテスト運用** - **自社に適したAI選び**: - オープンソース(Llama 2等)/クラウドサービス(ChatGPT Enterprise、Google Vertex AI等)を比較。 - セキュリティ基準(データ暗号化、学習済みモデルのカスタマイズ可否)を軸に選定します。 - **PoC(概念実証)**: 限定範囲でテスト運用し、精度・業務効率化効果を検証します。 --- ### **4. ガバナンス整備** - **利用ガイドライン策定**: - 機密情報の入力禁止ルールや出力内容のダブルチェック体制を確立。 - 著作権・プライバシーに関する法的リスクに対応します。 - **モニタリング体制**: AIの出力品質や倫理面を定期的に監視し、改善を継続します。 --- ### **5. 社内文化の醸成** - **成功事例の共有**: 効果が出た部門のケースを社内発表やレポートで周知し、モチベーションを向上させます。 - **インセンティブ設計**: AI活用で業務効率化を達成したチームを表彰・報酬化します。 --- ### **6. 外部連携と進化対応** - **ベンダー・専門家との連携**: 最新技術動向の情報収集やカスタマイズ支援を受けます。 - **AIの継続的アップデート**: モデルの精度向上や規制変化に応じて活用方法を見直します。 --- ### **ポイント** - **経営層のコミットメント**: 投資対効果(ROI)の試算を示し、長期的な展望で支持を得ることが不可欠です。 - **現場主導の改善**: ユーザーである従業員のフィードバックを活用し、ツール改善やプロセス最適化を重ねます。 生成AIは「ツール」であり、活用の成否は「人間の使い方」に依存します。技術導入だけでなく、組織全体のデジタルリテラシー向上と心理的安全性の確保が鍵となります。 5. 実際に使ってみた感想・所感 レスポンス: 今回の回答には4分程度かかりました。LLM自体の推論にかかる時間と、利用が集中しているが故の処理時間の長さ、両方の影響がありそうです 出力精度: Thinkingを行う推論モデル故に抽象度が高かったり複雑度の高いプロンプトに対しても深い洞察を感じられる回答が返ってきます API利用では思考過程が タグに囲まれた形式で出力されます。これは使いやすいですね タグの中身を見ていくと、まるで複数人でディスカッションが行われたような思考過程を辿っています まとめ: Azure AI Foundryを用いると、最新のサードパーティ製モデルを手軽に試すことが可能です 「まず試す」ことが重要な生成AI領域において、この手軽さは非常にありがたいですね We Are Hiring! KINTOテクノロジーズでは、事業における生成AIの活用を推進する仲間を探しています。まずは気軽にカジュアル面談からの対応も可能です。少しでも興味のある方は以下のリンク、または XのDM などからご連絡ください。お待ちしております!! @ card ここまでお読みいただき、ありがとうございました!
アバター
こんにちは! KINTOテクノロジーズ(以下、KTC)の生成AI活用PJTで生成AIエバンジェリストをしている和田( @cognac_n )です。 Azure AI Foundryを用いてDeepSeek R1を試してみた 1. はじめに 最近、大規模言語モデル(LLM)の進化がめざましく、さまざまな企業が独自のLLMを発表しています。その中でも注目を集めているのが、中国のAI企業DeepSeekが開発した DeepSeek R1 です。 2025年1月30日、MicrosoftはDeepSeek R1モデルを Azure AI Foundry とGitHubで提供開始しました。そこで本記事では、Azure AI Foundryを使ってDeepSeek R1を実際に試してみた際の流れや感想をまとめてみます。 2. Azure AI Foundryとは Azure AI Foundry は、Microsoftが提供する 包括的なAI開発プラットフォーム です。開発者が生成AIソリューションやカスタムコパイロットを 構築・評価・展開 するためのツールとサービスを一元的に提供してくれます。 主な特徴 多様なAIモデルの活用: MicrosoftやOpenAI、Hugging Face、Meta、Mistralなど、パートナー企業から提供される幅広い最先端モデル・オープンソースモデルをサポート。今回のDeepSeek R1もこの一環として提供されました。 統合されたAIツールチェーン: 開発ライフサイクル全体を加速させるためのSDKやAPI、ポータルが用意されており、データ前処理からモデル推論、継続的モニタリングまで一貫した体験が得られます。 責任あるAIの実践: 評価機能や安全フィルター、セキュリティ制御を組み込み、AIの信頼性と透明性を高める仕組みを提供します。 エンタープライズ向けのスケーラビリティ: Azureのマネージドインフラ上で高い可用性と拡張性が確保されており、企業レベルのモニタリング・ガバナンスをサポートします。 @ card Azure AI Foundry ポータルでモデル カタログを通じてモデルを使用する場合、プロンプトや出力がMicrosoftやモデル プロバイダーと共有されることはないと記載されており、様々なモデルをセキュアに利用することが可能です。 Microsoft では、ホスティング インフラストラクチャと API エンドポイントを提供および管理します。 この "サービスとしてのモデル" (MaaS) シナリオでホストされるモデルは、Azure のデータ、プライバシー、セキュリティのコミットメントの対象です。 Azure AI Foundry に適用できる Azure コンプライアンス認証の詳細を確認してください。 Microsoft は、従量課金制推論用にデプロイされたモデル (MaaS) により送信および生成されるプロンプトと出力のデータ プロセッサとして機能します。 Microsoft では、これらのプロンプトと出力をモデル プロバイダーと共有しません。 また、Microsoft は、Microsoft、モデル プロバイダー、またはサード パーティのモデルをトレーニングまたは改善するために、これらのプロンプトと出力を使用しません。 @ card 3. Azure AI Foundry上でのDeepSeek R1環境構築 ここからは、実際にDeepSeek R1をAzure AI Foundry上で利用するための手順を紹介します。なお、細かい画面遷移や設定項目については現時点(2025/1/31)の仕様となります。 UIや操作の変更が頻繁に行われるため、可能な限り公式ドキュメントを引用する形とします。 前提準備・アカウント設定 Azureアカウントの用意 Azure AI Foundryへのアクセス @ card DeepSeek R1の導入手順 プロジェクトの作成: Azure AI Foundryポータル上で新規プロジェクトを作成します。DeepSeek-R1が使用可能なリージョンは[eastus2, westus3, northcentralus, eastus, southcentralus, westus]です。(2025/1/31時点) @ card DeepSeek R1モデルの選択: 「モデルカタログ」からDeepSeek R1を探し、モデルページに遷移します。 デプロイ名の設定と価格の確認: 任意のデプロイ名を設定します コンテンツフィルターのON/OFFを設定します 2025/1/31時点で、DeepSeek-R1は無料提供されています。料金は今後変更されることが予想されるため、最新の情報を逐次チェックしましょう。 DeepSeek R1 use is currently priced at $0, and use is subject to rate limits which may change at any time. Pricing may change, and your continued use will be subject to the new price. The model is in preview; a new deployment may be required for continued use. 4. デプロイとエンドポイント構築: Azure AI Foundryのワークスペース内で自動的にエンドポイントが生成され、モデルを呼び出すURIやAPIキーが発行されます。 4. DeepSeek R1を使った簡単なデモ ここでは、実際にDeepSeek R1の推論を試すためのコード例を紹介します。 必要なライブラリをインストールします。 pip install azure-ai-inference APIを利用するコードの例を以下に記載します。 Azure AI Foundryではコードサンプルも提供されるため、トライが非常に簡単です。 from azure.ai.inference import ChatCompletionsClient from azure.ai.inference.models import SystemMessage, UserMessage from azure.core.credentials import AzureKeyCredential client = ChatCompletionsClient( endpoint="<YOUR_TARGET_URI>", credential=AzureKeyCredential("<YOUR_KEY>"), ) deploy_name = "<YOUR_DEPLOY_NAME>" response = client.complete( messages=[ SystemMessage(content="You are a helpful assistant."), UserMessage( content="企業における生成AI利活用を推進する方法を考えてください。日本語で回答してください。" ), ], max_tokens=2048, model=deploy_name, ) print(response.choices[0].message.content) 実際の応答例 <think> まず、問題は何かを理解する必要があるね。企業が生成AIを使う推進方法について考えなきゃいけないんだ。 生成AIといってもいろんな用途があるから、まずは企業内での現状を把握しなきゃ。どの部門がAIを活用できそうか、どのような課題があるのかを調べる必要があるだろう。例えば、マーケティングや顧客サポート、業務自動化などが考えられるね。 次に、教育や研修が大事だと思う。従業員がAIの使い方を学べる機会を提供しないと、実際に活用するのは難しい。特に、AIに対する不安や理解不足を解消するため、ワークショップやトレーニングプログラムを実施した方がいいかも。 また、適切なツールの選定も必要。自社のニーズに合った生成AIのプラットフォームやツールを選ぶことが重要。場合によっては、カスタマイズが必要かもしれないし、セキュリティやデータプライバシーの問題にも対応しなきゃいけない。 管理層のサポートも不可欠だよね。経営陣が推進しないと予算やリソースが確保できない。AI導入のメリットを明確に伝えて、経営層の理解を得る必要がある。 さらに、成功事例を作ることが効果的。一部の部門で試験的に導入して成果を出せば、他の部門にも広がりやすい。結果を共有して、社内全体に広げていく戦略がいいかもしれない。 あと、倫理的なガイドラインやポリシーを整備することも重要。AIの誤用を防ぎ、責任ある活用を促すため、明確なルールを作る必要があるだろう。 継続的な評価と改善も忘れちゃいけない。導入後も効果を測定し、フィードバックを元に改善を重ねることで、持続的な推進が可能になる。 そういったポイントを整理して、具体的なステップを考えていく必要がありそうだな。 </think> 企業における生成AIの利活用を推進するためには、以下のステップを体系的に進めることが効果的です。 --- ### **1. 現状分析と目標設定** - **ニーズの特定**: 各部門(企画、営業、開発、サポートなど)の業務課題を洗い出し、生成AIが解決できる領域(ドキュメント作成、データ分析、顧客対応等)を明確化します。 - **優先順位付け**: 効果が高く実現可能性の高いユースケース(例:問い合わせ自動返信、報告書自動作成)から優先的に導入します。 --- ### **2. 体制構築と意識改革** - **推進チーム発足**: 経営層・IT部門・各部門の代表者が連携し、予算・リソースを確保します。 - **教育プログラムの実施**: - **基礎理解**: 生成AIの仕組み・メリット・倫理リスク(情報漏洩、バイアス等)を研修で共有。 - **実践トレーニング**: プロンプト設計や出力検証の方法をハンズオン形式で習得させます。 --- ### **3. ツール選定とテスト運用** - **自社に適したAI選び**: - オープンソース(Llama 2等)/クラウドサービス(ChatGPT Enterprise、Google Vertex AI等)を比較。 - セキュリティ基準(データ暗号化、学習済みモデルのカスタマイズ可否)を軸に選定します。 - **PoC(概念実証)**: 限定範囲でテスト運用し、精度・業務効率化効果を検証します。 --- ### **4. ガバナンス整備** - **利用ガイドライン策定**: - 機密情報の入力禁止ルールや出力内容のダブルチェック体制を確立。 - 著作権・プライバシーに関する法的リスクに対応します。 - **モニタリング体制**: AIの出力品質や倫理面を定期的に監視し、改善を継続します。 --- ### **5. 社内文化の醸成** - **成功事例の共有**: 効果が出た部門のケースを社内発表やレポートで周知し、モチベーションを向上させます。 - **インセンティブ設計**: AI活用で業務効率化を達成したチームを表彰・報酬化します。 --- ### **6. 外部連携と進化対応** - **ベンダー・専門家との連携**: 最新技術動向の情報収集やカスタマイズ支援を受けます。 - **AIの継続的アップデート**: モデルの精度向上や規制変化に応じて活用方法を見直します。 --- ### **ポイント** - **経営層のコミットメント**: 投資対効果(ROI)の試算を示し、長期的な展望で支持を得ることが不可欠です。 - **現場主導の改善**: ユーザーである従業員のフィードバックを活用し、ツール改善やプロセス最適化を重ねます。 生成AIは「ツール」であり、活用の成否は「人間の使い方」に依存します。技術導入だけでなく、組織全体のデジタルリテラシー向上と心理的安全性の確保が鍵となります。 5. 実際に使ってみた感想・所感 レスポンス: 今回の回答には4分程度かかりました。LLM自体の推論にかかる時間と、利用が集中しているが故の処理時間の長さ、両方の影響がありそうです 出力精度: Thinkingを行う推論モデル故に抽象度が高かったり複雑度の高いプロンプトに対しても深い洞察を感じられる回答が返ってきます API利用では思考過程が タグに囲まれた形式で出力されます。これは使いやすいですね タグの中身を見ていくと、まるで複数人でディスカッションが行われたような思考過程を辿っています まとめ: Azure AI Foundryを用いると、最新のサードパーティ製モデルを手軽に試すことが可能です 「まず試す」ことが重要な生成AI領域において、この手軽さは非常にありがたいですね We Are Hiring! KINTOテクノロジーズでは、事業における生成AIの活用を推進する仲間を探しています。まずは気軽にカジュアル面談からの対応も可能です。少しでも興味のある方は以下のリンク、または XのDM などからご連絡ください。お待ちしております!! @ card ここまでお読みいただき、ありがとうございました!
アバター