TECH PLAY

KINTOテクノロジーズ

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

936

Introduction Konichiwa! I'm Felix, and I develop iOS applications at KINTO Technologies. This was my first experience at a Swift-focused conference. From March 22nd to 24th, 2024, I attended the try! Swift 2024 Tokyo event held in Shibuya. It offered an excellent chance to delve into the latest industry trends and network with other engineers. Presentations Among the many compelling presentations, I would like to highlight two that stood out to me. First, I would like to talk about the Duolingo's AI Tutor feature. The speaker, Xingyu Wang, shared insights of implemenation of their AI tutor feature. She also presented challenges such as building a chat interface and optimizing latency for helpful phrases, along with solutions leveraging GPT-4's capabilities. It was nice that she provided insights into the entire architecture, addressing not just the frontend but also the current challenges they are encountering. Personally, I had a similar goal of developing an English learning app for Japanese users before. Having this knowledge would be invaluable in creating similar services. Their incorporation of a sophisticated roleplay feature enhances learners' ability to practice conversational skills in a lifelike setting. Next up, Pointfree, renowned in the community for their frameworks, particularly impressed me with their presentation on testing Swift macros, introduced in Swift version 5.9. Macros, which are compiler plugins, enhance Swift code by generating new code, diagnostics, and fix-its. The duo of presenters introduced the complexities of writing and testing these macros, highlighting the nuances of Swift. They also demonstrated how their testing library, swift-macro-testing, enhances Apple's tools by making the macro testing process more streamlined, efficient, and effective. This presentation showcased their deep understanding of Swift and their innovative approach to improving development workflows. Booths The booth area was bustling as attendees were eager to interact with companies and pick up some giveaways. Cyber Agent's booth was particularly engaging, featuring a whiteboard where attendees could write code Recaps on post-its. This interactive activity was both educational and effective in boosting enthusiasm. At this conference, after the presentations, there was an innovative approach to the usual Q&A sessions. Instead of a formal setting, those with questions could meet directly with speakers at designated booth areas. I think this setup allowed for more personal interactions, where attendees could engage in conversations, ask their questions, and socialize, facilitating better communication and networking opportunities. Workshop On the final day of the conference, attendees could choose their preferred workshops. I opted for the workshop on TCA and sat towards the back of a large room accommodating about 200 participants. The workshop mainly offered a walkthrough on using the Composable Architecture to develop a sample "SyncUp" app. Initially, I tried to code along but eventually just observed. An interesting aspect was the framework's structured approach to managing side effects, making parts of the app that interact with the external world both testable and understandable. The unit testing process seemed particularly streamlined and clear. Conclusion Attending the try! Swift 2024 Tokyo conference was a highly enriching experience. It provided a unique platform to immerse myself in cutting-edge Swift technologies and connect with industry leaders and peers. The presentations were insightful, providing in-depth explorations of real-world challenges and creative solutions in iOS development. The interactive booth sessions and specialized workshops added great value, enhancing learning and networking opportunities. To anyone reading this, I definately recommend attending the next year Try Swift!
アバター
はじめに こんにちは。 KINTOテクノロジーズ モバイルアプリ開発グループの中口です。 KINTOかんたん申し込みアプリ の開発をしたり、 iOSチームの勉強会やイベントの企画を行ったりしています。 2024年3月22日-24日で開催された try! Swift Tokyo 2024 に、iOSチームから8名が参加しました。 後日、勉強会の一環で振り返りLT大会を開催したのでその様子をまとめます。 8名の中から5名はLTによる発表を行い、残り3名はKTCテックブログにて記事を公開しています。 テックブログの記事はこちら。 Recap of Try! Swift Tokyo 2024 Trying! Swift Community in 2024 もう一名の記事は後日公開予定!! LT会の様子 普段はiOSチームのメンバーだけで行っているチーム勉強会なのですが、 本日は全社的な勉強会サポーターをしている「学びの道の駅」メンバー (詳しくはこちら) や、 Androidチームのメンバーにもゲスト参加いただきました。 総勢20名を超える参加者となり非常に賑やかな会を開くことができました。 オンライン会場はこちら! みなさん、良い笑顔です😀 オフライン会場はこちら! この日は雨の影響か花粉の影響か在宅勤務のメンバーが多くオフラインは少し寂しかったですが、 こちらもみなさん、良い笑顔です😀 また、iOSチームの勉強会ではSlackに専用スレッドを立てて、みなさんにガヤガヤとコメントで盛り上げていただくのですが、1時間で150以上のコメントがつくほどの大盛況でした! 1人目 杜さん 参加されたセッションに関して幅広く感想を述べていらっしゃいました! また、運営スタッフさん、同時通訳の方への感謝もしっかり述べられていたのが印象的でした。 杜さんは、業務で使っているSwiftUIやTCAに関して普段から深い部分まで理解して使われている印象があるのですが今回のtry! Swiftでも、基礎を深掘りするようなセッションが多く、知見を深められたのではないかと思います。 杜さんの発表の様子です! 2人目 日野森さん( ヒロヤ@お腹すいた ) スタッフとして、3日間携わっておりその時の裏話をたくさん話していただきました! こちら でも記事が上がっているのでご覧ください!! 実はあのセッティングも日野森さんが!!というものがたくさんあったみたいです。 3日間でスタッフとして働く日野森さんを何度も見ましたがとても忙しそうでした。。。 ただ、2日目のクロージングの時にオーガナイザー、スピーカー、スタッフ全員が壇上に集合するシーンはとても感動的で、その中にいる日野森さんはとても輝いていました。 日野森さんの発表の様子です! 3人目 中口 私のLTになります。 今年はvisionOSのキャッチアップを頑張りたいな、と思っているのでLTでも SwiftでvisionOSのアプリをつくろう(1日目) Apple Vision Proならでは! 空間アプリ開発の始め方(3日目) こちらをピックアップして発表しました。 まだ業務でもプライベートでもvisionOSの開発はやったことはないのですが(もちろん実機も持っていない)、 ものすごくvisionOS欲が高まりました! 私の発表の様子です! 4人目 Ryommさん Zennで既に参加レポートを公開されており、そちらについて発表いただきました。(03/23に公開👀、、、早すぎる!!) try! Swift Tokyo 2024に参加しました! https://zenn.dev/ryomm/articles/e1683c1769e259 セッションを全体的に振り返りつつ、スポンサーブースやアフターパーティーでの様子も振り返っていただきました。 驚くべきコミュ力で、スピーカー含め多くの方と情報交換をしたとか! 隣の人と話し始めるコツは度胸と「ちわーっ」だそうです!! こういう場所はみんな誰かと話したがっているはずなので、どんどん話しかけてみるべきですね!見習いたい😭 Ryommさんの発表の様子です! 5人目 goseoさん 良いアプリケーションをデザインするための感覚の持ち方(1日目)の感想を発表いただきました! 実際にセッション内で紹介されていたソースコードを試してみたり、それをSwiftUIで実装した場合の感想などを紹介されておりました。 アニメーションに関して、SwiftUIではまだまだ癖があることを教えていただき勉強になりました! goseoさんの発表の様子です(後日別日で実施しました)! 終わりに 5年ぶりに開催されたtry! Swiftでしたが私自身は初参加でした。また、普段はカンファレンス参加はオンラインばかりでオフラインは初めてでした。とても勉強になり、貴重な体験ができました。次回以降はスポンサーやスタッフなどで参加して、もっと深く関わってみたいなと思いました。 今回は、try! Swiftに参加した後にアウトプット(LT大会やブログの執筆)に繋げられたことがチーム全体としてとても良い取り組みだと思いました。 次回以降のtry! SwiftやiOSDCなど大型のカンファレンスでは引き続き同様の活動を続けていければと思います!
アバター
KINTOテクノロジーズで my route(iOS) を開発しているRyommです。 Snapshot Test のリファレンス画像を任意のディレクトリに作成する方法の解説です。 結論 verifySnapshot(of:as:named:record:snapshotDirectory:timeout:file:testName:line:) メソッドを使えばディレクトリを指定できます。 背景 先日 Snapshot Test を導入した記事を書きましたが、しばらく運用してテストファイルが非常に多くなり、目的のテストファイルを探すのがとても大変な状況になりました。 ![大量のSnapshotTestファイル](/assets/blog/authors/ryomm/2024-04-26/01-yabatanien.png =150x) 大量の Snapshot Test ファイル そこで Snapshot Test ファイルを適当なサブディレクトリで分けることにしましたが、使用している Snapshot Test のライブラリ pointfreeco/swift-snapshot-testing の assertSnapshots(of:as:record:timeout:file:testName:line:) メソッドではリファレンス画像の作成場所を指定することはできません。 既存の Snapshot Test 関連のディレクトリ構造は以下のようになっています。 App/ └── AppTests/ └── Snapshot/ ├── TestVC1.swift ├── TestVC2.swift │ └── __Snapshots__/ ├── TestVC1/ │ └── Refarence.png └── TestVC2/ └── Refarence.png サブディレクトリにテストファイルを移動したとき、上記のメソッドでは以下のようにサブディレクトリ内に __Snapshots__ ディレクトリが作成され、さらにその中にテストファイルと同じ名前のディレクトリとリファレンス画像が作成される形になってしまいます。 App/ └── AppTests/ └── Snapshot/ ├── TestVC1/ │ ├── TestVC1.swift │ └── __Snapshots__/ │ └── Refarence.png ← ここに作られる😕 │ └── TestVC2/ ├── TestVC2.swift └── __Snapshots__/ └── Refarence.png ← ここに作られる😕 すでに存在しているCIの仕組みとして App/AppTests/Snapshot/__Snapshots__/ ディレクトリ以下を丸ごとS3に反映させているため、リファレンス画像の置き場所は変えたくありません。 目標とするディレクトリ構成は以下の形です。 App/ └── AppTests/ └── Snapshot/ ├── TestVC1/ │ └── TestVC1.swift ├── TestVC2/ │ └── TestVC2.swift │ └── __Snapshots__/ ← リファレンス画像はここに入れたい😣 ├── TestVC1/ │ └── Refarence.png └── TestVC2/ └── Refarence.png リファレンス画像のディレクトリを指定して Snapshot Test を行う verifySnapshot(of:as:named:record:snapshotDirectory:timeout:file:testName:line:) メソッドを利用すると、ディレクトリを指定することができます。 Snapshot Test に用意されている3つのメソッドは、それぞれ以下の関係になっています。 public func assertSnapshots<Value, Format>( matching value: @autoclosure () throws -> Value, as strategies: [String: Snapshotting<Value, Format>], record recording: Bool = false, timeout: TimeInterval = 5, file: StaticString = #file, testName: String = #function, line: UInt = #line ) { ... } ↓ as strategies に渡した比較する形式に対してforEachで実行する public func assertSnapshot<Value, Format>( matching value: @autoclosure () throws -> Value, as snapshotting: Snapshotting<Value, Format>, named name: String? = nil, record recording: Bool = false, timeout: TimeInterval = 5, file: StaticString = #file, testName: String = #function, line: UInt = #line ) { ... } ↓ を実行して返ってきた値を元にテストする verifySnapshot(of:as:named:record:snapshotDirectory:timeout:file:testName:line:) 実際のコードは こちら で確認することができます。 つまり、内部的に同じことをしていれば直接 verifySnapshot(of:as:named:record:snapshotDirectory:timeout:file:testName:line:) を使っても問題ないということです! ドン! extension XCTestCase { var precision: Float { 0.985 } func testSnapshot(vc: UIViewController, record: Bool = false, file: StaticString, function: String, line: UInt) { assert(UIDevice.current.name == "iPhone 15", "Please run the test by iPhone 15") SnapshotConfig.allCases.forEach { let failure = verifySnapshot( matching: vc, as: .image(on: $0.viewImageConfig, precision: precision), record: record, snapshotDirectory: "任意のパス", file: file, testName: function + $0.rawValue, line: line) guard let message = failure else { return } XCTFail(message, file: file, line: line) } } } my route では元々 strategies に一つの値しか渡していなかったため、 strategies でループさせる処理は端折りました。 さて、ディレクトリを指定することはできたものの、既存の Snapshot Test に準じて、テストファイル名に応じたディレクトリを作成して、その内部にリファレンス画像が作られるようにしたいです。 verifySnapshot(of:as:named:record:snapshotDirectory:timeout:file:testName:line:) に渡すパスは絶対パスにする必要があり、チームで開発しているとそれぞれ環境が異なるため、環境に合わせてパスを生成する処理が必要となります。 非常に愚直でかわいいコードになってしまいましたが、以下のように実装してみました。 extension XCTestCase { var precision: Float { 0.985 } private func getDirectoryPath(from file: StaticString) -> String { let fileUrl = URL(fileURLWithPath: "\(file)", isDirectory: false) let fileName = fileUrl.deletingPathExtension().lastPathComponent var separatedPath = fileUrl.pathComponents.dropFirst() // ここで [String]? 型になる // Snapshotフォルダ以降のパスを削除 let targetIndex = separatedPath.firstIndex(where: { $0 == "Snapshot"})! separatedPath.removeSubrange(targetIndex+1...separatedPath.count) let snapshotPath = separatedPath.joined(separator: "/") // verifySnapshotに渡すときはStringにするので、URL型に戻さずString型で書いちゃう return "/\(snapshotPath)/__Snapshots__/\(fileName)" } func testSnapshot(vc: UIViewController, record: Bool = false, file: StaticString, function: String, line: UInt) { assert(UIDevice.current.name == "iPhone 15", "Please run the test by iPhone 15") SnapshotConfig.allCases.forEach { let failure = verifySnapshot( matching: vc, as: .image(on: $0.viewImageConfig, precision: precision), record: record, snapshotDirectory: getDirectoryPath(from: file), file: file, testName: function + $0.rawValue, line: line) guard let message = failure else { return } XCTFail(message, file: file, line: line) } } } これでリファレンス画像は従来の場所のまま、Snapshot Test をサブディレクトリに分けることができるようになりました。 スナップショットテストを修正したいのに、中々ファイルが見つからない!という不便さを解消することができました。 まだまだ改善の余地はあると思うので、より快適な開発ライフを送れるようにしていきたいです♪
アバター
Spring BatchとDBUnitを使ったテストで起きた問題 自己紹介 こんにちは。プラットフォーム開発部/共通サービス開発グループ[^1][^2][^3][^4][^5][^6]/決済プラットフォームチームの竹花です。 今回は、Spring Batch + DBUnitを使ったテストで遭遇した問題について書きたいと思います。 環境 ライブラリ等 バージョン Java 17 MySQL 8.0.23 Spring Boot 3.1.5 Spring Boot Batch 3.1.5 JUnit 5.10.0 Spring Test DBUnit 1.3.0 遭遇した問題 Spring Boot3 + Spring Batchのテストにおいて、DBUnitを使っている。 BatchはChunkモデルで、ItemReaderでDB検索、ItemWriterでDB更新を行っている。 上記の前提で、Chunkサイズ以上のデータ件数でテスト実行すると、テストが終わらない... 確認したこと、試したこと 現象確認 コード new StepBuilder("step", jobRepository) .<InputDto, OutputDto>chunk( CHUNK_SIZE, transactionManager) .reader(reader) .processor(processor) .writer(writer) .build(); 上記のようなStepを含むバッチを以下のようにテストしていました。 @SpringBatchTest @SpringBootTest @TestPropertySource( properties = { "spring.batch.job.names: hoge-batch", "targetDate: 2023-01-01", }) @Transactional(isolation = Isolation.SERIALIZABLE) @TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class, TransactionDbUnitTestExecutionListener.class }) @DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class) class HogeBatchJobTest { @Autowired private JobLauncherTestUtils jobLauncherTestUtils; @BeforeEach void setUp() { } @Test @DatabaseSetup("classpath:dbunit/test_data_import.xlsx") @ExpectedDatabase( value = "classpath:dbunit/data_expected.xlsx", assertionMode = DatabaseAssertionMode.NON_STRICT_UNORDERED) void launchJob() throws Exception { val jobExecution = jobLauncherTestUtils.launchJob(); assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); } } テストデータをchunkサイズより少なく設定すると問題なくテストをパスしますが、 テストデータをchunkサイズ以上に設定すると途中でフリーズしてしまい、テストが終わらなくなりました。 (Chunkサイズ1でデータ件数1でも発生) DBのコネクションを疑ってみた Spring Batchは1つのchunkで1トランザクションとなります。 これを並列で処理するのであれば、同時実行数以上のDBコネクションが必要ではと考え、poolサイズを変更して確かめてみました。 spring: datasource: hikari: maximum-pool-size: 10 → 100に変更など しかし、変更しても問題は解消されませんでした... デバッグ開始 debugログを設定して、動作させてみました。 org.springframework.batch.core.step.item.ChunkOrientedTasklet の88行目のログ出力で止まっているようなので、ブレークポイントを貼って実行確認します。 たどり着いたのが、 org.springframework.batch.core.step.tasklet.TaskletStep の408行目。 どうやらセマフォがロックできない(=ロックの解放待ち)となっており、こちらで止まっているようでした。 Spring Batchの深淵へ 引き続き、ステップ実行で処理の流れを追跡しました。 関連する箇所について、ざっくり以下の流れとなっていました。 TaskletStep の doExecute が実行される セマフォを作成 TransactionSynchronization の実装である ChunkTransactionCallback にセマフォを渡し、トランザクション実行と紐付けて RepeatTemplate に設定 chunk分を対象にステップ処理が開始される TaskletStep の doInTransaction でセマフォのロックが行われる ステップの主処理実行 TransactionSynchronizationUtils`でcommitが実行される AbstractPlatformTransactionManager の triggerAfterCompletion メソッドが呼ばれ、処理内の invokeAfterCompletion`が実行される。 invokeAfterCompletion で ChunkTransactionCallback の afterCompletion`メソッドで、セマフォの解放が行われる。 データがまだ残っているなら、 4 に戻る 今回のテスト実行においては、 9 のセマフォの解放が行われないまま、再度 4 を経由して、 5 でフリーズした状態となっていました。 なぜ、セマフォが解放されないのか... 上記の確認の中の セマフォの解放 において、該当コードに以下の判定がありました。 status.isNewSynchronization() が true にならず、 invokeAfterCompletion が実行されませんでした。 org.springframework.transaction.support.DefaultTransactionStatus#isNewSynchronization は以下のようになっています。 /** * Return if a new transaction synchronization has been opened * for this transaction. */ public boolean isNewSynchronization() { return this.newSynchronization; } このトランザクションのために新しいトランザクション同期が開かれたかどうかを返す。 考察 なぜ isNewSynchronization が true にならないのかですが、そこを追え切れていないのが現状です。 ですが、いくつか試行錯誤してみた中のログにそのヒントがある気がしました。 テストクラスに@Transactionalをつけない場合 2024-03-27T08:57:14.527+0000 [Test worker] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.batch.core.repository.support.SimpleJobRepository.update] hoge-batch 19 2024-03-27T08:57:14.527+0000 [Test worker] DEBUG o.s.orm.jpa.JpaTransactionManager - Initiating transaction commit hoge-batch 19 2024-03-27T08:57:14.527+0000 [Test worker] DEBUG o.s.orm.jpa.JpaTransactionManager - Committing JPA transaction on EntityManager [SessionImpl(1075727694<open>)] hoge-batch 19 2024-03-27T08:57:14.534+0000 [Test worker] DEBUG o.s.orm.jpa.JpaTransactionManager - Closing JPA EntityManager [SessionImpl(1075727694<open>)] after transaction hoge-batch 19 2024-03-27T08:57:14.536+0000 [Test worker] DEBUG o.s.b.repeat.support.RepeatTemplate - Repeat operation about to start at count=2 hoge-batch 19 テストクラスに@Transactionalをつけた場合 2024-03-27T09:04:04.600+0000 [Test worker] TRACE o.s.t.i.TransactionInterceptor - Completing transaction for [org.springframework.batch.core.repository.support.SimpleJobRepository.update] hoge-batch 20 2024-03-27T09:04:04.601+0000 [Test worker] DEBUG o.s.b.repeat.support.RepeatTemplate - Repeat operation about to start at count=2 hoge-batch 20 @Transactionalをつけた場合に、 JpaTransactionManager の「Initiating transaction commit...」が出力されていません。 テストクラスは TransactionalTestExecutionListener を使っており、 @Transactional で同一トランザクションで実行されています。 DBUnitで登録したテストデータを、テスト対象処理で参照可能にし、テスト後に破棄(rollback)するためです。 しかし、これによって同一Stepの繰り返し実行時にも既存トランザクションが使いまわされている(=新規のトランザクションが開始されていない)ことで、 isNewSynchronization が true にならないのではないかと結論づけました。 回避方法 TransactionalTestExecutionListener を使わないようにする 力技ですが、 TransactionalTestExecutionListener を使わず自前でテスト後のクリーンアップをすることでフリーズを回避できました。 class HogeTestExecutionListenerChain extends TestExecutionListenerChain { private static final Class<?>[] CHAIN = { HogeTransactionalTestExecutionListener.class, DbUnitTestExecutionListener.class }; @Override protected Class<?>[] getChain() { return CHAIN; } } class HogeTransactionalTestExecutionListener implements TestExecutionListener { private static final String CREATE_BACKUP_TABLE_SQL = "CREATE TEMPORARY TABLE backup_%s AS SELECT * FROM %s"; private static final String TRUNCATE_TABLE_SQL = "TRUNCATE TABLE %s"; private static final String BACKUP_INSERT_SQL = "INSERT INTO %s SELECT * FROM backup_%s"; private static final List<String> TARGET_TABLE_NAMES = List.of( "hoge", "fuga", "dadada"); /** * テスト用作業テーブルを作成する * * @param testContext * @throws Exception */ @Override public void beforeTestMethod(TestContext testContext) throws Exception { val dataSource = (DataSource) testContext.getApplicationContext().getBean("dataSource"); val jdbcTemp = new JdbcTemplate(dataSource); // テスト前に既存データを一時テーブルにbackup TARGET_TABLE_NAMES.forEach( tableName -> jdbcTemp.execute(String.format(CREATE_BACKUP_TABLE_SQL, tableName, tableName))); // テーブル初期化 TARGET_TABLE_NAMES.forEach( tableName -> jdbcTemp.execute(String.format(TRUNCATE_TABLE_SQL, tableName))); } /** * テスト用作業テーブルを削除する * * @param testContext * @throws Exception */ @Override public void afterTestMethod(TestContext testContext) throws Exception { val dataSource = (DataSource) testContext.getApplicationContext().getBean("dataSource"); val jdbcTemp = new JdbcTemplate(dataSource); // テーブルを元に戻す TARGET_TABLE_NAMES.forEach( tableName -> jdbcTemp.execute(String.format(TRUNCATE_TABLE_SQL, tableName, tableName))); TARGET_TABLE_NAMES.forEach( tableName -> jdbcTemp.execute(String.format(BACKUP_INSERT_SQL, tableName, tableName))); } } TransactionDbUnitTestExecutionListenerを外し、TransactionalTestExecutionListenerを使わないようにします。 (エクセルのテストデータ読み込みは利用したいため、DbUnitTestExecutionListenerは使用します) カスタムのTestExecutionListenerを作成し、 事前処理にて対象テーブルのデータを一時テーブルに移し、テスト後に元に戻すようにしています。 beforeTestMethod はテストメソッドよりも前に実行され、 afterTestMethod はテストメソッドよりも後に実行されます。 上記により、Springのトランザクション管理そのままでテスト実行できるようになりました。 所感 検索してもしっくりくるような情報を見つけられず、暗中模索となった問題でした。 とはいえ、Spring Bootのソースを深掘りして見ていくことで、様々な発見もあり、学びのあるコードリーディングであったと思います。 (理解は追いついていませんが...) そもそもSpringやテストライブラリの使い方を間違えているのではないか等、 ライブラリ作成者の前提に基づいて妥当な実装にできているか、他に適切なクラスなどがあるのではないかなど疑問をもって、 引き続き学ばなければならないことがあるなと感じるとともに、 今後も「どうなってるんだろう?」という好奇心を持って、探究と改善に取り組んでいきたいと思いました。 本記事をお読みいただきありがとうございました。 同様の問題に悩む方の参考になれば幸いです。 [^1]: 共通サービス開発グループメンバーによる投稿 1 [ グローバル展開も視野に入れた決済プラットフォームにドメイン駆動設計(DDD)を取り入れた ] [^2]: 共通サービス開発グループメンバーによる投稿 2 [ 入社 1 年未満メンバーだけのチームによる新システム開発をリモートモブプログラミングで成功させた話 ] [^3]: 共通サービス開発グループメンバーによる投稿 3 [ JIRA と GitHub Actions を活用した複数環境へのデプロイトレーサビリティ向上の取り組み ] [^4]: 共通サービス開発グループメンバーによる投稿 4 [ VSCode Dev Container を使った開発環境構築 ] [^5]: 共通サービス開発グループメンバーによる投稿 5 [ Spring Bootを2系から3系へバージョンアップしました。 ] [^6]: 共通サービス開発グループメンバーによる投稿 6 [ MinIOを用いたS3ローカル開発環境の構築ガイド ]
アバター
Hello. I am @p2sk from the DBRE team. In the DBRE (Database Reliability Engineering) team, our cross-functional efforts are dedicated to addressing challenges such as resolving database-related issues and developing platforms that effectively balance governance with agility within our organization. DBRE is a relatively new concept, so very few companies have dedicated organizations to address it. Even among those that do, there is often a focus on different aspects and varied approaches. This makes DBRE an exceptionally captivating field, constantly evolving and developing. For more information on the background of the DBRE team and its role at KINTO Technologies, please see our Tech Blog article, The need for DBRE in KTC . Having been unable to identify the root cause of a timeout error resulting from lock contention (blocking) on Aurora MySQL, this article provides an example of how we developed a mechanism to consistently collect the necessary information to follow up on causes. In addition, this concept can be applied not only to RDS for MySQL but also to MySQL PaaS for cloud services other than AWS, as well as MySQL in its standalone form, so I hope you find this article useful. Background: Timeout occurred due to blocking A product developer contacted us to investigate a query timeout issue that occurred in an application. The error code was SQL Error: 1205 , which suggests a timeout due to exceeding the waiting time for lock acquisition. We use Performance Insights for Aurora MySQL monitoring. Upon reviewing the DB load during the relevant time period, there was indeed an increase in the " synch/cond/innodb/row_lock_wait_cond " wait event, which occurs when waiting to acquire row locks. Performance Insights Dashboard: Lock waits (depicted in orange) are increasing Performance Insights has a tab called " Top SQL " that displays SQL queries that were executed in descending order of their contribution to DB load at any given time. When I checked this, the UPDATE SQL was displayed as shown in the figure below, but only the SQL that timed out, the blocked SQL was being displayed. Top SQL tab: The update statement displayed is the one on the blocked side "Top SQL" is very useful to identify SQL queries, so for example, it has a high contribution rate during periods of high CPU load. On the other hand, in some cases, such as this one, it is not useful to identify the root cause of blocking. This is because the root cause of SQL (blocker) that is causing the blocking may not by itself contribute to the database load. For example, suppose the following SQL is executed in a session: -- Query A start transaction; update sample_table set c1 = 1 where pk_column = 1; This query is a single row update query with a primary key, so it completes very quickly. However, if the transaction is left open and the following SQL is executed in another session, it will be waiting for lock acquisition and blocking will occur. -- Query B update sample_table set c2 = 2 Query B continues to be blocked, so it will appear in "Top SQL" due to longer wait times. Query A, conversely, completes execution instantly and does not appear in "Top SQL," nor is it recorded in the MySQL slow query log. This example is extreme, but it illustrates a case in which it is difficult to identify blockers using Performance Insights. In contrast, there are cases where Performance Insights can identify blockers. For example, the execution of numerous identical UPDATE SQL queries can lead to a "Blocking Query = Blocked Query" scenario. In such case, Performance Insights is sufficient. However, the causes of blocking are diverse, and current Performance Insights has its limitations. Performance Insights was also unable to identify the blocker in this incident. We looked through various logs to determine the cause. Various logs were reviewed, including Audit Log, Error Log, General Log, and Slow Query Log, but the cause could not be determined. Through this investigation, we found that, currently, there is insufficient information to identify the cause of blocking. However, even if the same event occurs in the future, the situation in which we have no choice but to answer "Because of the lack of information, the cause is unknown," needs to be improved. Therefore, we decided to conduct a "solution investigation" to identify the root cause of the blocking. Solution Investigation We investigated the following to determine potential solutions for this issue: Amazon DevOps Guru for RDS SaaS monitoring DB-related OSS and DB monitoring tools Each of these is described below. Amazon DevOps Guru for RDS Amazon DevOps Guru is a machine learning-powered service to analyze metrics and logs from monitored AWS resources, automatically detects performance and operational issues, and provides recommendations for resolving them. The DevOps Guru for RDS is a feature within DevOps Guru specifically dedicated to detecting DB-related issues. The difference from Performance Insights is that DevOps Guru for RDS automatically analyzes issues and suggests solutions. It conveys the philosophy of AWS to realize a world of "managed solutions to issues in the event of an incident." When the actual blocking occurred, the following recommendations were displayed: DevOps Guru for RDS Recommendations: Suggested wait events and SQL to investigate The SQL displayed was the SQL on the blocked side, and it seemed difficult to identify the blocker. Currently, it seems that it only presents link to document that describes how to investigate when a "synch/cond/innodb/row_lock_wait" wait event is contributing to the DB load. Therefore, currently, it is necessary for humans to make final judgments on the proposed causes and recommendations. However, I feel that in the future a more managed incident response experience will be provided. SaaS monitoring A solution that can investigate the cause of database blocking at the SQL level is Datadog Database Monitoring feature . However, it currently supports only PostgreSQL and SQL Server, not MySQL. Similarly, in tools like New Relic and Mackerel , it appears that the feature to conduct post-blocking investigations is not available. DB-related OSS and DB monitoring tools We also investigated the following other DB-related OSS and DB monitoring tools, but no solutions seemed to be offered. Percona Toolkit Percona Monitoring and Management MySQL Enterprise Monitor On the other hand, the SQL Diagnostic Manager for MySQL was the only tool capable of addressing the MySQL blocking investigation. Despite being a DB monitoring tool for MySQL, we opted not to test or adopt it due to its extensive functionalities exceeding our needs, and the price being a limiting factor. Based on this investigation, we found that there were almost no existing solutions, so we decided to create our own mechanism. Therefore, we first organized the "manual investigation procedure for blocking causes." Since version 2 of Aurora MySQL (MySQL 5.7) is scheduled to reach EOL on October 31 of this year, the target is Aurora 3 (MySQL 8.0). Also, the target storage engine is InnoDB. Manual investigation procedure for blocking causes To check the blocking information in MySQL, you need to refer to the following two types of tables. Note that performance_schema must be enabled by setting the MySQL parameter performance_schema to 1. performance_schema.metadata_locks Contains acquired metadata lock information Check for blocked queries with lock_status = 'pending' records performance_schema.data_lock_waits Contains blocking information at the storage engine level (e.g., rows) For example, if you select performance_schema.data_lock_waits in a situation where metadata-caused blocking occurs, you will not get any record. Therefore, the information stored in the two types of tables will be used together to conduct the investigation. It is useful to use a View that combines these tables with other tables for easier analysis. The following is an introduction. Step 1: Make use of sys.schema_table_lock_waits sys.schema_table_lock_waits is a SQL Wrapper View using the following three tables: performance_schema.metadata_locks performance_schema.threads performance_schema.events_statements_current Selecting View while a Wait is occurring for acquiring metadata lock on the resource, will return the relevant records. For example, the following situation. -- Session 1: Acquire and keep metadata locks on tables with lock tables lock tables sample_table write; -- Session 2: Waiting to acquire incompatible shared metadata locks select * from sample_table; In this situation, select sys.schema_table_lock_waits to get the following recordset. The results of this View do not directly identify the blocker in SQL. The blocked query can be identified in the waiting_query column, but there is no blocking_query column, so I will use blocking_thread_id or blocking_pid to identify it. How to identify blockers: SQL-based method When identifying blockers on an SQL basis, use the thread ID of the blocker. The following query using performance_schema.events_statements_current will retrieve the last SQL text executed by the relevant thread. SELECT THREAD_ID, SQL_TEXT FROM performance_schema.events_statements_current WHERE THREAD_ID = 55100\G The result, for example, should look like this. I found out that it was performing lock tables on sample_table, and I was able to identify the blocker. This method has its drawbacks. If the blocker executes another additional query after acquiring locks, the SQL will be retrieved and the blocker cannot be identified. For example, the following situation. -- Session 1: Acquire and keep metadata locks on tables with lock tables lock tables sample_table write; -- Session 1: Run another query after lock tables Select 1; If you execute a similar query in this state, you will get the following results. Alternatively, performance_schema.events_statements_history can be used to retrieve the last N SQL texts executed by the relevant thread. SELECT THREAD_ID, SQL_TEXT FROM performance_schema.events_statements_history WHERE THREAD_ID = 55100 ORDER BY EVENT_ID\G; The result should look like this: The blocker could also be identified because the history could be retrieved. The parameter performance_schema_events_statements_history_size can be used to change how many SQL history entries are kept per thread (set to 10 during verification). The larger the size, the more likely it is to identify blockers, but this also means using more memory, and there's a limit to how large the size can be, so finding a balance is important. Whether history retrieval is enabled can be checked by selecting performance_schema.setup_consumers . It seems that the performance_schema.events_statements_history retrieval is enabled by default for Aurora MySQL. How to identify blockers: Log-based method When identifying blockers on an log basis, use the General Log and Audit Log. For example, if General Log retrieval is enabled on Aurora MySQL, all SQL history executed by the process can be retrieved using the following query in CloudWatch Logs Insights. fields @timestamp, @message | parse @message /(?<timestamp>[^\s]+)\s+(?<process_id>\d+)\s+(?<type>[^\s]+)\s+(?<query>.+)/ | filter process_id = 208450 | sort @timestamp asc Executing this query results in the following: CloudWatch Logs Insights Query Execution Result: SQL blocked by red box is a blocker We basically enable the General Log. There is a concern that SQL-based blockers will be removed from the history table and cannot be identified. Therefore, we decided to use a log-based identification method this time. Considerations for identifying blockers Identifying blockers ultimately requires human visual confirmation and judgment. The reason is that the lock acquisition information is directly related to the thread, and the SQL executed by the thread changes from time to time. Therefore, in a situation like the example, "the blocker finished executing the query, but the lock has been acquired," it is necessary to infer the root cause SQL from the history of SQL executed by the blocker process. However, just knowing the blocker thread ID or process ID can be expected to significantly improve the rate of identifying the root cause. Step 2: Make use of sys.innodb_lock_waits This is a SQL Wrapper View using the following three tables: performance_schema.data_lock_waits information_schema.INNODB_TRX performance_schema.data_locks If you select this View while a Wait is occurring for lock acquisition implemented by the storage engine (InnoDB), the record will be returned. For example, the following situation. -- Session 1: Keep the transaction that updated the record open start transaction; update sample_table set c2 = 10 where c1 = 1; -- Session 2: Try to update the same record delete from sample_table where c1 = 1; In this situation, select sys.innodb_lock_waits to get the following recordset. From this result, as with sys.schema_table_lock_waits , it is not possible to directly identify the blocker. Therefore, blocking_pid is used to identify the blocker using the log-based method described above. fields @timestamp, @message | parse @message /(?<timestamp>[^\s]+)\s+(?<process_id>\d+)\s+(?<type>[^\s]+)\s+(?<query>.+)/ | filter process_id = 208450 | sort @timestamp asc Executing this query results in the following: CloudWatch Logs Insights Query Execution Result: SQL blocked by red box is a blocker Summary of the above As a first step for post-investigation of root cause of Aurora MySQL blocking, I have outlined how to investigate the root cause when blocking occurs. The investigation procedure is as follows: Identify blocker process ID using two types of Views: sys.schema_table_lock_waits and sys.innodb_lock_waits Use CloudWatch Logs Insights to retrieve the SQL execution history of the process ID from the General Log Identify (estimate) the root cause SQL while visually checking Step 1 must be in a "blocking condition" to get results. Therefore, periodically collecting and storing two types of View equivalent information at N-second intervals enables post-investigation. In addition, it is necessary to select N such that the relationship of N seconds < Application timeout period is valid. Additional information about blocking Here are two additional points about blocking. Firstly, I will outline the difference between deadlocks and blocking, followed by an explanation of the blocking tree. Differences from deadlocks It is rare but blocking is sometimes confused with deadlock, so let's summarize the differences. A deadlock is also a form of blocking, but it is determined that the event will not be resolved unless one of the processes is forced to roll back. Therefore, when InnoDB detects a deadlock, automatic resolution occurs relatively quickly. On the other hand, in the case of normal blocking, there is no intervention by InnoDB because it is resolved when the blocker query is completed. A comparison between the two is summarized in the table below. Blocking Deadlock Automatic resolution by InnoDB Not supported Supported Query completion Both the blocker and the blocked side will eventually complete execution unless terminated midway due to a KILL or timeout error. One transaction is forced to terminate by InnoDB. General solution It can be resolved spontaneously by the completion of the blocker query or resolved with a timeout error after the application-set query timeout period elapses. After InnoDB detects a deadlock, it can be resolved by forcing one of the transactions to roll back. Blocking tree It is not an official term of MySQL, but I will describe the blocking tree. This refers to a situation where "a query that is a blocker is also blocked by another blocker." For example, the following situation. -- Session 1 begin; update sample_table set c1 = 2 where pk_column = 1; -- Session 2 begin; update other_table set c1 = 3 where pk_column = 1; update sample_table set c1 = 4 where pk_column = 1; -- Session 3 update other_table set c1 = 5 where pk_column = 1; In this situation, when you select sys.innodb_lock_waits , you will get two records: "Session 1 is blocking Session 2" and "Session 2 is blocking Session 3." In this case, the blocker from the perspective of Session 3 is Session 2, but the root cause of the problem (Root Blocker) is Session 1. Thus, blocking occurrences can sometimes be tree-like, making log-based investigations in such cases even more difficult. The importance of collecting information on blocking beforehand lies in the difficulty of investigating such blocking-related causes. In the following, I will introduce the design and implementation of the blocking information collection mechanism. Architectural Design We have multiple regions and multiple Aurora MySQL clusters running within a region. Therefore, the configuration needed to minimize the deployment and operational load across regions and clusters. Other requirements include: Functional requirements Can execute any SQL periodically against Aurora MySQL Can collect Aurora MySQL information from any region Can manage the DB to be executed Can store Query execution results in external storage Can stored SQL data be queried Privileges can be managed to restrict access to the collected data in the source database, allowing only those authorized to view it. Non-functional requirements Minimal overhead on the DB to be collected Data freshness during analysis can be limited to a time lag of about five minutes Notification will alert us if the system becomes inoperable Response in seconds during SQL-based analysis Collected logs can be aggregated into some kind of storage in a single location Can minimize the financial costs of operations In addition, the tables to be collected were organized as follows. Table to be Collected Even though you have the option to periodically gather specific results from sys.schema_table_lock_waits and sys.innodb_lock_waits , the system load will increase due to the complexity of these Views when compared to directly selecting data from the original tables. Therefore, to meet the non-functional requirement of 'Minimal overhead on the DB to be collected,' we opted to select the following six tables, which serve as the source for the Views. These views were then constructed on the query engine side, enabling the query load to be shifted away from the database side. Original tables of sys.schema_table_lock_waits performance_schema.metadata_locks performance_schema.threads performance_schema.events_statements_current Original tables of sys.innodb_lock_waits performance_schema.data_lock_waits information_schema.INNODB_TRX performance_schema.data_locks The easiest way would be to use MySQL Event , MySQL's task scheduling feature, to execute SELECT queries on these tables every N seconds and store the results in a dedicated table. However, this method is not suitable for the requirements because it generates a high write load to the target DB and requires individual login to the DB to check the results. Therefore, other methods were considered. Architecture Patterns First, an abstract architecture diagram was created as follows: For this architecture diagram, the AWS services to be used at each layer were selected based on the requirements. Collector service selection After evaluating the following services based on our past experiences, we decided to proceed with the design mainly using Lambda. EC2 It is assumed that this workload does not require as much processing power as running EC2 all the time, and is considered excessive from both an administrative and cost perspective The mechanism depends on the deployment to EC2 and execution environment on EC2 ECS on EC2 It is assumed that this workload does not require as much processing power as running EC2 all the time, and is considered excessive from both an administrative and cost perspective Depends on Container Repository such as ECR ECS on Fargate Serverless like Lambda, but depends on Container Repository such as ECR Lambda It is more independent than other compute layer services, and is considered best suited for performing the lightweight processing envisioned at this time Storage / Query Interface service selection Storage / Query Interface was configured with S3 + Athena. The reasons are as follows. Want to run SQL with JOIN CloudWatch Logs was also considered for storage, but rejected due to this requirement Fast response times and transaction processing are not required No advantage of using DB services such as RDS, DynamoDB, or Redshift Buffer service selection We have adopted Amazon Data Firehose as the buffer layer between the collector and storage. We also considered Kafka, SQS, Kinesis Data Streams, etc., but we chose Firehose for the following reasons. Put to Firehose and data will be automatically stored in S3 (no additional coding required) Can reduce the number of files in S3 by buffering them based on time or data size, enabling bulk storage in S3 Automatic compression reduces file size in S3 Dynamic partitioning feature allows dynamic determination of S3 file paths Based on the services selected above, five architecture patterns were created. For simplicity, the figure below illustrates one region. Option 1: Execute Lambda in MySQL Event Aurora MySQL is integrated with Lambda . This pattern is used to periodically invoke Lambda using MySQL Event. The architecture is as follows: ![Option 1: Architecture Diagram of Lambda Execution Pattern in MySQL Event](/assets/blog/authors/m.hirose/2024-03-12-13-16-16.png =600x) Option 2: Save data directly from Aurora into S3 Aurora MySQL is also integrated with S3 and can store data directly in S3. The architecture is very simple, as shown in the figure below. On the other hand, as in option 1 also requires deployment of MySQL Events, it will be necessary to deploy across multiple DB clusters when creating or modifying new Events. It must be handled manually and individually, or a mechanism must be in place to deploy to all target clusters. ![Option 2: Architecture Diagram of Pattern of Saving Files directly from Aurora to S3](/assets/blog/authors/m.hirose/2024-03-12-13-15-50.png =300x) Option 3: Step Functions Pattern A This pattern combines Step Functions and Lambda. By using the Map state, child workflows corresponding to the collector can be executed in parallel for each target cluster. The process of "executing SQL at N-second intervals" is implemented using a combination of Lambda and Wait state. This implementation results in a very large number of state transitions. For AWS Step Functions Standard Workflows, pricing is based on the number of state transitions, while for Express Workflows, there is a maximum execution time of five minutes per execution, but no charge is incurred based on the number of state transitions. Therefore, Express Workflows are implemented where the number of state transitions is large. This AWS Blog was used as a reference. Option 4: Step Functions Pattern B Like option 3, this pattern combines Step Functions and Lambda. The difference from option 3 is that the process "Execute SQLat N-second intervals" is implemented in Lambda, and "Execute SQL -> N-second Sleep" is repeated for 10 minutes. Since Lambda execution is limited to a maximum of 15 minutes, Step Functions is invoked in EventBridge every 10 minutes. Because the number of state transitions is very small, the financial cost of Step Functions can be reduced. On the other hand, since Lambda will continue to run even during Sleep, the Lambda billing amount is expected to be higher than in option 3. ![Option 4: Step Functions Architecture Diagram of Pattern B](/assets/blog/authors/m.hirose/2024-03-12-13-23-40_en.png =600x) Option 5: Sidecar Pattern We primarily use ECS as our container orchestration service, assuming that there is at least one ECS cluster accessible to each Aurora MySQL. Placing a newly implemented Collector as a Sidecar in a task has the advantage of not incurring additional computing resource costs, such as Lambda. However, if it does not fit within the resources of Fargate, it needs to be expanded. ![Option 5: Architecture Diagram of Sidecar Pattern](/assets/blog/authors/m.hirose/2024-03-12-13-47-37.png =600x) Architecture Comparison The results of comparing each option are summarized in the table below. Option 1 Option 2 Option 3 Option 4 Option 5 Developer and operator DBRE DBRE DBRE DBRE Since the container area falls outside our scope, it is necessary to request other teams Financial costs ☀ ☀ ☀ ☁ ☀ Implementation costs ☁ ☀ ☁ ☀ ☀ Development Agility ☀ (DBRE) ☀ (DBRE) ☀ (DBRE) ☀ (DBRE) ☁ (must be coordinated across teams) Deployability ☁ (Event deployment either requires manual intervention or a dedicated mechanism) ☁ (Event deployment either requires manual intervention or a dedicated mechanism) ☀ (IaC can be managed with existing development flow) ☀ (IaC can be managed with existing development flow) ☁ (must be coordinated across teams) Scalability ☀ ☀ ☀ ☀ ☁ (must be coordinated with Fargate team) Specific considerations Permissions must be configured for IAM and DB users to enable the launching of Lambda functions from Aurora No buffering, so writes to S3 occur synchronously and API executions are frequent Implementing Express Workflows requires careful consideration of the at-least-once execution model. The highest financial cost because Lambda runs longer than necessary The number of Sidecar containers can be the same as the number of tasks, resulting in duplicated processing Based on the above comparison, we adopted option 3, which uses Step Functions with both Standard and Express Workflows. The reasons are as follows. It is expected that the types of collected data will expand, and those who can control development and operations within their own team (DBRE) can respond swiftly. The option to use MySQL Event is simple configuration, yet there are numerous considerations, such as modifying cross-sectional IAM permissions and adding permissions for DB users, and the human cost is high whether automated or covered manually. Even though it costs a little more to implement, the additional benefits offered make option 3 the most balanced choice. In the following sections, I will introduce the aspects devised in the process of implementing the chosen option and the final architecture. Implementation Our DBRE team develops in Monorepo and uses Nx as a management tool. Infrastructure management is handled using Terraform, while Lambda implementation is performed in Go. For more information on the DBRE team's development flow using Nx, please see our Tech Blog article " AWS Serverless Architecture with Monorepo Tools - Nx and Terraform! (Japanese)" Final Architecture Diagram Taking into account multi-region support and other considerations, the final architecture is shown in the figure below. The main considerations are: Express Workflows terminate after four minutes because forced termination is treated as an error after five minutes. Since the number of accesses to DynamoDB is small and latency is not a bottleneck, it is aggregated in the Tokyo region. The data synchronization to S3 after putting to Firehose is asynchronous, so latency is not a bottleneck, and S3 is aggregated to the Tokyo region. To reduce the financial cost of frequent access to Secrets Manager, secret retrieval is performed outside the state loop. To prevent each Express Workflow from being executed multiple times, a locking mechanism is implemented using DynamoDB. Note: Since Express Workflows employ an at-least-once execution model. In the following sections, I will introduce the aspects devised during implementation. Create a dedicated DB user for each DB Only the following two permissions are required to execute the target SQL. GRANT SELECT ON performance_schema.* TO ${user_name}; -- required to select information_schema.INNODB_TRX GRANT PROCESS ON *.* TO ${user_name}; We have created a mechanism whereby DB users with only this permission are created for all Aurora MySQL. We have a batch process that connects to all Aurora MySQL on a daily basis and collects various information . This batch process has been modified to create DB users with the required permissions for all DBs. This automatically created a state in which the required DB users existed when a new DB was created. Reduce DB load and data size stored in S3 Records can be retrieved from some of the six target collection tables even if blocking has not occurred. Therefore, if all the cases are selected every N seconds, the load on Aurora will increase unnecessarily, although only slightly, and data will be stored unnecessarily in S3 as well. To prevent this, the implementation ensures that all relevant tables are selected only when blocking is occurring. To minimize the load, SQL for blocking detection was also organized as follows. Metadata blocking detection The metadata blocking occurrence detection query is as follows. select * from `performance_schema`.`metadata_locks` where lock_status = 'PENDING' limit 1 Only when records are obtained by this query, execute the SELECT query on all three following tables and transmit the results to Firehose. performance_schema.metadata_locks performance_schema.threads performance_schema.events_statements_current InnoDB blocking detection The blocking occurrence detection query of InnoDB is as follows. select * from `information_schema`.`INNODB_TRX` where timestampdiff(second, `TRX_WAIT_STARTED`, now()) >= 1 limit 1; Only when records are obtained by this query, execute the SELECT query on all three following tables and transmit the results to Firehose. performance_schema.data_lock_waits information_schema.INNODB_TRX performance_schema.data_locks Parallel processing of queries using goroutine Even if there is a slight deviation in the timing of SELECT execution on each table, if blocking continues, the probability of data inconsistency occurring when joining later is low. However, it is preferable to conduct them at the same time as much as possible. To achieve a state where "data continues to be collected at N-second intervals, it is also necessary to ensure that the Collector's Lambda execution time is as short as possible. Based on above two points, query execution is handled as concurrently as possible using goroutine. Use of session variables to avoid unexpected overloads Although we confirm in advance that the query load to be executed is sufficiently low, sometimes there may be situations where "the execution time is longer than expected" or "information gathering queries are caught in blocking." Therefore, we set max_execution_time and TRANSACTION ISOLATION LEVEL READ UNCOMMITTED at the session level to continue to obtain information as safely as possible. To implement this process in the Go language, we override the function Connect() in the driver.connector interface in the database/SQL/driver package. The implementation image, excluding error handling, is as follows type sessionCustomConnector struct { driver.Connector } func (c *sessionCustomConnector) Connect(ctx context.Context) (driver.Conn, error) { conn, err := c.Connector.Connect(ctx) execer, _ := conn.(driver.ExecerContext) sessionContexts := []string{ "SET SESSION max_execution_time = 1000", "SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED", } for _, sessionContext := range sessionContexts { execer.ExecContext(ctx, sessionContext, nil) } return conn, nil } func main() { cfg, _ := mysql.ParseDSN("dsn_string") defaultConnector, _ := mysql.NewConnector(cfg) db := sql.OpenDB(&sessionCustomConnector{defaultConnector}) rows, _ := db.Query("SELECT * FROM performance_schema.threads") ... } Locking mechanism for Express Workflows Since StepFunctions' Express Workflows employ an at-least-once execution model, the entire workflows can be executed multiple times. In this case, duplicate execution is not a major problem, but achieving exactly-once execution is preferable, so we implemented a simple locking mechanism using DynamoDB, with reference to the AWS Blog . Specifically, Lambda, which runs at the start of Express workflows, PUT data into a DynamoDB table with attribute_not_exists expression . The partition key specifies a unique ID generated by the parent workflow, it can be determined that "PUT succeeds = you are the first executor." If it fails, it determines that another child workflow is already running, skips further processing and exits. Leveraging Amazon Data Firehose Dynamic Partitioning Firehose Dynamic Partitioning feature is used to dynamically determine the S3 file path. The rule for dynamic partitioning (S3 bucket prefixes) was configured as follows, taking into account access control in Athena as described below. !{partitionKeyFromQuery:db_schema_name}/!{partitionKeyFromQuery:table_name}/!{partitionKeyFromQuery:env_name}/!{partitionKeyFromQuery:service_name}/day=!{timestamp:dd}/hour=!{timestamp:HH}/ If you put json data to Firehose Stream with this setting, it will find the attribute that is the partition key from the attribute in json and automatically save it to S3 with the file path according to the rule. For example, suppose the following json data is put into Firehose. { "db_schema_name":"performance_schema", "table_name":"threads", "env_name":"dev", "service_name":"some-service", "other_attr1":"hoge", "other_attr2":"fuga", ... } As a result, the file path to be saved in S3 is as follows: There is no need to specify any file path when putting to Firehose. It automatically determines the file name and saves it based on predefined rules. The file stored by Firehose in S3: Dynamic Partitioning automatically determines file path Since schema names, table names, etc. do not exist in the SELECT results of MySQL tables, we implemented to add them as common columns when generating JSON to be put into Firehose. Design of Athena tables and access rights Here is an example of creating a table in Athena based on the table definition in MySQL. The CREATE statement on the MySQL side for performance_schema.metadata_locks is as follows: CREATE TABLE `metadata_locks` ( `OBJECT_TYPE` varchar(64) NOT NULL, `OBJECT_SCHEMA` varchar(64) DEFAULT NULL, `OBJECT_NAME` varchar(64) DEFAULT NULL, `COLUMN_NAME` varchar(64) DEFAULT NULL, `OBJECT_INSTANCE_BEGIN` bigint unsigned NOT NULL, `LOCK_TYPE` varchar(32) NOT NULL, `LOCK_DURATION` varchar(32) NOT NULL, `LOCK_STATUS` varchar(32) NOT NULL, `SOURCE` varchar(64) DEFAULT NULL, `OWNER_THREAD_ID` bigint unsigned DEFAULT NULL, `OWNER_EVENT_ID` bigint unsigned DEFAULT NULL, PRIMARY KEY (`OBJECT_INSTANCE_BEGIN`), KEY `OBJECT_TYPE` (`OBJECT_TYPE`,`OBJECT_SCHEMA`,`OBJECT_NAME`,`COLUMN_NAME`), KEY `OWNER_THREAD_ID` (`OWNER_THREAD_ID`,`OWNER_EVENT_ID`) ) This is defined for Athena as follows: CREATE EXTERNAL TABLE `metadata_locks` ( `OBJECT_TYPE` string, `OBJECT_SCHEMA` string, `OBJECT_NAME` string, `COLUMN_NAME` string, `OBJECT_INSTANCE_BEGIN` bigint, `LOCK_TYPE` string, `LOCK_DURATION` string, `LOCK_STATUS` string, `SOURCE` string, `OWNER_THREAD_ID` bigint, `OWNER_EVENT_ID` bigint, `db_schema_name` string, `table_name` string, `aurora_cluster_timezone` string, `stats_collected_at_utc` timestamp ) PARTITIONED BY ( env_name string, service_name string, day int, hour int ) ROW FORMAT SERDE 'org.openx.data.jsonserde.JsonSerDe' LOCATION 's3://<bucket_name>/performance_schema/metadata_locks/' TBLPROPERTIES ( "projection.enabled" = "true", "projection.day.type" = "integer", "projection.day.range" = "01,31", "projection.day.digits" = "2", "projection.hour.type" = "integer", "projection.hour.range" = "0,23", "projection.hour.digits" = "2", "projection.env_name.type" = "injected", "projection.service_name.type" = "injected", "storage.location.template" = "s3://<bucket_name>/performance_schema/metadata_locks/${env_name}/${service_name}/day=${day}/hour=${hour}" ); The point is the design of partition keys. This ensures that only those who have access permission to the original DB can access the data. We assign two tags to all AWS resources: service_name, which is unique to each service, and env_name, which is unique to each environment. We use these tags as a means of access control. By including these two tags as part of the file path to be stored in S3, and by writing the resource using policy variables for the IAM Policy that is commonly assigned to each service, you can only SELECT data for the partition corresponding to the file path in S3 for which you have access permission, even if they are the same tables. The image below shows the permissions granted to the IAM Policy, which is commonly granted to each service. { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": [ "arn:aws:s3:::<bukect_name>/*/${aws:PrincipalTag/env_name}/${aws:PrincipalTag/service_name}/*" ] } Also, this time I wanted to make the partitions maintenance-free, so I used partition projection . When using partition projection, the range of possible values for partition keys must be known, but by using injected projection type , the range of values does not need to be communicated to Athena, allowing for maintenance-free dynamic partitioning. Reproduction of Views in Athena Here is how to create six tables needed for post-blocking investigation in Athena and wrap them with Views, just as in MySQL. The View definition was modified based on the MySQL View definition, such as adding a common column and adding partition key comparisons to the JOIN clause. The Athena definition of `sys.innodb_lock_waits<[1} is as follows. CREATE OR REPLACE VIEW innodb_lock_waits AS select DATE_ADD('hour', 9, w.stats_collected_at_utc) as stats_collected_at_jst, w.stats_collected_at_utc as stats_collected_at_utc, w.aurora_cluster_timezone as aurora_cluster_timezone, r.trx_wait_started AS wait_started, date_diff('second', r.trx_wait_started, r.stats_collected_at_utc) AS wait_age_secs, rl.OBJECT_SCHEMA AS locked_table_schema, rl.OBJECT_NAME AS locked_table_name, rl.PARTITION_NAME AS locked_table_partition, rl.SUBPARTITION_NAME AS locked_table_subpartition, rl.INDEX_NAME AS locked_index, rl.LOCK_TYPE AS locked_type, r.trx_id AS waiting_trx_id, r.trx_started AS waiting_trx_started, date_diff('second', r.trx_started, r.stats_collected_at_utc) AS waiting_trx_age_secs, r.trx_rows_locked AS waiting_trx_rows_locked, r.trx_rows_modified AS waiting_trx_rows_modified, r.trx_mysql_thread_id AS waiting_pid, r.trx_query AS waiting_query, rl.ENGINE_LOCK_ID AS waiting_lock_id, rl.LOCK_MODE AS waiting_lock_mode, b.trx_id AS blocking_trx_id, b.trx_mysql_thread_id AS blocking_pid, b.trx_query AS blocking_query, bl.ENGINE_LOCK_ID AS blocking_lock_id, bl.LOCK_MODE AS blocking_lock_mode, b.trx_started AS blocking_trx_started, date_diff('second', b.trx_started, b.stats_collected_at_utc) AS blocking_trx_age_secs, b.trx_rows_locked AS blocking_trx_rows_locked, b.trx_rows_modified AS blocking_trx_rows_modified, concat('KILL QUERY ', cast(b.trx_mysql_thread_id as varchar)) AS sql_kill_blocking_query, concat('KILL ', cast(b.trx_mysql_thread_id as varchar)) AS sql_kill_blocking_connection, w.env_name as env_name, w.service_name as service_name, w.day as day, w.hour as hour from ( ( ( ( data_lock_waits w join INNODB_TRX b on( ( b.trx_id = cast( w.BLOCKING_ENGINE_TRANSACTION_ID as bigint ) ) and w.stats_collected_at_utc = b.stats_collected_at_utc and w.day = b.day and w.hour = b.hour and w.env_name = b.env_name and w.service_name = b.service_name ) ) join INNODB_TRX r on( ( r.trx_id = cast( w.REQUESTING_ENGINE_TRANSACTION_ID as bigint ) ) and w.stats_collected_at_utc = r.stats_collected_at_utc and w.day = r.day and w.hour = r.hour and w.env_name = r.env_name and w.service_name = r.service_name ) ) join data_locks bl on( bl.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID and bl.stats_collected_at_utc = w.stats_collected_at_utc and bl.day = w.day and bl.hour = w.hour and bl.env_name = w.env_name and bl.service_name = w.service_name ) ) join data_locks rl on( rl.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID and rl.stats_collected_at_utc = w.stats_collected_at_utc and rl.day = w.day and rl.hour = w.hour and rl.env_name = w.env_name and rl.service_name = w.service_name ) ) In addition, the Athena definition of sys.schema_table_lock_waits is as follows. CREATE OR REPLACE VIEW schema_table_lock_waits AS select DATE_ADD('hour', 9, g.stats_collected_at_utc) as stats_collected_at_jst, g.stats_collected_at_utc AS stats_collected_at_utc, g.aurora_cluster_timezone as aurora_cluster_timezone, g.OBJECT_SCHEMA AS object_schema, g.OBJECT_NAME AS object_name, pt.THREAD_ID AS waiting_thread_id, pt.PROCESSLIST_ID AS waiting_pid, -- sys.ps_thread_account(p.OWNER_THREAD_ID) AS waiting_account, -- Not supported because it is unnecessary, although it is necessary to include it in select when collecting information in MySQL. p.LOCK_TYPE AS waiting_lock_type, p.LOCK_DURATION AS waiting_lock_duration, pt.PROCESSLIST_INFO AS waiting_query, pt.PROCESSLIST_TIME AS waiting_query_secs, ps.ROWS_AFFECTED AS waiting_query_rows_affected, ps.ROWS_EXAMINED AS waiting_query_rows_examined, gt.THREAD_ID AS blocking_thread_id, gt.PROCESSLIST_ID AS blocking_pid, -- sys.ps_thread_account(g.OWNER_THREAD_ID) AS blocking_account, -- Not supported because it is unnecessary, although it is necessary to include it in select when collecting information in MySQL. g.LOCK_TYPE AS blocking_lock_type, g.LOCK_DURATION AS blocking_lock_duration, concat('KILL QUERY ', cast(gt.PROCESSLIST_ID as varchar)) AS sql_kill_blocking_query, concat('KILL ', cast(gt.PROCESSLIST_ID as varchar)) AS sql_kill_blocking_connection, g.env_name as env_name, g.service_name as service_name, g.day as day, g.hour as hour from ( ( ( ( ( metadata_locks g join metadata_locks p on( ( (g.OBJECT_TYPE = p.OBJECT_TYPE) and (g.OBJECT_SCHEMA = p.OBJECT_SCHEMA) and (g.OBJECT_NAME = p.OBJECT_NAME) and (g.LOCK_STATUS = 'GRANTED') and (p.LOCK_STATUS = 'PENDING') AND (g.stats_collected_at_utc = p.stats_collected_at_utc and g.day = p.day and g.hour = p.hour and g.env_name = p.env_name and g.service_name = p.service_name) ) ) ) join threads gt on(g.OWNER_THREAD_ID = gt.THREAD_ID and g.stats_collected_at_utc = gt.stats_collected_at_utc and g.day = gt.day and g.hour = gt.hour and g.env_name = gt.env_name and g.service_name = gt.service_name) ) join threads pt on(p.OWNER_THREAD_ID = pt.THREAD_ID and p.stats_collected_at_utc = pt.stats_collected_at_utc and p.day = pt.day and p.hour = pt.hour and p.env_name = pt.env_name and p.service_name = pt.service_name) ) left join events_statements_current gs on(g.OWNER_THREAD_ID = gs.THREAD_ID and g.stats_collected_at_utc = gs.stats_collected_at_utc and g.day = gs.day and g.hour = gs.hour and g.env_name = gs.env_name and g.service_name = gs.service_name) ) left join events_statements_current ps on(p.OWNER_THREAD_ID = ps.THREAD_ID and p.stats_collected_at_utc = ps.stats_collected_at_utc and p.day = ps.day and p.hour = ps.hour and p.env_name = ps.env_name and p.service_name = ps.service_name) ) where (g.OBJECT_TYPE = 'TABLE') Results Using the mechanism created, I will actually generate blocking and conduct an investigation in Athena. select * from innodb_lock_waits where stats_collected_at_jst between timestamp '2024-03-01 15:00:00' and timestamp '2024-03-01 16:00:00' and env_name = 'dev' and service_name = 'some-service' and hour between cast(date_format(DATE_ADD('hour', -9, timestamp '2024-03-01 15:00:00'), '%H') as integer) and cast(date_format(DATE_ADD('hour', -9, timestamp '2024-03-01 16:00:00'), '%H') as integer) and day = 1 order by stats_collected_at_jst asc limit 100 If you execute the above query in Athena, specifying the time period during which the blocking occurred, the following results will be returned. Since we do not know the blocker's SQL, we use CloudWatch Logs Insights based on the process ID (blocking_pid column) to check the history of SQL executed by the blocker. fields @timestamp, @message | parse @message /(?<timestamp>[^\s]+)\s+(?<process_id>\d+)\s+(?<type>[^\s]+)\s+(?<query>.+)/ | filter process_id = 215734 | sort @timestamp desc The following results indicate that the blocker SQL is update d1.t1 set c1 = 12345 . The same procedure can now be used to check metadata-related blocking status in schema_table_lock_waits. Future Outlook As for the future outlook, the following is considered: As deployment to the product is scheduled for the future, accumulate knowledge on incidents caused by blocking through actual operations. Investigate and tune bottlenecks to minimize Lambda billed duration. Expand collection targets in performance_schema and information_schema Expand the scope of investigations, including analysis of index usage. Improve the DB layer's problem-solving capabilities through a cycle of expanding the information collected based on feedback from incident response. Visualization with BI services such as Amazon QuickSight Create a world where members unfamiliar with performance_schema can investigate the cause. Summary This article details a case where investigating the cause of timeout errors due to lock contention in Aurora MySQL led to the development of a mechanism for periodically collecting necessary information to follow up on the cause of the blocking. In order to follow up on blocking information in MySQL, we need to periodically collect wait data on two types of locks, metadata locks and InnoDB locks, using the following six tables. Metadata locks performance_schema.metadata_locks performance_schema.threads performance_schema.events_statements_current InnoDB locks performance_schema.data_lock_waits information_schema.INNODB_TRX performance_schema.data_locks Based on our environment, we designed and implemented a multi-region architecture capable of collecting information from multiple DB clusters. Consequently, we were able to create a SQL-based post-investigation with a time lag of up to five minutes after the occurrence of blocking, with results returned in a few seconds. Although these features may eventually be incorporated into SaaS and AWS services, our DBRE team values proactively implementing features on our own if deemed necessary. KINTO Technologies' DBRE team is looking for people to join us! Casual chats are also welcome, so if you are interested, feel free to DM me on X. Don't forget to follow us on our recruitment Twitter too! Appendix : References The following articles provide a clear summary of how to investigate blocking in the MySQL 8.0 series, and use them as a reference. Checking the row lock status of InnoDB [Part 1] Checking the row lock status of InnoDB [Part 2] Isono, Let's Show MySQL Lock Contention Once the blocker is identified, further investigation is required to determine the specific locks causing the blocking. The following article provides a clear and understandable summary of MySQL locks. About MySQL Locks I also referred to the MySQL Reference Manual. The metadata_locks Table The data_locks Table InnoDB INFORMATION_SCHEMA Transaction and Locking Information
アバター
Introduction Nice to meet you, I am Somi, and I work on developing the my route app for Android at KINTO Technologies Corporation. my route is an app that enriches travel experiences by providing various functions such as "Odekake Information" (information on traffic and the outings you want to do), "Search by Map," and "Odekake Memo" (a notepad function). The my route Android team is currently heavily using Jetpack Compose to improve the UI/UX. This UI toolkit improves code readability and lets us develop the UI quickly and flexibly. Also, the declarative UI approach simplifies the development process and makes UI components more reusable. With this information in mind, I will talk about some examples of Jetpack Compose features used in the my route Android app. In this article, I will talk about four features. Functionalities 1. drawRect and drawRoundRect Jetpack Compose uses Canvas to make it possible to draw in a specific range. drawRect and drawRoundRect are functions related to shapes that can be defined inside a Canvas. drawRect draws a rectangle with a specified offset and size, while drawRoundRect has all of the functions of the drawRect, plus the cornerRadius parameter which adjusts the roundness of the corners. my route has a function that reads coupon codes in text format with the device's camera. To accurately recognize codes, parts used to recognize text had to be transparent, and the rest had to be darkened. So, we implemented the UI with drawRect and drawRoundRect. @Composable fun TextScanCameraOverlayCanvas() { val overlayColor = MaterialTheme.colors.onSurfaceHighEmphasis.copy(alpha = 0.7f) ... Canvas( modifier = Modifier.fillMaxSize() ) { with(drawContext.canvas.nativeCanvas) { val checkPoint = saveLayer(null, null) drawRect(color = overlayColor) drawRoundRect( color = Color.Transparent, size = Size(width = layoutWidth.toPx(), height = 79.dp.toPx()), blendMode = BlendMode.Clear, cornerRadius = CornerRadius(7.dp.toPx()), topLeft = Offset(x = screenWidth.toPx(), y = rectHeight.toPx()) ) restoreToCount(checkPoint) } } } The above code is implemented with the following UI. To explain the code, drawRect uses a color specified with overlayColor to darken the whole screen. In addition, we used drawRoundRect to create a transparent rectangle with rounded corners to make it clear that text inside the area would be recognized. 2. KeyboardActions and KeyboardOptions KeyboardActions and KeyboardOptions are classes that belong to the TextField component. TextField is a UI element that handles inputs and allows you to set the type of keyboard characters that appear in the input field using KeyboardOptions. KeyboardActions can then define what happens when the Enter key is pressed. In the account screen in my route, there is a place where you store your credit card information for payments. Since the part where user enters the card number is related to the device's keyboard, we implemented it with KeyboardActions and KeyboardOptions. @Composable fun CreditCardNumberInputField( value: String, onValueChange: (String) -> Unit, placeholderText: String, onNextClick: () -> Unit = {} ) { ThinOutlinedTextField( ... singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, imeAction = ImeAction.Next ), keyboardActions = KeyboardActions( onNext = { onNextClick() } ) ) } The above code is implemented with the following UI. So that only credit card numbers could be entered, KeyboardActions sets the KeyboardType to Number, and ImeAction. Next is set so that the input moves as you type. KeyboardOptions also makes it so that the onNextClick() method is executed when the "Next" button on the keyboard is pressed. By the way, onNextClick() is set up in Fragments as follows. CreditCardNumberInputField( ... onNextClick = { binding.creditCardHolderName.requestFocus() } ) With these settings, when the "Next" button is pressed, you will go from entering the credit card number to the next step, entering your name. 3. LazyVerticalGrid LazyVerticalGrid displays items in a grid format. This grid can be scrolled through vertically and displays many items (or lists of unknown length). Also, the number of columns is adjusted according to the size of the screen, so items can be displayed effectively on various screens. The "This month's events" section in my route provides information on many events happening in the area where you are currently located. There was too much event information to be implemented in columns (title, image, event period), so we used LazyVerticalGrid to display event items in containers that could be scrolled through up and down over several rows. private const val COLUMNS = 2 LazyVerticalGrid( columns = GridCells.Fixed(COLUMNS), modifier = Modifier .padding(start = 16.dp, end = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp) ) { items(eventList.size) { index -> val item = eventList[index] EventItem( event = item, modifier = Modifier.singleClickable { onItemClicked(item) } ) } } The above code is implemented with the following UI. The image and title have been removed due to copyright. The items can now be displayed in a grid at regular intervals based on the size of the data in eventList, and the event information can be viewed constantly. 4. Drag And Drop The draggable modifier lets the user drag and drop something inside a screen component. If you need to control an entire drag flow, you use pointerInput. In my route, there is a function called "my station" that allows you to register up to 12 stations or bus stops. They are displayed in a card list format, so you can see it at a glance. This card list can be reordered freely and requires drag-and-drop operation to be implemented. itemsIndexed(stationList) { index, detail -> val isDragged = index == lazyColumnDragDropState.draggedItemIndex MyStationDraggableItem( detail = detail, draggableModifier = Modifier.pointerInput(Unit) { detectDragGestures( onDrag = { change, offset -> lazyColumnDragDropState.onDrag(scrollAmount = offset.y) lazyColumnDragDropState.scrollIfNeed() }, onDragStart = { lazyColumnDragDropState.onDragStart(index) }, onDragEnd = { lazyColumnDragDropState.onDragInterrupted() }, onDragCancel = { lazyColumnDragDropState.onDragInterrupted() } ) }, modifier = Modifier.graphicsLayer { val offsetOrNull = lazyColumnDragDropState.draggedItemY.takeIf { isDragged } translationY = offsetOrNull ?: 0f } .zIndex(if (isDragged) 1f else 0f) ) val isPinned = lazyColumnDragDropState.initialDraggedItem?.index == index if (isPinned) { val pinContainer = LocalPinnableContainer.current DisposableEffect(pinContainer) { val pinnedHandle = pinContainer?.pin() onDispose { pinnedHandle?.release() } } } } The above code is implemented with the following UI. Drag operations are detected by pointerInput and the detectDragGestures function processes drag events. When an item is dragged, the onDrag, onDragStart, onDragEnd, and onDragCancel methods of the lazyColumnDragDropState object are called, and the drag state is managed. provides the effect of updating and visually moving the Y-axis position of an item in a drag. This code also uses the isPinned variable and the LocalPinnableContainer to prevent items being dragged leaving the screen when the user scrolls. Summary This was a simple explanation, and you might not understand some parts right away, but that is how you use my route. At first, I felt rewriting the my route UI from an XML layout a little complicated as I was not used to Jetpack Compose. But I could understand the code written in Jetpack Compose very quickly; I found it is a very efficient way in terms of readability and maintenance. We will continue to improve the UX in my route by using Jetpack Compose in different ways. Thank you for reading to the end.
アバター
はじめに(活動の概要紹介) KINTOテクノロジーズで「学びの道の駅」という取り組みが始まりました! 「学び」+「道の駅」ってどういうことでしょうか?当社では アウトプットカルチャーを推進 しており、その活動として、テックブログやイベント登壇など様々な取り組みを行っております。 では、アウトプットの推進力となるのは何でしょうか? アウトプットの前提としての「インプット」つまり「学習した内容」というのがとても重要になると私たちは考えています。社内の 学習する力を強化 していく、そんなチームが立ち上がりました。今回のプロジェクトも有志で集まって活動が始まっています。 「道の駅」という言葉にもいろいろな想いが込められています。みなさんは道の駅を利用したことがありますでしょうか?道の駅は様々な地方の特産物が集まるコミュニティであり、旅人が身体を休ませる場所であり、他では出会うことが出来ない様々な未知の世界に遭遇できる素敵な「 居場所 」だと私たちは考えています。 そこで「学び」という旅を続けるみんなが気軽に立ち寄って、 新しい出会い に心ときめくそんな、みんなが集まって 元気をもらえる ような居場所を作る「道の駅」を作り出したいという想いから「学びの道の駅」が誕生しました。 「学びの道の駅」は何をやるのか? 社内の「勉強会」と「勉強会」が交わる「道の駅」として、勉強会を軸にした社内活性を支援します。 社内広報活動 今度、こういうテーマで勉強会やるよ! 気になるあの勉強会、どんな感じなんだろう? 勉強会の支援 勉強会を始めてみたいけど、どうやって始めると良いのか? 勉強会の運営しているけど、盛り上がらない… などの、お悩み相談 【運営メンバーに聞いてみた】どんな想いから「学びの道の駅」にたどり着きましたか? 中西: 私は「人生=学び」だと日々考えています。人は常に新たなことを学ぶことで生きがいを見つけたり、心の拠り所を見つけたり、人生に活力を与えてくれます。いままで出会って素敵だなと魅力的に感じる方は皆さん常に新たなことを学び続けている方でキラキラと輝いていました。会社全体にキラキラと輝く人たちが集う場所が作れたら毎日の仕事でもより良いものづくりができるようになると思っていた中で、「社内の勉強会の情報が散らばっている」「どんな学びの環境があるか知りたい」という声が社内から届くようになり、今回のプロジェクト立ち上げに繋がりました。 HOKA: 私は普段は人事の仕事をしているので、従業員の皆さんと面談している中で「もっとグループ間を越えたコミュニケーションがしたい」という声をいただき、漠然と「何とかしたいな」という気持ちを持っていました。同時に、業務を通して「KINTOテクノロジーズで活躍している人は勉強会に参加しているな」と気づくことがありました。この2点が交差して「学びながら交流できる仕組みが必要?!」というアイデアが生まれを上司に相談したところ、きんちゃんと中西さんを紹介され、「学びの道の駅」が文字通り爆誕しました。 きんちゃん: 私は15年以上前から様々な場面で「勉強会」の文化に触れてきました。KINTOテクノロジーズは、私が入社したときから既に「学びが仕事に溶け込んでいる」とても良い文化を持っていました。この良い文化をもっともっと拡げ、人と組織と事業の成長に貢献したい!という想いから、「勉強会の情報を集める」行動に関わっていく事となりました。 成り立ち 【成り立ち1】社内の勉強会の情報をまとめたい! KINTOテクノロジーズは、「勉強会、輪読会」といった「社員が有志で学ぶ活動」が活発な組織です。 社内で色々な勉強会が行われているけど、「どこで?」「いつ?」行われているのかが分からない!知りたい!もっと学びたい!という声を色々耳にする機会があり、それを見える化したいね!という想いが、私たちの活動の原点になります。 ということで、早速、情報収集をしてみると短期間で40件ほど勉強会が存在することが分かりました。他にも隠れ勉強会の存在を把握しているので、小規模なものも含めておそらく60以上の勉強会が社内で行われているのでは無いかと推測しています。 そこで、「こんなに勉強会が活発なのってすごくない?」と思った3人が集い、話し始めたのが2023年の11月末。 【成り立ち2】何をやろう!? まず、最初のミーティングでは、やりたい事を列挙していきました。勉強会を片っ端から突撃してみる?テックブログでどんどん発信する?等のアイデアが出たものの、まずは私たちのことを社内で知ってもらうことが重要では?という仮説に至りました。 そこで、私たちは3週間後の12/21に開催される社内LT大会に参加することにしました。まだ「学びの道の駅」には触れず、3人それぞれがLTに登壇し、きんちゃんは見事優勝(パチパチ)。まずは社内の人に自分たちを知ってもらうという行動を起こしました。 ※詳しくはLT大会のテックブログをご参照ください↓↓ 社内限定のLT大会を開催しました! 【成り立ち3】インセプションデッキを作ろう! 2023年12月27日に行われたMTGでは、「やりたいことがたくさんある私たちには指針が必要」ということに気づきました。そこで、年明けから「インセプションデッキ」を作成することにしました。インセプションデッキとは、ソフトウェア開発プロジェクトにおいて、メンバー全員がプロジェクトの開発に共通認識と目標を持って取り組むために作成されます。私たちは下記4項を明文化しました。 我々はなぜここにいるのか エレベーターピッチ やらないことリスト 俺たちのAチーム 明文化したことにより、「学びの道の駅」というプロジェクト名も自然にイメージが沸いて来て、迷わず決めることができました。 インセプションデッキを作る途中、協調学習の話や、ソース原理の話を交え、私たちはそれぞれ「学び」への想いを語らいました。インセプションデッキを作る工程自体も、私たちにとっての学びになっている、と感じた瞬間でした。 ついにエンジン始動!! インセプションデッキが出来上がったのが2024年1月後半。出来上がった時、私たちは少し焦っていました。なぜならば、インセプションデッキを作ったことで、やりたいこと・やるべきことが明確になり、一刻も早く動き出したかったからです。(インセプションデッキの提案者であるきんちゃんは「しめしめ、予想通りだ」とこっそり喜んでいたとかいないとか) 動き出す第一歩として月次で開催されているKINTOテクノロジーズ全メンバーが集まる「部会」で、私たち「学びの道の駅」が誕生したことを発表しました! それと同時に、「突撃!となりの勉強会」も開始していました。 2月22日に合同勉強会を運営されている皆さんに会議室に集まって頂きインタビューをしたのです。事前に企画書やインタビュー項目も作らず、スマホを出してその場で録音。インタビューする側もされる側も「え?この場で?!」という若干の戸惑いはありながらも、協力してくれました。(みなさんありがとう!) 後日、Podcastとして流せるよう不要コメントをカットし、無事に3月13日には全社Slackで全従業員にお披露目することができました。 今後について その後、私たちは3つの勉強会に突撃し、Podcastを2本公開し、Blogを2本取り掛かりながら、改めて振り返りと自分たちの今後について話しました 皆が知りたいことは何? 勉強会そのものに興味があるのか? 運営している側は、何を知ってもらいたいか? などを話し合った結果、「勉強会により、目的やニーズはそれぞれ違う。それぞれの個性に合わせたストーリーを個別に組み立てる方が良い」という結論に至りました。 また、 Podcastはなんの役割? 勉強会の広告宣伝としてのコンテンツ? 社内報としてのコンテンツ? といった点についても検討した結果、「こんなにもたくさんの勉強会をやっていることがKTCの日常」、すなわち「勉強会文化が根付いていることを見える化できれば目的達成なのでは」という結論に至りました。今後については、勉強会に突撃しながらPodcastを作り、失敗があれば学び、伸ばすところは伸ばす、という活動方針で進める事にしました! 実は、この「動きながら、ふりかえり、軌道修正して、より良い方向へ進んでいく」というアジャイルな進め方に一人、冷や汗をかいているHOKAがいました。KTCに入社するまで情報の取り扱いにはルールやフローが決まっている会社で働いていたからです。 「学びの道の駅」の事務局として活動することにより、人事でありながらKTCの開発方針「小さく作って大きく育てる」を学ぶ機会となっています。 「学びの道の駅」は始まったばかり。これからも時々、KINTOテックブログに登場する予定ですので、どうぞよろしくお願いいたします。
アバター
KINTOテクノロジーズで my route(iOS) を開発しているRyommです。 同じく開発メンバーの保坂さんと張さん、そしてパートナー1名の計4名でスナップショットテストを導入・実装しました。 はじめに my routeアプリには現在SwiftUI化を進めていこうという流れがあり、そのための布石としてスナップショットテストを導入することにしました。 my routeにおけるSwiftUI化は、土台はUIViewControllerのまま、中のコンテンツだけをまずSwiftUIに置き換える形で進めるため、ここで実装したスナップショットテストはそのまま利用できる想定です。 ここでは、スナップショットテストをUIKitで構築されたアプリに適用するにあたって試行錯誤したテクニックを紹介します。 Snapshot Testとは コードの改修前と改修後のスクリーンショットに差分が出ていないかを確認できるテストです。 ライブラリは、Point-Freeの https://github.com/pointfreeco/swift-snapshot-testing を使用しています。 my routeでは、以下のようにXCTestCaseを拡張してassertSnapshotsをラップしたメソッドを作成しています。 閾値が98.5%になっているのは、非常に細かい許容範囲の差異が成功になるように色々試して出た値です。 extension XCTestCase { var precision: Float { 0.985 } func testSnapshot(vc: UIViewController, record: Bool = false, file: StaticString, function: String, line: UInt) { assert(UIDevice.current.name == "iPhone 15", "Please run the test by iPhone 15") // SnapshotConfigはテストする端末一覧のenum SnapshotConfig.allCases.forEach { assertSnapshots(matching: vc, as: [.image(on: $0.viewImageConfig, precision: precision)], record: record, file: file, testName: function + $0.rawValue, line: line) } } } 画面ごとのスナップショットテストは以下のように書いています。 final class SampleVCTests: XCTestCase { // snapshot test 録画モードか否か var record = false func testViewController() throws { let SampleVC = SampleVC(coder: coder) let navi = UINavigationController(rootViewController: SampleVC) navi.modalPresentationStyle = .fullScreen // ここでライフサイクルメソッドが一通り呼び出される UIApplication.shared.rootViewController = navi // viewDidLoad以降のライフサイクルメソッドが実行端末分呼ばれる testSnapshot(vc: navi, record: record, file: #file, function: #function, line: #line) } } Tips viewWillAppear以降にAPIで取得したデータがViewに反映されるのを待ちたい APIで取得したデータがViewに反映されてからスナップショットテストが実行されるようにしたいですが、画面に反映される前にスナップショットテストが実行されてしまい、インジケーターが表示されたままになってしまうなどの問題がありました。 素のままではAPI通信後のデータがViewに反映されたかどうかを判断するのが難しいため、判定用のデリゲートを用意します。 protocol BaseViewControllerDelegate: AnyObject { func viewDidDraw() } ViewControllerクラスで、上で用意したデリゲートに準拠したデリゲートプロパティを作成し、初期化時に特に指定されなかった場合はnilになるようにしておきます。 class SampleVC: BaseViewController { // ... weak var baseDelegate: BaseViewControllerDelegate? // .... init(baseDelegate: BaseViewControllerDelegate? = nil) { self.baseDelegate = baseDelegate super.init(nibName: nil, bundle: nil) } // ... } APIを呼び出して画面に反映している場面、例えばCombineで結果を受け取って画面に反映したあとに baseDelegate.viewDidDraw() を呼び出すことで、スナップショットテスト側に結果をViewに反映ができたことを教えられるようになります。 someAPIResult.receive(on: DispatchQueue.main) .sink(receiveValue: { [weak self] result in guard let self else { return } switch result { case .success(let item): self.hideIndicator() self.updateView(with: item) // データ反映完了のタイミング self.baseDelegate?.viewDidDraw() case .failure(let error): self.hideIndicator() self.showError(error: error) } }) .store(in: &cancellables) baseDelegate.viewDidDraw() が実行されるのを待ちたいため、スナップショットテストにXCTestExpectationを追加します。 final class SampleVCTests: XCTestCase { var record = false var expectation: XCTestExpectation! func testViewController() throws { let SampleVC = SampleVC(coder: coder, baseDelegate: self) let navi = UINavigationController(rootViewController: SampleVC) navi.modalPresentationStyle = .fullScreen UIApplication.shared.rootViewController = navi expectation = expectation(description: "callSomeAPI finished") wait(for: [expectation], timeout: 5.0) viewController.baseViewControllerDelegate = nil testSnapshot(vc: navi, record: record, file: #file, function: #function, line: #line) } func viewDidDraw() { expectation.fulfill() } } 反映したいAPIから取得するデータが複数あるとき(= baseDelegate.viewDidDraw() を複数箇所で呼びたいとき)は、 expectedFulfillmentCount や assertForOverFulfill を指定します。 final class SampleVCTests: XCTestCase { var record = false var expectation: XCTestExpectation! func testViewController() throws { let SampleVC = SampleVC(coder: coder, baseDelegate: self) let navi = UINavigationController(rootViewController: SampleVC) navi.modalPresentationStyle = .fullScreen UIApplication.shared.rootViewController = navi expectation = expectation(description: "callSomeAPI finished") // viewDidDraw()が2回呼ばれるとき expectation.expectedFulfillmentCount = 2 // 指定した回数を超えてviewDidDraw()が呼ばれる可能性があるとき、超えた分は無視する expectation.assertForOverFulfill = false wait(for: [expectation], timeout: 5.0) viewController.baseViewControllerDelegate = nil testSnapshot(vc: navi, record: record, file: #file, function: #function, line: #line) } func viewDidDraw() { expectation.fulfill() } } 前の画面のbaseViewControllerDelegateが残っていると、全画面を通してスナップショットテストを実行した際に testSnapshot() を呼び出したタイミングでviewDidLoad以降のライフサイクルメソッドが実行端末分呼ばれるため、APIも再度実行され、 viewDidDraw() も実行されてしまい、multiple calls エラーになります。 そのため、 wait() の後にbaseViewControllerDelegateをクリアしています。 端末でframeがズレる スナップショットテストでは複数端末のスナップショットを生成できますが、一部の端末でパーツの配置やサイズがズレてしまう問題がありました。 ずれとる これは、スナップショットテストの実行のライフサイクルに起因しています。 スナップショットテストではある一つの端末で起動し、その後別の端末は再読み込みをせずにサイズを変えて再描画されます。つまり、 viewDidLoad() は最初の一度のみ実行され、その他の端末分は viewWillAppear() から実行されます。 対処法としては、テストしたいViewControllerをラップしたMockViewControllerを作成し、 viewDidLoad() で読んでいるメソッドを viewWillAppear() で呼ぶように上書きします。 import XCTest @testable import App final class SampleVCTests: XCTestCase { // snapshot test 録画モードか否か var record = false func testViewController() throws { // 画面を呼び出す時と同様に書く let storyboard = UIStoryboard(name: "Sample", bundle: nil) let SampleVC = storyboard.instantiateViewController(identifier: "Sample") { coder in // スナップショットテスト用にラップしたVC MockSampleVC(coder: coder, completeHander: nil) } let navi = UINavigationController(rootViewController: SampleVC) navi.modalPresentationStyle = .fullScreen UIApplication.shared.rootViewController = navi testSnapshot(vc: navi, record: record, file: #file, function: #function, line: #line) } } class MockSampleVC: SampleVC { required init?(coder: NSCoder) { fatalError("init(coder: \\(coder) has not been implemented") } override init?(coder: NSCoder, completeHander: ((_ readString: String?) -> Void)? = nil) { super.init(coder: coder, completeHander: completeHander) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 以下は本来viewDidLoad()で呼び出しているメソッド super.setNavigationBar() super.setCameraPreviewMask() super.cameraPreview() super.stopCamera() } } まだ直らない・・・ それでも描画がズレる場合、 layoutIfNeeded() メソッドを呼び出し、フレームを更新すると多くの場合で直りました。 import XCTest @testable import App final class SampleVCTests: XCTestCase { var record = false func testViewController() throws { let storyboard = UIStoryboard(name: "Sample", bundle: nil) let SampleVC = storyboard.instantiateViewController(identifier: "Sample") { coder in MockSampleVC(coder: coder, completeHander: nil) } let navi = UINavigationController(rootViewController: SampleVC) navi.modalPresentationStyle = .fullScreen UIApplication.shared.rootViewController = navi testSnapshot(vc: navi, record: record, file: #file, function: #function, line: #line) } } fileprivate class MockSampleVC: SampleVC { required init?(coder: NSCoder) { fatalError("init(coder: \\(coder) has not been implemented") } override init?(coder: NSCoder, completeHander: ((_ readString: String?) -> Void)? = nil) { super.init(coder: coder, completeHander: completeHander) } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // 描画系メソッドを呼ぶ前にframeを更新 self.videoView.layoutIfNeeded() self.targetView.layoutIfNeeded() super.setNavigationBar() super.setCameraPreviewMask() super.cameraPreview() super.stopCamera() } } いい感じ Webview画面のスナップショット Webviewで表示している画面のコンテンツについては関知しないが、配置しているツールバーなどはスナップショットテストを適応したい、という場面があると思います。 そのような場合、WebViewをロードする部分をWebView自体の設定とは切り分け、テストで呼ばないようにモックすると良いです。 実装側で self.webview.load(urlRequest) などを呼んでWebViewのコンテンツを表示するメソッドと、WebView自体の設定をしているメソッドと切り分けています。 // VCの実装 class SampleWebviewVC: BaseViewController { // ... override func viewDidLoad() { super.viewDidLoad() self.setNavigationBar() **self.setWebView()** self.setToolBar() } override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) **self.setWebViewContent()** } // ... /** * WebViewの設定とコンテンツの設定をするメソッドを分ける */ /// WebViewを設定する func setWebView() { self.webView.uiDelegate = self self.webView.navigationDelegate = self // Webページ読み込み状態の監視 webViewObservers.append(self.webView.observe(\\.estimatedProgress, options: .new) { [weak self] _, change in guard let self = self else { return } if let newValue = change.newValue { self.loadingProgress.setProgress(Float(newValue), animated: true) } }) } /// WebViewにコンテンツを設定する private func setWebViewContent() { let request = URLRequest(url: self.url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60) self.webView.load(request) } // ... } そしてテスト対象のVCをラップしたモックではWebViewのコンテンツをロードするメソッドを呼ばないようにします。 import XCTest @testable import App final class SampleWebviewVCTests: XCTestCase { private let record = false func testViewController() throws { let storyboard = UIStoryboard(name: "SampleWebview", bundle: .main) let SampleWebviewVC = storyboard.instantiateViewController(identifier: "SampleWebview") { coder in MockSampleWebviewVC(coder: coder, url: URL(string: "<https://top.myroute.fun/>")!, linkType: .hoge) } let navi = UINavigationController(rootViewController: SampleWebviewVC) navi.modalPresentationStyle = .fullScreen UIApplication.shared.rootViewController = navi testSnapshot(vc: navi, record: record, file: #file, function: #function, line: #line) } } fileprivate class MockSampleWebviewVC: SampleWebviewVC { override init?(coder: NSCoder, url: URL, linkType: LinkNamesItem?) { super.init(coder: coder, url: url, linkType: linkType) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewWillAppear(_ animated: Bool) { // viewDidLoadで呼び出していたメソッドをviewWillAppearで呼ぶように変更 self.setNavigationBar() self.setWebView() self.setToolBar() super.viewWillAppear(animated) } override func viewDidAppear(_ animated: Bool) { // Do nothing // WebViewのコンテンツ設定をするメソッドを呼ばないように上書き } } カメラを呼び出している画面のスナップショット カメラを呼び出し、その上にカスタマイズしたViewを表示している画面もスナップショットしたいです。しかし、シミュレータ上ではカメラが動かないので、どうにかカメラ部分を無効化しつつオーバーレイ部分をテストできるようにする必要があります。 シミュレーター上でカメラ画像が動くようにダミー映像を差し込めるようにする案もありましたが、メインではない画面のスナップショットテストのためだけに導入するのもコストが見合わず悩みどころです。 myrouteのスナップショットテストでは、カメラ映像の入力を取り込んだり、AVCaptureVideoPreviewLayerで表示するキャプチャーを設定したりする部分を丸ごと呼ばないようにモックで上書きするようにしました。こうすることで、入力のないAVCaptureVideoPreviewLayerが真っ白な画面として表示され、その上にカスタマイズしたViewを表示することができます。 実際の実装では以下のように書かれているところを・・・ class UseCameraVC: BaseViewController { // ... override func viewDidLoad() { super.viewDidLoad() self.videoView.layoutIfNeeded() setNavigationBar() setCameraPreviewMask() do { guard let videoDevice = AVCaptureDevice.default(for: AVMediaType.video) else { return } let videoInput = try AVCaptureDeviceInput(device: videoDevice) as AVCaptureDeviceInput if captureSession.canAddInput(videoInput) { captureSession.addInput(videoInput) let videoOutput = AVCaptureVideoDataOutput() if captureSession.canAddOutput(videoOutput) { captureSession.addOutput(videoOutput) videoOutput.setSampleBufferDelegate(self, queue: DispatchQueue.main) } } } catch { return } cameraPreview() } override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) // シミュレータの時はカメラが使えないので、閉じる #if targetEnvironment(simulator) stopCamera() dismiss(animated: true) #else captureSession.startRunning() #endif } } 以下のようにモックで上書きします。 frameがズレる問題で解説した理由から、 viewDidLoad() で呼び出していたメソッド群も viewWillAppear() で呼ぶようにしています。 class MockUseCameraVC: UseCameraVC { // ... override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.videoView.layoutIfNeeded() super.setNavigationBar() super.setCameraPreviewMask() super.cameraPreview() super.stopCamera() } } cameraPreview() メソッドはAVCaptureVideoPreviewLayerで captureSession からカメラ映像を画面に表示していますが、ここの入力がないように上書きしてあるので、白いViewで描画されます。 CI戦略 スナップショットテスト導入初期は、リファレンス画像を単一のS3バケットにアップロードし、レビューの際は都度リファレンス画像をダウンロードしてテストを実行していました。 しかし、あるViewを修正して同時にリファレンス画像を更新した場合に、そのPRがマージされるまでの間は他のPRのテストが通らなくなってしまうという問題がありました。 そこで、リファレンス画像をホストしているバケット内に2つディレクトリを作成しました。 片方はPRレビュー時の画像をホストしており、PRがマージされたらもう一方のディレクトリにコピーします。そうすることで、リファレンス画像の更新があっても他PRのテストの妨げにならないようにしました。 便利なシェルたち my routeでは4つのスナップショット用のシェルを用意しています。 1つ目は現行の画面のリファレンス画像を一通りダウンロードするシェルです。 これにより、ローカルでテストが通るようになります。 # developブランチから切り替えたときに利用 # 例:sh setup_snapshot.sh # リファレンス画像のディレクトリから古いものを掃除 rm -r AppTests/Snapshot/__Snapshots__/ # S3からリファレンス画像をダウンロード aws s3 cp $awspath/AppTests/Snapshot/__Snapshots__ --recursive --profile user 2つ目はPull Requestを作成する際に、変更があるリファレンス画像をPRレビュー用のS3バケットにアップロードするシェルです。 # PRを作成する際に、変更したテストを引数にしてアップロードしてください # 例:sh upload_snapshot.sh ×××Tests path="./SpotTests/Snapshot/__Snapshots__" awspath="s3://strl-mrt-web-s3b-mat-001-jjkn32-e/mobile-app-test/ios/feature/__Snapshots__" if [ $# = 0 ]; then echo "引数がありません" else for testName in "${@}"; do if [[ $testName == *"Tests"* ]]; then echo "$path/$testName" aws s3 cp "$path/$testName" "$awspath/$testName" --exclude ".DS_Store" --recursive --profile user else echo "($0testName) テストが存在しません" fi done fi 3つ目は改修が入った画面のリファレンス画像を個別にダウンロードするシェルです。 画面の変更があるPull Requestのレビュー時に使用します。 # テストレビューする際、対象のテストのリファレンス画像をダウンロードしてください # 例:sh download_snapshot.sh ×××Tests if [ $# = 0 ]; then echo "引数がありません" else rm -r AppTests/Snapshot/__Snapshots__/ for testName in "${@}"; do if [[ $testName == *"Tests"* ]]; then echo "$localpath/$testName" aws s3 cp "$awspath/$testName" "$localpath/$testName" --recursive --profile user else echo "($0testName) テストが存在しません" fi done fi 4つ目は強制的にリファレンス画像を更新するシェルです。 基本的にはテストファイルに修正があった画面のリファレンス画像を自動的にコピーするようにしているので不要ですが、共通部品を修正した場合など、テストファイルを変更せずにリファレンス画像に変更が入る際に有用です。 # 修正したテストファイル以外のリファレンス画像に影響がある場合(共通部品を修正した場合等)、 # 手動でアップロードしてください # マージ後に利用してください # 例:sh force_upload_snapshot.sh ×××Tests if [ $# = 0 ]; then echo "引数がありません" else echo "強制的にAWS S3のdevelopフォルダにアップロードしますか?【yes/no】" read question if [ $question = "yes" ]; then for testName in "${@}"; do if [[ $testName == *"Tests"* ]]; then echo "$localpath/$testName" aws s3 cp "$localpath/$testName" "$awsFeaturePath/$testName" --exclude ".DS_Store" --recursive --profile user aws s3 cp "$localpath/$testName" "$awsDevelopPath/$testName" --exclude ".DS_Store" --recursive --profile user else echo "($testName) テストが存在しません" fi done else echo "終了" fi fi 4つもあるとどれがいつ誰が使うのかわからなくなってしまうので、Taskfileにも定義し、説明をすぐ出せるようにしています。 実行時は、ファイル名の指定など引数を渡すときに -- をつけなくてはいけなかったり、若干長くなるのでシェルをそのまま呼ぶ事が多いですが、この説明のためだけに設定する価値があると思います。 % task task: [default] task -l --sort none task: Available tasks for this project: * default: show commands * setup_snapshot: [For Assignee] [ブランチ切替後] スナップショットテストの修正時など、developブランチから切り替えたときに利用 (例) task setup_snapshot または sh setup_snapshot.sh * upload_snapshot: [For Assignee] [PR作成時] 変更したテストを引数にして、PR確認用のS3へスナップショット画像をアップロード (例) task upload_snapshot -- ×××Tests または sh upload_snapshot.sh ×××Tests * download_snapshot: [For Reviewer] [レビュー時] 対象のテストを引数にして、リファレンス画像をダウンロード (例) task download_snapshot -- ×××Tests または sh download_snapshot.sh ×××Tests * force_upload_snapshot: [For Assignee] [マージ後] 修正したテストファイル以外のリファレンス画像に影響がある場合(共通部品を修正した場合等)に、変更があるテストを引数にして、手動でアップロード (例) task force_upload_snapshot -- ×××Tests または sh force_upload_snapshot.sh ×××Tests また、これはRyommが個人的に設定しているものですが、シェルにプロファイル名がベタ書きになっているのを自分の環境で設定しているプロファイルに書き換えるエイリアスも用意しておくと便利です。(プロファイル名にこだわりがある人向け) ここでは、 user とベタ書きされているプロファイルを myroute-user に書き換えています。 alias sett="gsed -i 's/user/myroute-user/' setup_snapshot.sh && gsed -i 's/user/myroute-user/' upload_snapshot.sh && gsed -i 's/user/myroute-user/' download_snapshot.sh && gsed -i 's/user/myroute-user/' force_upload_snapshot.sh" Bitrise my routeではBitriseを利用してCIを行っています。 スナップショットテストの変更を含むPRがマージされたとき、Bitriseはスナップショットテストの修正があるかどうかを自動的に判断し、リファレンス画像をfeatureフォルダーからdevelopフォルダへコピーします。 これにより、すべての状況においてスナップショットテストが常に正常に動作することができます。 肉眼で判断できないリファレンス画像の差異を炙り出す 目視では違いが分からないものの、スナップショットテストがエラーを吐く時があります。 (3_3)? そんなときは、ImageMagickを用いて重ね合わせて見ると違いを見つけやすいです。 以下のようにコマンドを実行すると・・・ convert Snapshot/refarence.png -color-matrix "6x3: 1 0 0 0 0 0.4 0 1 0 0 0 0 0 0 1 0 0 0" ~/changeColor.png \ && magick Snapshot/failure.png ~/changeColor.png -compose dissolve -define compose:args='60,100' -composite ~/Desktop/blend.png \ && rm ~/changeColor.png 画像を重ね合わせて見ることができます。 リファレンス画像の色相を赤っぽくしてから重ね合わせることで、若干見やすくなるかと思います。 さらに使いやすいように、.bashrcに登録しておくのがおすすめです。 compare() { convert $1 -color-matrix "6x3: 1 0 0 0 0 0.4 0 1 0 0 0 0 0 0 1 0 0 0" ~/Desktop/changeColor.png; magick $1 ~/Desktop/changeColor.png -compose dissolve -define compose:args='60,100' -composite ~/Desktop/blend.png; rm ~/Desktop/changeColor.png } ある程度置かれるファイルが同じ場合は、引数に取る値をパス全体ではなくテスト名のみにしても良いかもしれません。 オンライン上にホストされている画像も実行することができるので、レビュー時にも使えます。 さいごに突撃インタビュー! さいごにスナップショットテストを導入してみた感想をインタビューしてきました! 張さん「最初の保坂さんの研究のおかげで、私たちも今はこんな便利な方法でスナップショットを対応できています。その後は、Ryommさんの協力で忘れないよう色々な実装方法がドキュメントとしてまとめて整理されました。本当に良かったと思います。感謝しております。🙇‍♂️」 保坂さん「全体テストするとすごい時間かかるのがネックなので、今後短縮できるような取り組みをしていきたい。」 Ryommとしては、ロジックが変わったときに画面には影響が出ないけどスナップショットテストには影響があることがあって、それの修正がつらいな〜という気持ちが最近芽生えてます。SwiftUI化を進める際には違いがないか確認しやすくて、その点はよかったと思います!
アバター
「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」輪読会が最高だったのでシェアしたい こんにちは、あわっち( @_awache ) です。 今回は「** GitLabに学ぶ 世界最先端のリモート組織のつくりかた ドキュメントの活用でオフィスなしでも最大の成果を出すグローバル企業のしくみ **」という本に魅せられ、輪読会を社内外を巻き込んだイベントにしている取り組みについて共有させていただきます。 告知: 「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」そーだいなる輪読会 フィナーレ を開催します いきなりの告知となりますが大切なことなので最初に書きます。 Cnnpass: 「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」そーだいなる輪読会 フィナーレ 開催日時: 2024-04-25(木) 18:00 ~ 21:00 (17:40 開場) 形式: オフライン 会場: KINTOテクノロジーズ株式会社(略称:KTC)室町オフィス 「** GitLabに学ぶ 世界最先端のリモート組織のつくりかた ドキュメントの活用でオフィスなしでも最大の成果を出すグローバル企業のしくみ **」の輪読会をすでにした方、現在進行形でしている方、そしてこれからしたい方向けの輪読会イベントとなっております。 実際に自分たちで輪読会をしてみてどんな形で輪読会をしたのか?実際反響どうだったのか?この本からどんな気づきを得られたのか?を参加者全員でディスカッションできる場として提供したいと考えています。 まだ枠に余裕がありますので興味ある方はぜひご参加ください!軽い気持ちで来ても楽しめるように工夫してみたいと考えています。 (リモート組織の話をオフラインでしようという矛盾には触れないでおいてください笑) 輪読会によくある課題 継続的な参加者の確保 本を一冊読み終えるためには定期的に集まる必要があります、週に 1回開催するとして適切な分量に分割すると2 ~3 ヶ月くらいは参加者の時間を確保する必要があります 途中フェードアウト 継続的に物事を進めることは非常に難しいです 会を重ねるごとに一人、また一人とフェードアウトしてしまうことは自然な流れです 参加者のモチベーションを繋ぎ止めることができなければ最後にはボッチ輪読会、みたいなことにもなりかねません 途中参加の難しさ 輪読会は本を読んでいくという特性上、途中参加のハードルが上がりやすいので人数は減ることはあっても増えることは非常に珍しい状況です リーダーシップの持続 リードする人の負担は様々なところにあります 事前準備 参加者の時間の確保 ファシリテーション これらは単発ではなくその輪読会が完了するまでずっと続きます 一人でやり続けるには相当なモチベーションが必要となります 参加者の読書速度や理解度の違いを認識する 参加者によって読書のスピードや理解度は異なります、それを認識しないで輪読会を行うと議論が盛り上がらずに退屈な時間が過ぎることになります などなど、本を一冊やり切ることってなんだかんだ難しいですよね。。。僕自身も途中でフェードアウトしたり最後までやりきれないで自然消滅させてしまったりしたことが何度もあります。 ただ、今回はどうしても この本の考え方を社内に広く共有したかった ことと、 最後までやり切りたい という思いが強かったこともあり、どうしたらこれらの課題を解決できるかを考えていました。 例えば、自分たちだけではなく企業の枠を超えて複数の場所で同じ題材で別々の方法で輪読会を実施し、最後に実施した内容をアウトプットするという機会を作ることで課題に対して効果的にアプローチできるのではないかという仮説を立ててみたたのですが、実現には僕だけでやるには限界があります。 そこで、弊社 DBRE チームで技術アドバイザーをしていただいている @soudai1025 さんに相談し、 @soudai1025 さんの協力のもと「そーだいなる輪読会」という企画を始めることにしました。 そーだいなる輪読会の開催 そーだいなる輪読会は複数の企業が このキックオフをきっかけに 3ヶ月程度の時間を設けて輪読会を実施 最後にアウトプットをする という三段構えのイベントです。 キックオフの様子は YouTube に上がっていますのでこちらもぜひご確認ください。 https://www.youtube.com/watch?v=IBgmGtpW15Q 輪読会開催までの道のり キックオフも終わり輪読会を開催するまでに僕が準備したことをざらっと記載してみます。 仲間集め まずは社内のオープンチャンネルで仲間集めです。輪読会に興味がある方を把握し、手を挙げてくれる方を待ちます。結果として 14名の同志 を獲得しました! 写経 (本一冊分書き起こし) 輪読会をリードしていこうと決めた時から写経することの覚悟を決めていました。 読む → 書く→ 見直す、ということを一気にできる写経というアクションは書かれていることを短時間で理解するためには非常にいいアクティビティだと思っています。 ただし、この本は300ページ強、覚悟が必要です(笑) 書籍のまとめ購入 採用情報にも記載がある のですが、KINTOテクノロジーズは必要な書籍は会社で購入することができます。14名の同志の中でこの本を持っていない方が何名かいらっしゃったのでこの制度を利用し持っていない方の分をまとめ買いしました。 社内輪読会の進め方検討 みんなが負荷なく、いつ来てもそれなりに楽しむためにはどうしたらいいのか、それを考えて本気で悩みました。具体的なアクションは後ほど紹介しますね。 このようなことをしていたら気づいたら社内輪読会のキックオフが 2月になってしまいました。 社内輪読会キックオフ Working Agreement 僕個人としてどんな輪読会の空気を作りたいか、をまとめたものを参加者の方々に共有しました。 以下がその内容です。 本輪読会は 参加者の負荷を最小限に抑える ことを考えて設計する 本を読んでこれなかったとしてもフォローし合う 最初の 10分間、黙々読書タイムを設ける ディスカッションをメイン とし、その アウトプットを公開 することで実際に参加していないメンバーでも途中参加可能な空気を作る 毎回アウトプットをまとめて誰でも閲覧可能な状態にする (可能であれば) Zoom を録画し公開する 同じ内容を複数回やることも可能 参加者の 自由な発言を妨げない 参加者の発言に共感できること、できないことがあることを受け入れる 自由なディスカッションを活性化させる ディスカッションは最大 4人のブレイクアウトルームで実施する 少人数でディスカッションを行うことで話すことの心理的ハードルを下げ、それぞれが話したい内容を話せる状態を作るる ROM (Read Only Member) 参加を拒絶しない 状況を理解するために意思を参加メンバーに伝える 今日はちょっと話せない 周りの環境で話しづらい アウトプットは全員で積極的に作る ディスカッション内容の議事録などは手が空いている人 (発言していない人など) が積極的にログとして残す 輪読会の進め方 継続的なディスカッションを行うにはやはり一定のタイムラインがあることが望ましいと考えています。 ちょっと遅れたけど輪読会に途中から入りたい、と思っても白熱したディスカッションを行なっている時間帯だったら心理的に入ることが難しいかもしれません。 逆に今何をしているのか、ある程度わかっていれば、例えば今黙々読書タイムだから途中から入っても大丈夫、となる可能性もあります。 そのため型をバシッと決めました。 基本設計 黙々読書タイム (10分) ディスカッションタイム (30分) 内容共有 (20分) ディスカッション内容 共感したこと 共感できなかったこと KTC で実際に実践してみたいこと 次の回で実践した結果を共有してもいいかもしれない ディスカッション内容アウトプット ディスカッション中の議事は Google Slide を用意するのでそこに記載 ディスカッションタイム終了後に全員でチームの内容を共有し合う 使用するツールの選定 Gather Webミーティングツールとしては Gather を選択しました。 僕たちはディスカッションをメインとしたため、参加している人たちが気軽に話し合える人数で話していくことを考えていました。Zoom だと毎回ブレイクアウトルームを作らないといけないですし振り分けるのも大変です。 バーチャルオフィス空間である Gatherであれば、全員で集まる、少人数の部屋に移動してディスカッションができる、という自分たちのニーズにバッチリ合いました。 ただし、録画を共有する、ということには不向きなのでそこは諦めました。 代わりにしっかりとログを残して後からでも見直せるようにすることを徹底しました。 Microsoft Loop コラボレーションには Loop を選択しました。KINTOテクノロジーズは基本的に Confluenceを使用しているのですが参加者が自由にメモを書き、共同編集を行うということに対しては少し弱かったので色々と考えた結果 Confluenceと体験がそれほど変わらない、けど共同編集のストレスが少ないという理由でこちらを採用です。 おかわりの会の設定 輪読会の時間は毎週火曜日の 18:00 ~ 19:00 に設定しました。ただ、この時間だとちょっと遅いですし、突発の業務都合が入ったり、お子さんがいたりする方にとってはゴールデンタイムと重なったりすることがあります。 そんな時に一度でも参加を逃してしまうと再参加の心理的ハードルが高くなってしまいます。なので僕は翌日の水曜日、12:00 ~ 13:00 で全く同じ内容を実施することを決めました。 これによって参加逃しのリスクを下げることができますし、前日に参加してくださった方でもより深く内容を理解する時間が持てるだけでなく、別の参加者の話を聞くことで新しい視点を持てて毎回楽しく参加することができました。 生成 AI の活用 上記の Working Agreement にも書いたのですが、僕自身本を読んでこなくてもフォローし合える場を作りたい思いが強かったです。最初の 10分間で黙々タイムを設けると言っても 10分間で必要な分量を読み切ることは割と難しいです。 そこで強い味方になってくれたのが写経と ChatGPT です。写経した文字列を必要な分だけ ChatGPT で要約をすることでたとえ10分間の黙々タイムでも参加者のインプットの質は大きく変わることを実感しました。 例えば第1部を要約するとこんな感じです。およそ 12ページがこの量になると黙々タイムも効果的だと思いませんか? ![AIざっくり要約](/assets/blog/authors/_awache/20240422/AIざっくり要約.png =750x) 元の文章も Confluence にはあるので要約の中で気になったところがあったらキーワードを検索するとすぐにそのポイントを見つけることもできます。 個人的にはこれがあったからこそ最後まで走りきれたんだと思っているくらい重要な要素でした。 社内輪読会 ![輪読会の様子](/assets/blog/authors/_awache/20240422/gather.png =750x) 結果として全部で 17回の輪読会を開催しました。17回も行なった中で最後までぼっちにならずにやり切ることができました(笑) 毎回参加してくださる方もいましたし、ご自身の都合で来れる時に来ていただく、という方もいました。おかわりがあることで 1回当たりの参加人数のばらつきがあったとしても、一章あたりの参加という点ではなかなかいい数字な気がしています。 第1部 リモート組織のメリットを読み解く / 第2部 世界最先端のリモート組織並行するためのプロセス 2024-02-13 (6名) 2024-02-14 (9名) 第5章 カルチャーはバリューによって醸成される 2024-02-21 (8名) 2024-02-27 (4名) 2024-02-28 (4名) 第6章 コミュニケーションのルール 2024-03-05 (7名) 2024-03-06 (5名) 第7章 リモート組織におけるオンボーディングの重要性 / 第8章 心理的安全性の醸成 2024-03-13 (7名) 2024-03-19 (5名) 第9章 個人のパフォーマンスを引き出す/第10章 GitHub Value に基づいた人事制度 2024-03-26 (7名) 2024-03-27 (5名) 第11章 マネージャーの役割とマネジメントを支援するための仕組み & 第12章 コンディショニングを実現する 2024-04-02 (6名) 2024-04-03 (7名) 第13章 L&D を活用してパフォーマンスとエンゲージメントを向上させる & おわりに 2024-04-09 (7名) 2024-04-10 (5名) wrap up! 2024-04-16 (5名) 2024-04-17 (4名) どんなことを話したのか、についてはぜひ 「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」そーだいなる輪読会 フィナーレ に参加して聞いてみてください! というのもやはりこの本は自分たちからみたら目指すべき姿が多く書かれており、現実との GAP についてどう考えているのか、という少し記事にしづらい生臭い議論がたくさんあったので、そこは当日お話しさせてください。 この輪読会を通じて得たもの/アウトプットしたもの 仲間 この輪読会に参加くださったことで初めて話した方も何人かいました、またこの輪読会を通じて参加くださった方々の思考も知ることができましたし、これからもこの繋がりを大切にしながら KINTOテクノロジーズを盛り上げていこうと思います。 弊社にはオープンに感謝を言い合える #thanks というチャンネルがあります。輪読会が終わった日に参加くださった方から温かい言葉をいただけたのも非常に嬉しかったです。 ![thanks](/assets/blog/authors/_awache/20240422/thanks.png =750x) 写経 これがあったから AI 要約だったり、輪読会のディスカッションの中で出た様々な話にも対応できたりしたので今後も輪読会をリードして行こうと思ったら重要なプロセスだと感じています。 AI 要約 生成 AI を活用して出力した要約はやはり強力です。どこに何が書いてあったか、時間の経過とともに忘れてしまうこともあるかもしれないですが、要約があればさらっと 10分眺めるだけで記憶が呼び戻されます。 マンダラチャート 自分なりにこの本のポイントをマンダラチャート方式でまとめてみました。もちろん全部やるなんて無理なのでポイントとテーマを決めて少しづつ自分のできることを増やしていきたいです。 おわりに 僕たちが行なった「** GitLabに学ぶ 世界最先端のリモート組織のつくりかた ドキュメントの活用でオフィスなしでも最大の成果を出すグローバル企業のしくみ **」の輪読会の様子はいかがでしたでしょうか? 個人的にはこれまで自分で行なってきたどの輪読会よりも満足度が高かったのでシェアをしたい気持ちが強くなったのでテックブログに書き起こさせていただきました。 本当はまだまだ書きたいこともあるのですが長くなりすぎるので一旦ここまでにします。 再告知: 「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」そーだいなる輪読会 フィナーレ を開催します まだ枠が余っています。僕たちも楽しい会にできるように頑張りますので、来てもいいという方はぜひご応募ください、切実にお願いします。 Cnnpass: 「GitLabに学ぶ 世界最先端のリモート組織のつくりかた」そーだいなる輪読会 フィナーレ 開催日時: 2024-04-25(木) 18:00 ~ 21:00 (17:40 開場) 形式: オフライン 会場: KINTOテクノロジーズ株式会社(略称:KTC)室町オフィス 「** GitLabに学ぶ 世界最先端のリモート組織のつくりかた ドキュメントの活用でオフィスなしでも最大の成果を出すグローバル企業のしくみ **」の輪読会をすでにした方、現在進行形でしている方、そしてこれからしたい方向けの輪読会イベントとなっております。 イベントでもお会いできることを楽しみにしております♪ それでは!!
アバター
Introduction Hello! I am rioma from KINTO Technologies' Development Support Division. I usually work as a corporate engineer, maintaining and managing the IT systems used throughout the company. We recently held a study session in the form of case study presentations and roundtable discussions, specializing in the corporate IT area under the title " KINTO Technologies MeetUp! - Sharing Four Cases for Information Systems Professionals by Information Systems Professionals ." In this article, I will introduce the content of the case study presented at that study session, along with supplementary information. Why We Chose This Topic We had an idea to host our first ever study session, inviting professionals from outside our organization. Given that we had a project available that could serve as a presentation topic, I proposed focusing on the theme of transitioning to authentication infrastructures. Although the transition was technically from our sister company, KINTO, and their environment -not ours-, I believe I was able to provide a preliminary introduction to KINTO Technologies' corporate IT activities, encompassing our schedule and events occurring at that time. In this article, I will briefly supplement the information presented and provide a reintroduction of the content. Premise "Why was it necessary to switch to a new authentication infrastructure?" At KINTO, there were a lot of minor inconveniences and security issues surrounding the authentication infrastructure. After considering Microsoft Entra ID (formerly Azure AD), it appeared to be the optimal solution, so we decided to proceed with switching to Entra ID. Other reasons for this choice included the fact that KINTO Technologies' authentication infrastructure was already Entra ID, motivating us to implement it with the goal of enhancing collaboration, such as tenant integration, and the fact that we had the Microsoft E3 license but were not making the most of it. There were also significant advantages in terms of cost cutting. On Switching Over There were two considerations when making the switch. The first, to implement access policies which were previously controlled using certificates under similar conditions but without using certificates. The setting is based on conditional access, but as an overview, I was able to implement an access policy that is more robust and flexible than the existing one by combining settings such as "devices registered with MDM" as a condition and blocking any non-matching attribute values. The second is a specification that passwords for all accounts are reset when switching. The specification had quite a strong impact, and I had to find a way to respond to it without affecting all our internal members. To address this, I changed the passwords for all accounts following a forced system reset, based on certain rules. Since the change rules were known beforehand, and detailed procedures after logging in were also deployed, the login process posed no issues, and there were only a few inquiries. *In fact, most people were using PINs instead of passwords to log in, so the announcement about the changed password was not particularly meaningful; instead, it led to a bit of confusion. Trouble Surrounding Switching Work While it is one of the most common cases in the development of procedure manuals, I received numerous comments that the explanation of the procedure manual and work outline was difficult to understand. This was almost 100% my fault. I was overly focused on explaining the risks and effects of such a big-impact work as switching authorization infrastructures, choosing words and explanations that proved challenging for those less familiar with such systems. As mentioned above, I only realized just before the switchover that one of the PC login patterns after the switchover was to log in with PINs. The login instruction that had been developed in advance of the switchover did not mention this at all. This created a puzzling situation for those accustomed to logging in with PINs. Fortunately, I managed to fix the materials on the last day and the day of the switchover, but the repeated revisions and reissues were time consuming and confusing. In addition, I noticed a design error during the switchover, and had to take the slightly unreasonable response of implementing the switchover procedure while correcting the design and procedure manual. I was relieved that the issue was not a fundamental part of the project. Please see the attached slides as there are more issues. Conclusion While there were some minor inquiries and suggestions, no critical issues such as extended downtime preventing work for extended periods occurred. Consequently, the switchover of the authentication infrastructure was successfully implemented. Later, I was very happy to hear from internal staff who said, "It's amazing that there were few inquiries or business impact despite the scale of the transition." I will continue to implement system changes/transitions in the future, and aim to better our internal environment based on this experience.
アバター
Introduction Hello, I am Ki-yuno, in charge of front-end development for KINTO FACTORY. In this article, I would like to outline the process of setting up a debugging environment for React projects in Visual Studio Code (hereafter referred to as VS Code). I have only used VS Code as super notepad, so I struggled with it quite a bit (mainly due to the language barrier). For those of you who are about to build a debugging environment in VS Code, feel free to explore beyond the challenges I faced. Good luck! Environment Information OS : macOS Sonoma 14.1.2 VS Code : ver1.85.1 Node.js : 18.15.0 terminal : zsh Development Procedure 1. Setup launch.json Add launch.json to build a debugger launch configuration. Select the "Run and Debug" in the left side menu of VS Code. Clicking "Create a launch.json file" after making your selection will create a launch.json file in your project. :::message When setting it up for the first time, the debugger need to be selected. I will select Node.js since this is for React. ::: Creating a launch.json file Immediately after creation, a default launch configuration is added to launch.json . A default launch configuration has been added immediately after auto-generation 2. Add a New Launch Configuration Add a debugger launch configuration to the launch.json you just added. The launch configuration to be added is as follows: { "name": "[localhost]Chromedebug", "type": "node-terminal", "request": "launch", "command": "npm run dev", "serverReadyAction": { "pattern": "started server on .+, url: (https?://.+)", "uriFormat": "%s", "action": "debugWithChrome" }, "sourceMaps":true, "trace": true, "sourceMapPathOverrides": { "webpack:///./*": "${webRoot}/src/*" } } The edited launch.json will look like the image below. I deleted the default launch configuration, but there is no problem to leave it as is. You can also edit the command property if you want to change the command at startup, or add tasks with the preLaunchTask property if you want to run multiple commands when debugging (I'll not go into details about this this time). name The property value will be the name of the launch configuration 3. Start Debugging All you have to do is press F5 and the debugger will start. When the debugger is started successfully, a debugging button will appear at the top center. You can use the debugging button at the top center or the F key to execute actions such as step in, step over, etc. When in Trouble Here are the specific challenges that I encountered. I hope this helps those who have run into similar issues. ◽ The debugging terminal displays ‘sh-3.2$’, and upon execution, it returns ‘npm command not found’ Restarting VS Code solves this problem. Apparently this occurs when Microsoft auto-launches VS Code. In my environment, I log into Microsoft 365 when my PC starts, and I encountered this problem when VS Code auto-launched upon login. ◽ Despite npm being installed, it continues to return 'npm command not found' even when debugging starts Add the following to the .vscode/settings.json file. If you have not created the file itself, please do so first. { // npm scripts Path setting for executing commands "terminal.integrated.profiles.osx": { "zsh": { "path": "/bin/zsh", "args": ["-l", "-i"] } } } :::message If the terminal execution environment is bash, change the property name and path from zsh to bash ::: This enables the path to pass through to the debugger's execution terminal so that npm commands can be executed. Summary In this article, I summarized how to build an environment for debugging React projects in VS Code. I think we can still play around more make our VS Code workflow more efficient, so I hope next time we can further improve it with tasks.json . In my opinion, being able to debug during development significantly increases QOL (quality of life) and boosts development productivity. As an additional effect, this could contribute to a more positive atmosphere in the office and increased the level of happiness during commuting... maybe? Best wishes for your debugging life. Thank you for reading! Lastly, KINTO FACTORY, where I belong, is looking for people to work with. If you are interested, please check out the job openings below! @ card @ card
アバター
はじめに こんにちは、テックブログをお読みいただいてるみなさん。 最近、Marketing Cloudを導入することになり、その中で「 のりかえGO メール配信」を行うために、単発のメール送信よりも、Journeyを作成して自動化処理をトリガーにしたいと考えています。 Journeyとは、顧客がある一定の行動を起こした際に、自動的に複数のマーケティング手法を展開する仕組みです。例えば、顧客がメール内の特定のリンクをクリックしたら、その後に関連する情報を自動的に配信するなど、自動化されたマーケティングの一環です。 残念ながら、Automation StudioでJourneyをアクティビティとして追加する方法が見つからず、悩んでいました。そこで、いろいろ調査した結果をブログにまとめてみました。 メール配信の相棒 Journey Builderを使用する理由はいくつかあります: ・分岐、ランダム性、エンゲージメントの活用が可能 ・Salesforceとの連携が可能。例えば、タスクやケースの作成、オブジェクトの更新など ただし、Journey BuilderではスクリプトやSQLクエリの実行ができません。例えば、大量のメールを送信する前に同期済みデータソースをマージする必要がある場合などです。そのような場合は、Automation Studioでこれらのアクティビティが完了した後にJourney Builderを呼び出す必要があります。 したがって、Automation StudioとJourney Builderを統合して、メールを配信するのが望ましいですね。その設定方法を一緒に見ていきましょう。 設定まわり Automationを作成して、開始ソースとしてスケジュールを追加します。スケジュールを将来の時間に設定して保存します。保存しないと後続設定がうまく出来ないので、忘れないでくださいね。 アクティビティ(SQLクエリやフィルターなど)を追加します。これはJourneyと連携するにあたって必須です。データエクステンションが選択されていない場合、Journeyをトリガーすることができません。 Journeyを作成します。エントリーソースとしてデータエクステンションを追加します。ステップ2で使用されているデータエクステンションを選択します。これが大事です。違うデータエクステンションを選択してしまうと、ステップ1のAutomationと連携できません。 ※この時点では、Journeyを保存してAutomationに戻っても、アクティビティからJourneyを選択できません。なぜなら、Automationのアクティビティには「Journey」という選択肢が存在しないからです。焦らずにお待ちください。今から、マジックをお見せします! ![Step3-2](/assets/blog/authors/Robb/20240319/03-2.png =300x) Journeyで、キャンバスの下に表示されている「スケジュール」をクリックして、スケジュールの種別で「オートメーション」を選択した後、「選択」をクリックします。「オートメーション」が非活性で選べない?ステップ1に戻ってAutomationを保存してみたら? 「スケジュールのサマリー」で「スケジュールの設定」をクリックして、ステップ1で作成したAutomationを選択します。 連絡先の評価を編集して、Journeyで処理するレコードを指定します。 メールやフローコントロールなどを追加します。 準備完了!Journeyを検証して、アクティベートしましょう。ここでアクティベートしてもすぐにメールを飛ばさないので、心配はいりません。送信タイミングはAutomationに依存していますからね。 Automationに戻って、Journeyが勝手にAutomationに追加されましたよね。凄いと思わないですか? 最後に、Automationを思い切ってアクティブしましょう。Automationがトリガーされるたびに、Journeyもトリガーされるようになっています! お疲れ様です。ここで、コーヒーを飲みながら一息入れることにします。皆さんもお気に入りの飲み物でリフレッシュして、メール自動配信の楽しみを味わってみてくださいね。 それでは、良いマーケティングを! 出典: https://www.softwebsolutions.com/resources/salesforce-integration-with-marketing-automation.html
アバター
初めに こんにちは、グローバル開発グループでID Platformを開発しているリョウです。2024/01/19に渋谷ストリームホールで OpenID Summit に参加しましたので、感想と自分が見つけた面白かったポイントを共有する為に本記事を執筆いたします。 コロナ禍でOpenID Summit Tokyo 2020から4年ぶりに開催されたので、当日はOpenIDに興味ある方々が大勢会場に集まり、非常に盛り上がっていました。 今回のトピックとしては、コロナを挟んだ4年間でデジタルIDがどんな変化をもたらしたか、そして将来的にデジタルアイデンティティが世の中でどんな発展を遂げそうか、といった内容でした。 プログラムの流れ 参加感想 OpenIDといえば、他の汎用性の高い技術領域と比べて知名度はあまりなく、聞いたことも無い方々が沢山いらっしゃると思いますが、今回のサミット会場に着いた際、多くの企業からOpenIDに関心のある方が集まっていたので、若干驚きました。 今回初めて参加する方もいるので、午前中は今までOpenIDが発展してきた歴史、OpenIDのデジタルアイデンティティと電子マネー面における今後の発展や展望、OpenIDファウンデーション・ジャパンワーキンググループにて行われている資料の翻訳や人材育成活動の詳細を中心にご紹介いただきました。 午前中発表された内容から、今後のOpenIDが発展していく方向は認証認可領域から身分証明(デジタルアイデンティティ)と電子マネー運用への変化だと深く感じました。 午後のTopicはOpenIDを実際導入運用の段階遭った問題や全体的なソリューション・対策に関して各会社から説明をいただきました。 ただ、午後のプログラムは2会場での開催だったため、 NARUTO のような多重分身術を持たない私は、同時に2会場に参加出来なく非常に残念です。 印象深い発表 OpenID Connect活用時のなりすまし攻撃対策の検討 発表者: 湯浅 潤樹 — 奈良先端科学技術大学 サイバーレジリエンス構成学研究室 紹介いただいた事例はなかなかレアですが、修士課程2年でOpenIDの運用実験をここまで深掘りされていることに驚きました。OpenIDの認証モードはいくつかありますが、使用例によっては安全性が低い場合もありますので、この発表のような特定ケースでリスクがある部分を今後の展開で注意しながら進めていくべきだと思いました。 メルカリアプリのアクセストークン 発表者: グエン・ザー — 株式会社メルカリ IDプラットフォームチーム ソフトウェアエンジニア メルカリアプリ は日本で非常に人気のある中古品販売プラットフォームとして有名です。そのIDプラットフォームエンジニアの方が、メルカリIDの運用上、古い手法でどんな困難があったのか、そしてユーザーがモバイルアプリやブラウザ上でスムーズにサービスを利用できるようになるまでの努力について説明いただきました。その中でも、ブラウザCookieの活用において多くの目標を達成してきたけれど、 Chrome ブラウザだけは特別な仕様で、 Cookie有効期間が400日 であることを知りました。我々もIDプラットフォームを立ち上げてからUI・UXに関しては様々なチャレンジや努力をしてきましたが、 Cookie有効期間は400日 といったことは初めて知りました。 JWTについて JWT(JSON Web Token)という名前はよく耳にするかと思いますが、認証・認可にあまり触れあわない方だとJWTの役割や、よく同時に出るJWK, JWS, JWE間の相互関係について触れる機会が無いかもしれないので、あらかじめ簡単に説明しましょう: JWTはネットワーク上で情報交換する際に、交換情報の信頼度を確保出来る 規範 となります。そのうち、JWSとJWEはJWTの規範の実現例となります。 JWS(JSON Web Signature)は下記図のように「.」に区切られて3つのパートとなります:**Header(認証方法記載),Payload(実際の情報),Signature(改竄されない保証)**。 JWSをbase64でエンコードされましたので、デコード出来たPayloadはすべての情報が開示されてます。 JWK(Json Web Key)は、JWTの説明と合わせてすると、JWTのHeaderに記載して方法でPayloadの内容のハッシュを暗号化してSignatureを生成する暗号化鍵となります。 JWE(JSON Web Encryption)は上記のJWSとクレべて、安全性と完全性を当時に守れるJWTの実現となります。そのため「.」で5つパートに分割されました、二番目はPayload専用の暗号が復号化鍵となります。暗号化鍵の所有者以外Payloadの内容を復号化は出来ません。 画像引用元 SD-JWT 今回のOpenIDサミットにてイタリアの電子マネー導入運用の実績を紹介いただきました。そこで SD-JWT という新概念について説明がありました。私自身初耳だったので、サミットが終了してから自分でも検索してみました。ここからがいよいよ本記事の主題です。私が検索したSD-JWTについて簡単に説明したいと思います。 Selective Disclosure JWT(SD-JWT)は名前の通り、選択的に開示するJWTというものです。JWSとJWEがすでに世の中に存在しているのに、どんな経緯でSD-JWTが設計されたのかを最初に説明します。 Payloadの内容開示については、現在以下の二つが存在します: 全部開示 :JWSをbase64で解析して、Payloadにあるすべての内容を誰でも見ることができます。 全部非開示:JWEの場合は復号化キーを持っている所有者以外、JWEのPayloadの内容を見れません。 ただ、一部の情報しか公開したくない場合には対応する案がありません。そのため、SD-JWTが世に生まれました。 例えば、電子ウォレットの所有者が10万円の商品を購入する際、商品の販売者が購入者の認証に使う一般的な属性情報(誕生日、住所、電話番号など)を見る必要がなく、購入者の電子ウォレット残高だけを見たい場合。購入者としても、すべての個人情報を全部公開せずに、残高やIDなどの必須情報だけを開示すれば購入できます。 これだけでは足りないかもしれませんが、JWSから所有者の必要な情報だけを事業者へ開示することで、ある程度は個人情報漏えい防止にも有効になります。 SD-JWT実現方法 従来のID token生成手順から着手します。 まずは、あるユーザーAさんの個人情報を以下のようにJSON形式で表示します。 { "sub": "cd48414c-381a-4b50-a935-858c1012daf0", "given_name": "jun", "family_name": "liang", "email": "jun.liang@example.com", "phone_number": "+81-080-123-4567", "address": { "street_address": "123-456", "locality": "shibuya", "region": "Tokyo", "country": "JP" }, "birthdate": "1989-01-01" } そして、発行者はそれぞれの属性情報に対してSD-JWT Salt(ランダム値)を付与します。 { "sd_release": { "sub": "[\"2GLC42sKQveCfGfryNRN9c\", \"cd48414c-381a-4b50-a935-858c1012daf0\"]", "given_name": "[\"eluV5Og3gSNII8EYnsxC_B\", \"jun\"]", "family_name": "[\"6Ij7tM-a5iVPGboS5tmvEA\", \"liang\"]", "email": "[\"eI8ZWm9QnKPpNPeNen3dhQ\", \"jun.liang@example.com\"]", "phone_number": "[\"Qg_O64zqAxe412a108iroA\", \"+81-080-123-4567\"]", "address": "[\"AJx-095VPrpTtM4QMOqROA\", {\"street_address\": \"123-456\", \"locality\": \"shibuya\", \"region\": \"Tokyo\", \"country\": \"JP\"}]", "birthdate": "[\"Pc33CK2LchcU_lHggv_ufQ\", \"1989-01-01\"]" } } 「_sd_alg」に記載されたハッシュ関数で「sd_release」のそれぞれの属性情報を計算して下記の「_sd」へ格納し、かつ発行者の署名鍵(cnf)、有効期間(ext)、発行時間(iat)を加えて新しいPayloadを作成できます。 Payloadに基づいて最新のTokenを発行すると、SD-JWTが作成できました。 { "kid": "tLD9eT6t2cvfFbpgL0o5j/OooTotmvRIw9kGXREjC7U=", "alg": "RS256" }. { "_sd": [ "5nXy0Z3QiEba1V1lJzeKhAOGQXFlKLIWCLlhf_O-cmo", "9gZhHAhV7LZnOFZq_q7Fh8rzdqrrNM-hRWsVOlW3nuw", "S-JPBSkvqliFv1__thuXt3IzX5B_ZXm4W2qs4BoNFrA", "bviw7pWAkbzI078ZNVa_eMZvk0tdPa5w2o9R3Zycjo4", "o-LBCDrFF6tC9ew1vAlUmw6Y30CHZF5jOUFhpx5mogI", "pzkHIM9sv7oZH6YKDsRqNgFGLpEKIj3c5G6UKaTsAjQ", "rnAzCT6DTy4TsX9QCDv2wwAE4Ze20uRigtVNQkA52X0" ], "iss": "https://example.com/issuer", "iat": 1706075413, "exp": 1735689661, "_sd_alg": "sha-256", "cnf": { "jwk": { "kty": "EC", "crv": "P-256", "x": "SVqB4JcUD6lsfvqMr-OKUNUphdNn64Eay60978ZlL74", "y": "lf0u0pMj4lGAzZix5u4Cm5CMQIgMNpkwy163wtKYVKI", "d": "0g5vAEKzugrXaRbgKG0Tj2qJ5lMP4Bezds1_sTybkfk" } } }. { シグネチャー 発行者は公開鍵でPayloadのシグネチャーを計算してここに置く、Payloadの内容を改竄されない事が保証できる } 「sd_release」と「_sd」それぞれの属性とハッシュ値の順番は守らなくても大丈夫です。 SD-JWT利用方法 発行者はSD-JWTと「sd_release」を一緒に所有者へ送ります。 所有者は使う場面によって、開示したい属性情報とSD-JWTを同時に提出すると、安全性と完全性を守る前提で認証ができます。 "email": "[\"eI8ZWm9QnKPpNPeNen3dhQ\", \"jun.liang@example.com\"]", メールアドレスだけを開示したいのであれば、上記の部分とSD-JWTを一緒に提出することになります。 検証者は以下の2点を確認することでメールの正確性が確認できます  emailの部分をハッシュ関数で計算する結果と「_sd」一覧にある "5nXy0Z3QiEba1V1lJzeKhAOGQXFlKLIWCLlhf_O-cmo" が一致すること PayloadのSignature再計算結果とSD-JWTに有るSignatureが一致すること(Payloadが改ざんされていない) まとめ 今回のサミットへ参加することによってOpenIDが発展してきた歴史や、今後の展開について理解できました。また、これまで我々IDチームが使っていたJWTと違う形式であるSD-JWTについても知ることができました。 面白い話がたくさんありましたので、普段はID分野に携わっていない方々へもぜひご参加をおすすめします。 今後、KINTOテクノロジーズとしても登壇できるようになることを心待ちにしています。 レファレンス OpenID Summit Tokyo 2024
アバター
Introduction This article is about the creation process behind our mascot in Japan. This initial phase recounts the journey since we received the character creation request until we gave it form. Hello, I am Sugimoto from the Creative Office. To explain our team briefly, the Creative Office is in charge of overseeing the communication with customers of the KINTO Japan vehicle subscription service, and using their feedback for planning and generating outputs. To elaborate a little, we are in charge of understanding the communication issues existing on the project side (We have the power to do this because we develop in-house!), coming up with tangible solutions through them, and communicating all messages from the business side in a consistent manner (= branding). With the above in mind, I will talk about how we created the KINTO mascot by us leveraging our circumstances and knowledge. Keep in mind that this of course is not a guide on how to make cool characters. I hope you read this with a light heart, as a mother would with her child’s health journal, or maybe more like reading your illustrated diary when you were in elementary school, with your exciting summer vacation project. Embarking on the Character's Origin Story The creation of our official mascot kicked off in November 2021. I was the Creative Director (CD), another team member of the Creative Office was the Art Director (AD), and we were joined by members of the PR and Branding Team from the Marketing Planning Division. Together, we formed the “mascot character project” (hereinafter referred to as PJ). While calling it a project might sound grandiose, our intention was to start small and grow it gradually. Our aim was to appeal to young people and women to counteract the trend of them falling out of love with vehicles. Initially, it was not conceived as corporate branding project with corporate commercials and posters right off the bat. That said, all of the members were eager to create and raise their own “child” who would be loved by everyone. Although it was not an official one, we already had an illustration of a character on the service's website. Everyone in the company called him Kuroko-kun. Kuroko in Japanese refers to stage assistants in traditional theater plays. First, we had a meeting to discuss how to use Kuroko-kun. Kuroko-kun would appear throughout the KINTO website and quietly support people who are thinking about owning a vehicle or have concerns about driving, but does so from behind the scenes. Although we were attached to Kuroko-kun, many of us thought that it would be better to start from scratch and create a new KINTO character, so the new goal of the project shifted to being about creating one. Kuroko-kun Works Hard! Where Did We Start? First, we decided to make our purpose clear when developing our new character. [Aim: To turn consumers into fans of KINTO]. We wanted people who have never purchased a vehicle or are not yet interested in vehicles to learn how enjoyable having a car can be. We thought it would be best to have a character to accompany you in the joy of driving, rather than a character that teaches you about driving. Instead of going straight to an illustrator, we decided to let employees get involved and come up with their own character ideas. This had the added benefit of working as internal branding. We expected the engineers and business team members who were involved in creating the service to come up with rich ideas from an inside perspective of the company, and we were excited and nervous about the feedback we would get. During this proposal period, the team came up with character ideas to create a basis for characters to pick. For these ideas we gathered, we decided on the direction of the character using the below three steps. 1. Check that the character follows an archetype This was the first step of the output process, and the Creative Office took the lead. KINTO Japan has its brand personality, which I will talk about another time. It personifies the brand and describes what elements and personality it has (an archetype). The personality elements that make up the KINTO brand in Japan, or KINTO-san if you will, are the Explorer which seeks a free driving style, the Every Person which is familiar and has the ability to empathize with others, the humorous Jester, and the Sage that gives others specialized knowledge. At first it may sound all over the place, but it all comes together if you see KINTO's personality as someone curious, who likes to entertain people around them, and wants to be useful with their knowledge. Of course, the new character would be a major representation of the brand, so we decided to make sure its concept matched its personality. 2. Character characteristics and motifs We organized our character ideas into “roles/attributes” and “motifs”, and we extracted the characteristics. These are some of the ideas 3. Making character ideas that could solve our problem We divided the characteristics and motifs we organized in point 2. into four directions. A: A "character that represents the very DNA of KINTO" that represents the fun and freedom of driving and tells the brand story B: An approachable "character that represents friendliness toward drivers" C: A "character that symbolizes the elements of freedom and moving forward" D: A “character that embodies innovation and intelligence” Lovable Characters Gathered Throughout the Company We put up posters throughout the company asking for submissions. While the PJ members discussed the character concept, we collected 24 character proposals from volunteers from October to November 2022. During the screening, we did a survey throughout the company asking, "Which character do you like and think fits KINTO?" Incidentally, during the process of making the mascot character, we wanted to use as many of the KINTO and KTC employees' hopes and wishes as we could, and we collected a good amount of survey responses. Thinking about it now, I think the employees were already starting to get attached to the characters at the time. Getting back on topic, the results of the initial survey showed that the idea with the "cloud" motif was popular, and clouds were also related to the origin of the company's name, KINTO. So, we decided to go in the direction of a cloud character. From there, we compared the opinions of the judges (the project members) with the responses to the survey, found similarities and differences, and summarized what elements to add to the character. This was the core part of deciding what kind of appearance and characteristics we wanted our child to have. I will end today's article here. In the next article I will tell you more on how our character took shape!
アバター
はじめに こんにちは。 KINTOテクノロジーズ モバイルアプリ開発グループの中口です。 KINTOかんたん申し込みアプリ のiOSチーム(以下、iOSチーム)でチームリーダーをやっています。 不定期に振り返り会を行うのですが、振り返り会ってすごく難しいですよね。。。 みんなの本音を引き出せているんだろうか?? チームが抱える本当の課題はなんだろう?? 自分のファシリテーションがうまくできているだろうか?? 理由を挙げ出せばキリがないです。 先日 クラスメソッド社が開催したこちらのwebセミナー を視聴したのですが、 その中で「自走するチームをどのように作るか」というセッションがあり非常に感銘を受けました。 セッション内で紹介されたクラスメソッド社が提供する振り返り会の体験会をぜひ受けてみたいと思い申し込んでみました。 本記事ではその様子をご紹介いたします。 事前ヒアリング 振り返り会実施の前にクラスメソッド阿部様、高柳様とお打ち合わせをさせていただきました。 チーム状況に最適な振り返り会を実施するためiOSチームの現状を1時間近くヒアリングいただきました。 振り返り会の全体像 振り返り会当日は高柳様、伊藤様の2名にお越しいただきファシリテーションをしていただきました。 約2時間の振り返りなのですが会の大まかな流れとして、 参加者全員の自己紹介 振り返り会の目的をみんなで認識合わせ 「チームをもう少し良くするために」を個人ワークで考える 同じ内容をペアワークで考える チーム全体共有 具体的なアクションプランをペアワークで考える チーム全体共有 クロージング という流れで進みました。 前半戦 約2時間の振り返りでしたが、注目すべきはおよそ半分の時間を 「1. 参加者全員の自己紹介」と「2.振り返り会の目的をみんなで認識合わせ」に使ったことでした。 「1. 参加者全員の自己紹介」ではファシリテーターから【名前やニックネーム】、【チームでの役割】、【このメンバーで会話が多い、または少ない人】などの質問を受けました。 そこでチームの雰囲気や各個人の性格を見るだけで無く、メンバー間の関係性や相性を見極められていたようです。 「2.振り返り会の目的をみんなで認識合わせ」に関しては、 私の方からご要望を出していた今のチームを もう少し良くする ためには何ができるか、という内容でチーム合意を得ました。 現在のチームは、大きなリリースを昨年9月に終え現在は機能改善やリファクタリングなどのタスクが中心のため、状況が落ち着いているのですが、こういったチームが もう少し良くする ということを実践することは結構難しかったりするようです。 また、この会の主催者(私)が参加者にどういった目的(役割や期待していること)でこの会に招待しているか、を 一人一人に 伝えました。 こうすることで、参加者は自分が何を発言したらいいのか明確になり発言しやすくなる、という効果があるようです。 私自身も普段なかなかタイミングが無かったり、直接言うことが照れくさいような話を伝えることができ良い機会だったと思います。 この、前半戦に時間を割くことで会の参加者全員が発言をしやすい雰囲気が作られ、ラポール形成が大きく進んだと実感しました。 ファシリテーションの様子 後半戦 「3. 「チームをもう少し良くするために」を個人ワークで考える」以降はワークで進みます。 ただし、いわゆる振り返りのフレームワークなどは使わず、 どんなことがあればチームを もう少し良くする ことができるだろうを付箋に書き出す、 というシンプルな作業を繰り返しました。 個人ワークを行い、次に2人1組のペアワークを行いました。 ペアワークが向いているケースと向いていないケースがあるようで、このチームはペアワークが向いているとのことです。 また、どういった2人1組の組み合わせにするかは重要であり心理的な負担が発生してしまうような組み合わせは作らないようにすることがポイントだそうです。 ペアワークの様子 その後、各自発表するのですが、これまで私が行ってきた振り返り会では引き出せないような意見がたくさん出ました。 前半戦でのラポール形成やペアワークによって引き出すことができたと実感しました。 そして、ここまで出た意見をもとに具体的にどういったアクションに落とし込むかを 「6. 具体的なアクションプランをペアワークで考える」で行い再度発表しました。 発表の様子 結果として実践することが決まったアクションとしては下記となりました。 雑談チャンネルを作る みんなが自由な雑談できる場 週に一度雑談に特化したミーティングを設ける 個人的な話をすることで、より信頼構築が進む可能性があると思いパブリックではなくプライベートチャンネルを作る ミーティングはなるべく会議室に来る(出社していても自席からオンライン参加の人が多かったため) 担当タスクに関して方針相談会を設ける タスクチケットに期限を明記する これらに関しては、早速翌日からできることに取り組んでいます。 クロージング 会の最後には、会議の時間配分や人の特徴を理解した上で意見を振るなどの会議をデザインすることの重要性を高柳様からお話をいただきました。 特に今回の振り返り会では、途中でペアワークを多用したなど 人 にフォーカスしたことでそのような進め方を実践されたとのことでした。 クロージングの様子 実施後アンケートの結果 実施後アンケートを行いまして、そのサマリーを掲載いたします。 (回答数は10名です) 期待値の変化 実施前:6.3→実施後:9 NPS 80 (NPSとは?) 「”参加した後”の満足度について、その理由を教えてください(フリーテキスト)」のAI要約 アンケートの結果から、参加者は会議の進行やファシリテーターの解説に満足していることがわかります。 また、具体的なアクションを決定し、それが次のアクションにつながったことに対する肯定的な意見が多く見られました。 さらに、チームメンバーの思考を理解する機会があったことや、普段聞けない話が聞けたことも評価されています。 これらの結果から、この会議は参加者にとって有意義な時間であったと言えます。 0を超えることだけでもすごいNPSが脅威の80でした! 感想 今回の振り返り会を通して、コミュニケーション不足を感じているメンバーが多かったことに気づくことができ、そこにフォーカスしたネクストアクションへ落とし込むことができたため大変充実した振り返り会となりました。アンケート結果から参加メンバーも満足していたことがわかり嬉しかったです。 また、会議のファシリテーターというのはものすごく重要な役割なんだなということをまざまざと実感しました。 このスキルは一朝一夕では身につかないとても高度なスキルであるとともに、組織としてこういった人材の育成・獲得には力を入れるべきだと思いました。 まずは私自身がファシリテーションの勉強していき、より良い会議が実施できるようになりたいと思います。
アバター
はじめに こんにちは。 KINTOテクノロジーズ モバイルアプリ開発グループの中口です。 KINTOかんたん申し込みアプリ のiOSチーム(以下、iOSチーム)でチームリーダーをやっています。 不定期に振り返り会を行うのですが、振り返り会ってすごく難しいですよね。。。 みんなの本音を引き出せているんだろうか?? チームが抱える本当の課題はなんだろう?? 自分のファシリテーションがうまくできているだろうか?? 理由を挙げ出せばキリがないです。 先日 クラスメソッド社が開催したこちらのwebセミナー を視聴したのですが、 その中で「自走するチームをどのように作るか」というセッションがあり非常に感銘を受けました。 セッション内で紹介されたクラスメソッド社が提供する振り返り会の体験会をぜひ受けてみたいと思い申し込んでみました。 本記事ではその様子をご紹介いたします。 事前ヒアリング 振り返り会実施の前にクラスメソッド阿部様、高柳様とお打ち合わせをさせていただきました。 チーム状況に最適な振り返り会を実施するためiOSチームの現状を1時間近くヒアリングいただきました。 振り返り会の全体像 振り返り会当日は高柳様、伊藤様の2名にお越しいただきファシリテーションをしていただきました。 約2時間の振り返りなのですが会の大まかな流れとして、 参加者全員の自己紹介 振り返り会の目的をみんなで認識合わせ 「チームをもう少し良くするために」を個人ワークで考える 同じ内容をペアワークで考える チーム全体共有 具体的なアクションプランをペアワークで考える チーム全体共有 クロージング という流れで進みました。 前半戦 約2時間の振り返りでしたが、注目すべきはおよそ半分の時間を 「1. 参加者全員の自己紹介」と「2.振り返り会の目的をみんなで認識合わせ」に使ったことでした。 「1. 参加者全員の自己紹介」ではファシリテーターから【名前やニックネーム】、【チームでの役割】、【このメンバーで会話が多い、または少ない人】などの質問を受けました。 そこでチームの雰囲気や各個人の性格を見るだけで無く、メンバー間の関係性や相性を見極められていたようです。 「2.振り返り会の目的をみんなで認識合わせ」に関しては、 私の方からご要望を出していた今のチームを もう少し良くする ためには何ができるか、という内容でチーム合意を得ました。 現在のチームは、大きなリリースを昨年9月に終え現在は機能改善やリファクタリングなどのタスクが中心のため、状況が落ち着いているのですが、こういったチームが もう少し良くする ということを実践することは結構難しかったりするようです。 また、この会の主催者(私)が参加者にどういった目的(役割や期待していること)でこの会に招待しているか、を 一人一人に 伝えました。 こうすることで、参加者は自分が何を発言したらいいのか明確になり発言しやすくなる、という効果があるようです。 私自身も普段なかなかタイミングが無かったり、直接言うことが照れくさいような話を伝えることができ良い機会だったと思います。 この、前半戦に時間を割くことで会の参加者全員が発言をしやすい雰囲気が作られ、ラポール形成が大きく進んだと実感しました。 ファシリテーションの様子 後半戦 「3. 「チームをもう少し良くするために」を個人ワークで考える」以降はワークで進みます。 ただし、いわゆる振り返りのフレームワークなどは使わず、 どんなことがあればチームを もう少し良くする ことができるだろうを付箋に書き出す、 というシンプルな作業を繰り返しました。 個人ワークを行い、次に2人1組のペアワークを行いました。 ペアワークが向いているケースと向いていないケースがあるようで、このチームはペアワークが向いているとのことです。 また、どういった2人1組の組み合わせにするかは重要であり心理的な負担が発生してしまうような組み合わせは作らないようにすることがポイントだそうです。 ペアワークの様子 その後、各自発表するのですが、これまで私が行ってきた振り返り会では引き出せないような意見がたくさん出ました。 前半戦でのラポール形成やペアワークによって引き出すことができたと実感しました。 そして、ここまで出た意見をもとに具体的にどういったアクションに落とし込むかを 「6. 具体的なアクションプランをペアワークで考える」で行い再度発表しました。 発表の様子 結果として実践することが決まったアクションとしては下記となりました。 雑談チャンネルを作る みんなが自由な雑談できる場 週に一度雑談に特化したミーティングを設ける 個人的な話をすることで、より信頼構築が進む可能性があると思いパブリックではなくプライベートチャンネルを作る ミーティングはなるべく会議室に来る(出社していても自席からオンライン参加の人が多かったため) 担当タスクに関して方針相談会を設ける タスクチケットに期限を明記する これらに関しては、早速翌日からできることに取り組んでいます。 クロージング 会の最後には、会議の時間配分や人の特徴を理解した上で意見を振るなどの会議をデザインすることの重要性を高柳様からお話をいただきました。 特に今回の振り返り会では、途中でペアワークを多用したなど 人 にフォーカスしたことでそのような進め方を実践されたとのことでした。 クロージングの様子 実施後アンケートの結果 実施後アンケートを行いまして、そのサマリーを掲載いたします。 (回答数は10名です) 期待値の変化 実施前:6.3→実施後:9 NPS 80 (NPSとは?) 「”参加した後”の満足度について、その理由を教えてください(フリーテキスト)」のAI要約 アンケートの結果から、参加者は会議の進行やファシリテーターの解説に満足していることがわかります。 また、具体的なアクションを決定し、それが次のアクションにつながったことに対する肯定的な意見が多く見られました。 さらに、チームメンバーの思考を理解する機会があったことや、普段聞けない話が聞けたことも評価されています。 これらの結果から、この会議は参加者にとって有意義な時間であったと言えます。 0を超えることだけでもすごいNPSが脅威の80でした! 感想 今回の振り返り会を通して、コミュニケーション不足を感じているメンバーが多かったことに気づくことができ、そこにフォーカスしたネクストアクションへ落とし込むことができたため大変充実した振り返り会となりました。アンケート結果から参加メンバーも満足していたことがわかり嬉しかったです。 また、会議のファシリテーターというのはものすごく重要な役割なんだなということをまざまざと実感しました。 このスキルは一朝一夕では身につかないとても高度なスキルであるとともに、組織としてこういった人材の育成・獲得には力を入れるべきだと思いました。 まずは私自身がファシリテーションの勉強していき、より良い会議が実施できるようになりたいと思います。
アバター
Self-introduction Nice to meet you! I am Romie, developing the Android version of the my route app at the Mobile App Development Group. It's been two years since I began Android development during my previous job, where I mainly implemented all layouts in xml format, even for personal development. I must admit, I feel a bit embarrassed to say that I only started delving into Compose properly after joining KINTO Technologies in December 2023. And this is also my first time writing an article on this Tech Blog! Target Audience of This Article This article is intended: for absolute beginners in Android development for those who have only written layout design in xml format for any reasons, and have no prior knowledge about Compose for those who are having troubles displaying corrected components while testing on actual devices My Encounter with Preview The first time I joined this company I did not know anything about Compose, so I had to re-implement the following screen from xml format to Compose. ![About my route screen](/assets/blog/authors/romie/2024-02-08-compose-preview-beginner/03.png =200x) About my route screen As soon as I started code reading, I found a mysterious function named Preview. @Preview @Composable private fun PreviewAccountCenter() { SampleAppTheme { AccountCenter() } } The aim of this function was to display the preview of the account center button. However, since the function was not called anywhere in the same .kt file despite being a private function, I assumed that it was not used and decided to proceed with the implementation without making changes to any Preview-related code. After successfully completing the Compose process and creating a pull request, I received the following comment: "There is no Preview here, please add one!" I thought to myself, 'What's the point of creating a function that isn't called?' I then imitated what others built to add a preview of the entire screen. I did confirm that the actual device was working without problems, but I was only looking at the actual device back then. While still wondering what Preview was for, I looked at the Split screen of Android Studio. It was then that I realized the exact screen displayed on the physical device was also visible within Android Studio! So that's what Preview is for, I thought to myself; to display it on the Split screen without having to call the function! It is also written in its official document[^1]. There is no need to deploy the app to a device or emulator. You can preview multiple specific composable functions, each with different width and height limits, font scaling, and themes. As you develop the app, the preview is updated, allowing you to quickly see your changes. [^1]:Reference: Android for Developers Preview and Operation Check One day, I was working on enhancing the route details screen of the 'My Route' app, adding images and text for the 8 directions of travel section starting from the station exit. The implementation itself was done immediately, but the problem was to check the operation. It takes a lot of time to check whether images and wording for 8 directions are added correctly. The steps to reproduce are as follows. ![Steps to reproduce the direction section display](/assets/blog/authors/romie/2024-02-08-compose-preview-beginner/01.gif =150x) Steps to reproduce the direction section display So, how can we efficiently check all 8 directions? Furthermore, if the UI display is disrupted or incorrect images are shown, it requires additional time to rectify and verify it from the beginning. This is where Preview comes into play. It is implemented as follows. @Preview @Composable fun PreviewWalkRoute() { SampleAppTheme { Surface { WalkRoute( routeDetail = RouteDetail(), point = Point( pointNo = "0", distance = "200", direction = Point.Direction.FORWARD, ), ) } } } If you view the Split screen in Android Studio through Build, this is what you can see. Direction section preview screen Just enter the direction you want to verify, and you can confirm whether the correct image and text are present. Also, it’s fine to just test only one scenario to determine if the display is broken! This approach significantly reduces the time needed for testing. Conclusion I am sure that I will continue to know more of the various aspects of Compose, including Preview, as I progress in the future. For now, I simply wanted to share my brief experience with you, illustrating how a Compose beginner was amazed by the capabilities of the preview function. Other than that, I hope you look forward to more on the journey of this newbie.
アバター
Hey, I found a bunch of NotFound error events in AWS CloudTrail! Hello. I am Kurihara from the Cloud Center of Excellence (CCoE) team at KINTO Technologies, who couldn't dislike alcohol even after watching the Japanese series "Drinking Habit 50." As Tada from my team previously introduced CCoE Activities and Providing Google Cloud Security Preset Environments , we are working every day to keep our cloud environment secure. While analyzing AWS CloudTrail logs to check the AWS health of our account, I noticed that there were a lot of NotFound-type errors on a regular basis. This may sound boring, but if you are an AWS user, chances are you have encountered the same event. Despite searching extensively on Google, I couldn't find any relevant information, so I decided to document my investigation through a blog post. Conclusion Overall, when analyzing AWS CloudTrail, NotFound-type errors via the service link crawl in the AWS Config recorder should be excluded and analyzed. Error events inevitably occur due to the behavior of AWS Config, so they should be properly filtered to reduce analysis noise. Details of Investigation KINTO Technologies has a multi-account configuration where Landing Zones are managed in AWS Control Tower in accordance with best practices for AWS multi-account management . Therefore, AWS Config manages configuration information and AWS CloudTrail manages audit logs. While analyzing AWS CloudTrail logs to check the AWS health of our account, I found that NotFound-type error events were occurring in large numbers and on a regular basis. Here are the results of the AWS Athena analysis of CloudTrail logs for about a month from a certain AWS account. This account is issued with minimal security settings and no workload has been built. -- Analyze the top of errorCode WITH filterd AS ( SELECT * FROM cloudtrail_logs WHERE errorCode IS NOT NULL ) SELECT errorCode, count(errorcode) as eventCount, count(errorCode) * 100 / (select count(*) from filterd) as errorRate FROM filterd GROUP BY errorCode eventCount errorRate ResourceNotFoundException 1,515 18 ReplicationConfigurationNotFoundError 1,112 13 ObjectLockConfigurationNotFoundError 958 11 NoSuchWebsiteConfiguration 954 11 NoSuchCORSConfiguration 952 11 InvalidRequestException 627 7 Client.RequestLimitExceeded 609 7 -- Check the frequency of occurrence of a specific erroCode SELECT date(from_iso8601_timestamp(eventtime)) as "date" count(*) as count FROM cloudtrail_logs WHERE errorcode = 'ResourceNotFoundException' GROUP BY date(from_iso8601_timestamp(eventtime)) ORDER BY "date" ASC LIMIT 5 date count 2023-10-19 52 2023-10-20 80 2023-10-21 80 2023-10-22 80 2023-10-23 80 I picked up a few error codes and looked at the AWS CloudTrail records (the actual AWS CloudTrail logs are listed at the end of this article) and found that all of them were recorded in the arn field of the userIdentity that was the access source as arn:aws:sts::${AWS_ACCOUNT_ID}:assumed-role/AWSServiceRoleForConfig/${SESSION_NAME} . This is the Service-Linked Roles attached to AWS Config. I could not figure out why NotFound would occur even though the target resource exists, but when I checked the eventName section, I realized that it is not an API to get configuration information of the resource itself, but rather for each of its dependent resources. Resource errorCode API that was called (eventName) Lambda ResourceNotFoundException GetPolicy20150331v2 S3 ReplicationConfigurationNotFoundError GetBucketReplication S3 NoSuchCORSConfiguration GetBucketCors Although it is not an error that affects the workload, we would like to eliminate it as it is noise in general monitoring and troubleshooting. To do so, we need to take non-essential actions such as "configure something in the related resource" (for example, adding a Lambda resource-based policy that allows InvokeFunction actions only from its own account). We came to the corresponding conclusion that our CCoE team excludes access from the AWS Config service-linked role when analyzing AWS CloudTrail. If you analyze with AWS Athena, it is an image of executing the following query. SELECT * FROM cloudtrail_logs WHERE userIdentity.arn not like '%AWSServiceRoleForConfig%' A Brief Deep Dive I will delve a bit further into the process of recording configuration information in AWS Config, based on insights gained during this investigation. There are two points that are not explicitly stated in the official documentation, but were found in this investigation. Dependent (supplemental) resources (I named it myself) recording behavior Frequency of recording dependent (supplemental) resources Dependent (supplemental) resource recording behavior AWS Config not only records configuration information of the resource itself, but also related resources (relationships). They are named direct relationship and indirect relationship . AWS Config derives the relationships for most resource types from the configuration field, which are called "direct" relationships. A direct relationship is a one-way connection (A→B) between a resource (A) and another resource (B), typically obtained from the describe API response of resource (A). In the past, for some resource types that AWS Config initially supported, it also captured relationships from the configurations of other resources, creating "indirect" relationships that are bidirectional (B→A). For example, the relationship between an Amazon EC2 instance and its security group is direct because the security groups are included in the describe API response for the Amazon EC2 instance. On the other hand, the relationship between a security group and an Amazon EC2 instance is indirect because describing a security group does not return any information about the instances it is associated with. As a result, when a resource configuration change is detected, AWS Config not only creates a CI for that resource, but also generates CIs for any related resources, including those with indirect relationships. For example, when AWS Config detects changes in an Amazon EC2 instance, it creates a CI for the instance and a CI for the security group that is associated with the instance. -- https://docs.aws.amazon.com/config/latest/developerguide/faq.html#faq-1 There are resources, which I name them on my own, dependent (supplemental) resource , that are separate from related resources and appear to be settings for the resource itself, but they also have separate acquisition APIs. In the case of Lambda, Lambda itself is a resource that can be obtained with GetFunction , whereas resource-based policy is another resource that can be obtained with GetPolicy . Looking at the Configuration Item (CI), the resource-based policy that is a dependent (supplemental) resource, is recorded in the supplementaryConfiguration field as follows: { "version": "1.3", "accountId": "<$AWS_ACCOUNT_ID>", "configurationItemCaptureTime": "2023-12-15T09:52:19.238Z", "configurationItemStatus": "OK", "configurationStateId": "************", "configurationItemMD5Hash": "", "arn": "arn:aws:lambda:ap-northeast-1:<$AWS_ACCOUNT_ID>:function:check-config-behavior", "resourceType": "AWS::Lambda::Function", "resourceId": "check-config-behavior", "resourceName": "check-config-behavior", "awsRegion": "ap-northeast-1", "availabilityZone": "Not Applicable", "tags": { "Purpose": "investigate" }, "relatedEvents": [], # Related resources "relationships": [ { "resourceType": "AWS::IAM::Role", "resourceName": "check-config-behavior-role-nkmqq3sh", "relationshipName": "Is associated with " } ], ... Omitted # Dependent (supplemental) resources "supplementaryConfiguration": { "Policy": "{\"Version\":\"2012-10-17\",\"Id\":\"default\",\"Statement\":[{\"Sid\":\"test-poilcy\",\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::<$AWS_ACCOUNT_ID>:root\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:ap-northeast-1:<$AWS_ACCOUNT_ID>:function:check-config-behavior\"}]}", "Tags": { "Purpose": "investigate" } } } Frequency of recording dependent (supplemental) resources The frequency of recording CIs in AWS Config depends on the setting of RecordingMode , but this does not seem to be the case for dependent (supplemental) resources. If it was a NotFound-type error, it may have been due to retry attempts. However, the observed behavior indicated that recording was attempted once every 12 or 24 hours. This also does not seem to be a regularity subject to the type of dependent (supplemental) resources. This is the result of my investigation, although it is quite a black box behavior. Summary The above introduced the identity of the mysterious NotFound-type error events output to AWS CloudTrail and countermeasures. The details will be further investigated in the future, but it has been confirmed that similar error events are occurring from the service-linked roles in Macie. Although AWS CloudTrail analysis is a tedious task, it is also an opportunity to gain a deeper understanding of the behavior of AWS services. Therefore, let's perform it proactively! For engineers who want to leverage AWS to its fullest, and who think Keisuke Koide is a talented actor, the Platform Group is currently seeking to hire you! Finally, I will conclude this article by listing each AWS CloudTrail error event. Thank you for reading. Lambda: ResourceNotFoundException { "eventVersion": "1.08", "userIdentity": { "type": "AssumedRole", "principalId": "************:LambdaDescribeHandlerSession", "arn": "arn:aws:sts::<$AWS_ACCOUNT_ID>:assumed-role/AWSServiceRoleForConfig/LambdaDescribeHandlerSession", "accountId": "<$AWS_ACCOUNT_ID>", "accessKeyId": "*********", "sessionContext": { "sessionIssuer": { "type": "Role", "principalId": "*********", "arn": "arn:aws:iam::<$AWS_ACCOUNT_ID>:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", "accountId": "<$AWS_ACCOUNT_ID>", "userName": "AWSServiceRoleForConfig" }, "webIdFederationData": {}, "attributes": { "creationDate": "2023-12-03T09:09:17Z", "mfaAuthenticated": "false" } }, "invokedBy": "config.amazonaws.com" }, "eventTime": "2023-12-03T09:09:19Z", "eventSource": "lambda.amazonaws.com", "eventName": "GetPolicy20150331v2", "awsRegion": "ap-northeast-1", "sourceIPAddress": "config.amazonaws.com", "userAgent": "config.amazonaws.com", "errorCode": "ResourceNotFoundException", "errorMessage": "The resource you requested does not exist.", "requestParameters": { "functionName": "**************" }, "responseElements": null, "requestID": "******************", "eventID": "******************", "readOnly": true, "eventType": "AwsApiCall", "managementEvent": true, "recipientAccountId": "<$AWS_ACCOUNT_ID>", "eventCategory": "Management" } S3: ReplicationConfigurationNotFoundError { "eventVersion": "1.09", "userIdentity": { "type": "AssumedRole", "principalId": "**********:AWSConfig-Describe", "arn": "arn:aws:sts::<$AWS_ACCOUNT_ID>:assumed-role/AWSServiceRoleForConfig/AWSConfig-Describe", "accountId": "<$AWS_ACCOUNT_ID>", "accessKeyId": "*************", "sessionContext": { "sessionIssuer": { "type": "Role", "principalId": "*************", "arn": "arn:aws:iam::<$AWS_ACCOUNT_ID>:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", "accountId": "<$AWS_ACCOUNT_ID>", "userName": "AWSServiceRoleForConfig" }, "attributes": { "creationDate": "2023-12-03T13:09:16Z", "mfaAuthenticated": "false" } }, "invokedBy": "config.amazonaws.com" }, "eventTime": "2023-12-03T13:09:55Z", "eventSource": "s3.amazonaws.com", "eventName": "GetBucketReplication", "awsRegion": "ap-northeast-1", "sourceIPAddress": "config.amazonaws.com", "userAgent": "config.amazonaws.com", "errorCode": "ReplicationConfigurationNotFoundError", "errorMessage": "The replication configuration was not found", "requestParameters": { "replication": "", "bucketName": "*********", "Host": "*************" }, "responseElements": null, "additionalEventData": { "SignatureVersion": "SigV4", "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", "bytesTransferredIn": 0, "AuthenticationMethod": "AuthHeader", "x-amz-id-2": "**************", "bytesTransferredOut": 338 }, "requestID": "**********", "eventID": "*************", "readOnly": true, "resources": [ { "accountId": "<$AWS_ACCOUNT_ID>", "type": "AWS::S3::Bucket", "ARN": "arn:aws:s3:::***********" } ], "eventType": "AwsApiCall", "managementEvent": true, "recipientAccountId": "<$AWS_ACCOUNT_ID>", "vpcEndpointId": "vpce-***********", "eventCategory": "Management" } S3: NoSuchCORSConfiguration { "eventVersion": "1.09", "userIdentity": { "type": "AssumedRole", "principalId": "***********:AWSConfig-Describe", "arn": "arn:aws:sts::<$AWS_ACCOUNT_ID>:assumed-role/AWSServiceRoleForConfig/AWSConfig-Describe", "accountId": "<$AWS_ACCOUNT_ID>", "accessKeyId": "***************", "sessionContext": { "sessionIssuer": { "type": "Role", "principalId": "*************", "arn": "arn:aws:iam::<$AWS_ACCOUNT_ID>:role/aws-service-role/config.amazonaws.com/AWSServiceRoleForConfig", "accountId": "<$AWS_ACCOUNT_ID>", "userName": "AWSServiceRoleForConfig" }, "attributes": { "creationDate": "2023-12-03T13:09:16Z", "mfaAuthenticated": "false" } }, "invokedBy": "config.amazonaws.com" }, "eventTime": "2023-12-03T13:09:55Z", "eventSource": "s3.amazonaws.com", "eventName": "GetBucketCors", "awsRegion": "ap-northeast-1", "sourceIPAddress": "config.amazonaws.com", "userAgent": "config.amazonaws.com", "errorCode": "NoSuchCORSConfiguration", "errorMessage": "The CORS configuration does not exist", "requestParameters": { "bucketName": "********", "Host": "*************************8", "cors": "" }, "responseElements": null, "additionalEventData": { "SignatureVersion": "SigV4", "CipherSuite": "ECDHE-RSA-AES128-GCM-SHA256", "bytesTransferredIn": 0, "AuthenticationMethod": "AuthHeader", "x-amz-id-2": "*********************", "bytesTransferredOut": 339 }, "requestID": "***********", "eventID": "*****************", "readOnly": true, "resources": [ { "accountId": "<$AWS_ACCOUNT_ID>", "type": "AWS::S3::Bucket", "ARN": "arn:aws:s3:::*************" } ], "eventType": "AwsApiCall", "managementEvent": true, "recipientAccountId": "<$AWS_ACCOUNT_ID>", "vpcEndpointId": "vpce-********", "eventCategory": "Management" }
アバター
はじめに こんにちは。ご覧いただきありがとうございます! KINTO FACTORY (以下 FACTORY)という今お乗りのクルマをアップグレードできるサービスで、フロントエンド開発をしている中本です。 今回は、AWS の CloudWatch RUM を用いてブラウザ等のクライアントで発生したエラーを検知する方法について、紹介させて頂きたいと思います。 導入するきっかけ 導入するきっかけとなったのが、カスタマーセンター(CC)からの連絡で、とあるユーザー様が FACTORY の WEB サイトから商品の注文へ進もうとしたところ、画面が遷移しない不具合が発生していると調査依頼を受けたことでした。 すぐさま、API ログなどを解析しエラーが発生しているか確認しましたが、これといって不具合につながるものは発見できませんでした。 そこで次に、フロントエンドでどのような機種やブラウザからアクセスされているか確認してみました。 Cloud Front のアクセスログから、該当ユーザーのアクセスを調べ User-Agent を見てみると、、 Android 10; Chrome/80.0.3987.149 割と古い Android 端末からのアクセスでした。 そのことを念頭に置き、不具合が起きているページのソースを解析したりしていると、ある FE 開発メンバーから、JavaScript の replaceAll が怪しいのではという助言が、、、 こちらの関数、Chrome version が 85 以上での対応でした。。(FACTORY の推奨環境は各ブラウザの最新版としているため、QA でもここまで古いバージョンはテストしておりませんでした) ※チーム内の他メンバーからは、 こちら にて使用する関数を検索することで、どのブラウザではどのバージョンから対応しているか、簡単に検索できることも教えてもらいました! これまでの FACTORY の監視は、BFF レイヤー以下でのエラーを検知し、PagerDuty や Slack へ通知しておりましたが、クライアンでのエラー検知はできておらず、このようにお客様からのご連絡で初めて気付ける状態でした。 このままだと、お客様から何かしらのアクションを頂かない限り、このようなクライアント側で起こるエラーに気づくことさえできないと思い、対策をすることにしました。 検知方法 もともと、FACTORY のフロントエンドには、AWS の CloudWatch RUM (Real-time User Monitoring)の client.js を読み込んでいました。しかし、この機能を特に何かに使うこともしておらず(ユーザージャーニーなどは別途 GA で分析している)、少し勿体無い状況でした。 調べていくと、RUM の仕組みによりブラウザ等のクライアント上で、JavaScript から イベントを CloudWatch へ送信できる ことを知り、 この仕組みを用いることで何かしらのエラーが起きた際、カスタムイベント送信し、検知する仕組みを作ることにしました。 通知方法 通知のおおまかな流れは以下のとおりです。 ブラウザでエラーを検知した場合、CloudWatch RUM でエラー内容をメッセージに含んだ、カスタムイベントを送信する window.crm("recordEvent", { type: "error_handle_event", data: { /* 解析に必要な情報。exception errorの中身等 */ }, }); Cloud Watch Alerm で上記イベントを検知し、イベントが発生した場合にエラー内容を SNS へ送信 上記 SNS が SQS へ通知し Lambda がメッセージを拾って OpenSearch へエラー通知(こちらの仕組みは既存の API エラーを検知・通知する仕組みを流用) 運用してみて こちらの仕組みを本番環境にも反映して、数ヶ月運用して来ましたが、幸いなことに導入のきっかけとなった JavaScript のエラー等、クリティカルな問題は発生しておりません。 ただ、検索エンジンのクローラーや bot などからの、意図しないアクセスでエラーが発生するケースも検知できており、導入するまで特に気に留めていなかったアクセスにも気付けるようになったので、監視の大事さを改めて知る戒めにもなりました。 最後に FACTORY のように WEB サイトでお買い物を頂くには、エラーが発生し商品が購入できなくなったりページが見えなくなるというケースを、できる限り防ぐ必要があります。しかし、すべてのお客様の端末・ブラウザにて動作を保証するにも限界があるかと思います。 そこで、エラーが発生してしまった場合には、できるだけわかりやすいメッセージ(対処法など)を画面へ返してあげることと、運用面として開発している我々が、発生した事実と不具合内容に早急に気づける仕組みが必要かなと思います。 今後も様々なツールや仕組みを駆使して、安定した WEB サイトの運用を目指していきたいです。
アバター
​As an authentication engineer of KINTO, Hoang Pham will present an article about Passkey, which was implemented on the Global KINTO ID platform (GKIDP). After joining “ OpenID Summit Tokyo 2024 ” and hearing about Passkey combined with OIDC, I thought that I should write something about how Passkey brings much profit to our ID platform. I. Passkey Autofill on GKIDP Passkeys are a replacement for passwords that provide faster, easier, and more secure sign-ins to websites and apps across a user’s devices. Below is how users can authenticate by passkey with a single click. ![](/assets/blog/authors/pham.hoang/Fig1.gif =400x) Fig 1. Login by Passkey with KINTO Italy IDP The beauty of Passkey demonstrated by its seamless UX exactly is the same as the “Pass word recommendations”, so users do not need to know the intricacies of what is different between a Passkey or a password. The system uses asymmetric cryptography behind without a password or anything the user must remember. Just FaceID authentication, and everything is set! Passkey is the most secure and state of the art on authentication system in the field which has been supported by Android and iOS since late 2022. It is still in development and being upgraded. To ensure our GKIDP (Global KINTO ID Platform) remains up-to-date with the latest technologies, we introduced Passkey Autofill in July 2023, just right after Mercari , Yahoo Japan , GitHub , and MoneyForward integrated it into their respective ID Platforms. In the next parts, I will explain how we leverage Passkey on Federated login and make GKIDP users more comfortable with our “Global Login” feature. II. Passkey on Federated Identity To briefly explain our product, our Global KINTO ID Platform, or GKIDP is the authentication system deployed in Italy, Brazil, Thailand, Qatar, and South American countries for the KINTO services in those locations as of March 2024. By compliance with the GDPR and data protection regulations, we separate GKIDP into multiple Identity Providers (IDPs) located in each country and identify users as one single user’s Global ID through a “Coordinator”. By leveraging Global ID, users may be able to enjoy shared benefits across KINTO services around the world. Fig 2. GKIDP and Passkey-supported IDPs In most cases (Fig. 1. Login with Passkey), users just use the local IDP for federated authentication and log in to use KINTO services inside their country. But in our case, Passkey was implemented on each of our IDP (for example, Brazil IDP) to help all RP-relying party applications or “satellite services” (for example, KINTO One Personal or other KINTO services in Brazil) include a Passkey functionality. This advantage was also mentioned at the OpenID Summit Tokyo 2024 in which we participated, so it was good to know we are on the right track to implement Passkey combined with the OpenID Connect protocol. Additionally, GKIDP has a unique feature to let users, not only log in to the KINTO or KINTO related services inside their country but also outside, if they travel or move to other countries where there are other KINTO services. We call it the “Global login” feature. It contains many steps, but it tries to solve the difficulty for users to remember multiple usernames and passwords from different countries. The implementation of a Passkey can streamline the global user login process, requiring only a few simple steps without the need to remember or input any information. For example, let’s see how the Italy KINTO Go user (same user in the example of Fig. 1) could make use of the global login to access the KINTO Share service in Thailand with just a few clicks in Fig. 3, reducing the log in experience time from an average of 2–3 minutes to around 30 seconds. Users can utilize a single Passkey to access all KINTO services, regardless of whether the local IDP supports Passkey or not. ![](/assets/blog/authors/pham.hoang/Fig3.gif =300x) Fig 3. Global Login with Passkey The passkey is not only integrated into the local login and global login processes but also into all authentication screens including re-authentications, etc. Once a Passkey is registered, users hardly need a password to verify anything anymore. III. Passkey and some interesting numbers Fig 4. Passkey registered users In our Italy IDP case, we received 875 users who registered and using Passkey, occupying 52.2% of new registrations since Passkey was released. We hope that this number will increase as users update their OS to support Passkey Autofill (iOS >16.0 and Android> 9) In Brazil, despite the focus on Desktop PC users with KINTO Brazil, where Passkey isn't widely used on Microsoft PCs, we still have more than 20% among the 1176 newly registered users. IV. Conclusion As KINTO engineers, we are very excited to introduce new technologies for a passwordless future and strengthen user data protection. Leveraging Passkey, users can log in with ease with the highest level of security with this method nowadays. We are looking forward to connect many new KINTO services to our IDP(s) hub: GKIDP. Another article from Hoang Pham: https://blog.kinto-technologies.com/posts/2022-12-02-load-balancing/
アバター