コネヒト開発者ブログ

コネヒト開発者ブログ

iOSDC2021で「知られざる課金ステータス」というタイトルで発表してきました

こんにちは!コネヒトでiOSアプリの開発をしている @ohayoukenchan です。
2021.9.17-9.19に開催されたiOSDC2021で発表してきました。

iOSDCとは?

iOS関連技術をコアのテーマとしたオンラインカンファレンスです。

今回はオンラインということで、トークは事前収録されニコ生で配信されました。 また、トークセッション後のAsk The SpeakerはDiscordにて行なわれました。 事前収録のおかげでトーク中もDiscordで質問にお答えする余裕があったりオンラインイベントならではの良さがありました。

コネヒトはシルバースポンサーとして協賛させていただきました!

自分のトーク内容

お試しオファーの無料期間中にプロモーションオファーを適用したり、プロモーションオファーの無料期間中にプロモーションオファーを適用したりすると、ユーザーの課金状態がどのようになるかについてお話しました。

オファーの比較

それぞれのオファーについて簡単な表にしてみました。 オファーコードに関しては省略しております

お試しオファー プロモーションオファー
主な用途 新規サブスクリプション利用者の獲得 既存のサブスクリプション利用者の維持と過去の利用者の再登録
利用資格 App内の新規サブスクリプション利用者 App内の既存または過去のサブスクリプション利用者
利用の限度 サブスクリプショングループごとに1件のお試しオファーを利用可能 デベロッパはユーザーが何件までオファーを利用できるか決定 する
オファーの限度 サブスクリプションごと、地域ごとに、1件のオファー サブスクリプションごとに、10件のアクティブなオファー
互換性(iOS) iOS 10以降 iOS 12.2以降

同一サブスクリプションのプロモーションオファーを適応する場合

プロモーションオファーはサブスクリプションに紐づく形で作成できます。
オファー適用前のサブスクリプションが適用するオファーに紐づく場合は、現在の無料期間終了後にプロモーションオファーが適用されます。

お試しオファー無料中の場合、お試しオファー終了後にプロモーションオファーが適用される

f:id:ohayoukenchan:20210928180731p:plain
プロモーションオファーが課金前のサブスクリプションに紐づく場合

課金中の場合、契約期間終了後にプロモーションオファーが適用される

f:id:ohayoukenchan:20210928181857p:plain
プロモーションオファーが課金前のサブスクリプションに紐づく場合(課金中)

クロスグレードが発生する場合

クロスグレードはユーザーが、同等のレベルのサブスクリプションに切り替えることを指します。詳しくはこちらをご確認ください。

プロモーションオファーが紐付いていない別のサブスクリプションからプロモーションオファーを適用させることも出来ます。 その場合、グレードと契約期間からクロスグレードの条件が決まります。 今回はすべて同一グレードのみの検証となっています。 グレードが異なる場合アップグレードやダウングレードの対象となりますのでご注意ください。

別のサブスクリプション(期間が同じ)のプロモーションオファーを適用する場合

別のサブスクリプションで期間が同じサブスクリプションからプロモーションオファーを適用する場合は現在の契約は即時終了します。

お試しオファー無料中の場合、お試しオファーは即終了しプロモーションオファーの無料期間になる

f:id:ohayoukenchan:20210928180836p:plain
プロモーションオファーが課金前のサブスクリプションに紐づかない場合(お試しオファー中)

課金中の場合、現在の契約は終了し、プロモーションオファーの無料期間になる。そして契約期間の差額分返金される

App 内課金が同じ期間のものである場合、前の App 内課金から比例配分された金額は、元の支払い方法で返金されます。

f:id:ohayoukenchan:20210928181239p:plain
プロモーションオファーが課金前のサブスクリプションに紐づかない場合(課金中)

別のサブスクリプション(期間が異なる)のプロモーションオファーを適用する場合

同じグレード、同じ期間の場合は現在適用中のオファーは即破棄されていましたが、期間が異なる場合は条件が異なります。 期間が異なる場合、オファー適用は現在の契約が終了した後の適用となります。

お試しオファー無料中の場合、お試しオファー終了後にプロモーションオファー適用になる

f:id:ohayoukenchan:20210928181315p:plain
プロモーションオファーが課金前のサブスクリプションに紐づかない場合(お試しオファー中)

課金中の場合、契約期間終了後にプロモーションオファー適用になる

App 内課金が異なる期間のものである場合、クロスグレードは、カスタマーの次の更新日に有効になります。 同一サブスクリプションのプロモーションオファーを適用する場合と同じ挙動となります。

f:id:ohayoukenchan:20210928181408p:plain
プロモーションオファーが課金前のサブスクリプションに紐づかない場合(課金中)

プロモーションオファー無料期間中にプロモーションオファー

プロモーションオファーの無料期間中に再度プロモーションオファーを適用させようとすると、現在適用中のオファーとは別に1件までペンディングさせることができます。ペンディング中のオファーはレシート情報のpending_renewal_infoに記録され、次回更新日に有効化することができます。 pending_renewal_infoは常に最新のオファーしか記録しないので複数のプロモーションオファーを適用待ちにできないので、プロモーションオファーを何回かけても最新のオファー一件のみしかペンディングさせることが出来ないことに注意してください。

f:id:ohayoukenchan:20210928181445p:plain
プロモーションオファー無料期間中にプロモーションオファーを適応させ、 再度プロモーションオファー

話さなかったこと

7日間無料状態で課金を試すときは鍛錬が必要

sandboxでの7日間は5分しかないので無料期間内にオファーを追加するのが大変だったりしました。 sandboxアカウント名を短くしたり、事前に課金開始画面を開いておいたり。DBでレシート検証するユーザーの情報を事前に用意したりすることでいかに早く処理を完了するかを鍛えることができました。

sandbox環境で何度やっても想定通り行かない日がある

私はexpire_dateを過ぎても全く更新されない地雷を踏みました 別の日に試すとすんなりうまくいったので、sandbox調子悪いなとおもったら別の日に試したほうがよさそうです。

ハマったこと

サーバー側で現在の自動継続課金の有効期限を、レシート内の一番未来のexpire_date_msでソートして取得していたが、お試しオファー1年無料の後に、6ヶ月無料を適用させた場合、有効期限が6ヶ月後を想定しているのに対して、破棄された1年無料の有効期限を取得してしまっていた。サーバー側で保持した有効期限はクライアント側で表示ロジックの制御に使用していたので、有効期限が異なり、ユーザーに適切な体験を提供できなくなりそうだった。最新のレシートの有効期限を取得することで回避しました。

終わりに

オファーの切り替えなどはユーザーが意図しないケースも多く、ユーザー体験を損ねてしまっているのではないかと感じています。 ママリでは購読開始前に現在の課金ステータスから、返金対象だったり契約が消えてしまう可能性のあるユーザーに注意文言を表示していますが、そもそも確認されているのかも怪しいと思っています。 StoreKit2の発表により、アプリ内で現在の課金状態を表示することが出来るようになったり、返金APIが追加されたりするのでユーザー体験を損ねないようなユーザー体験を提供できそうですね。

コンポーネントカタログ・ユニットテストを停滞させないための開発フロー設計

こんにちは、リードエンジニアの @dachi_023 です。4ヶ月前に Storybookを利用した開発フローの設計 という記事を書いてから今も引き続き上手くやれてますよ、という話とテスト運用についても改善を入れたことなどについて書きます。プロダクトコード外の運用ができていないなと感じる方向けの記事になればいいなと思っています。

運用し続けることが大事

テストがあることで仕様を担保しながら安心して実装することができます。コンポーネントカタログがあることでコンポーネントのドキュメントを簡単に生成でき、また UI に関する話をする際に実際に動くものとして提示できる材料になります。

といったようにテストもコンポーネントカタログも特定のシーンで大きく効果を発揮するものですが、導入しただけでは意味がなく運用し続けるための工夫が必要です。例えば特定のケースが担保できていないテストや実装時にコンポーネントカタログに追加し忘れたコンポーネントがある状態が放置され続けていればその効果も薄まりますし、それを解消する方向に導かなければ足りない箇所がどんどんと増えていくかもしれません。

これらを達成するために意図通りの仕様で動作しているかを検査する仕組みを作ったり、日常的に取り組めるよう開発フローの一部として組み込んだりすることが重要だと考えています。入れたら勝手に運用が回っていく、ということはそうそうないと思います。

コンポーネント単位で PR を作る

(冒頭に貼り付けた記事 に経緯などが書かれていますので是非読んでみてください)

私達のチームではいきなりページ全体を実装する、といった方法は取らずコンポーネントごとに実装しています。ページごと丸っと誰かをアサインして開発していくといったこともできなくはないですがページによって実装難易度も大きく変わってきますし、何日もかかったりすると他の作業をブロックしたり小さくリリースすることが難しくなります。また、コード量や対象範囲が大きくなることでレビュー時にレビュアー・レビュイーの双方に負担をかけかねません。

そういったことを回避するためにまずはページ内で新規実装する必要があるコンポーネントの実装を GitHub Issue 化して個別に対応 → Storybook に追加・レビュー → ひと通り揃ったところでページを組み立て、というフローにしています。

f:id:dachi023:20210831120416p:plain
1つの機能を実装するために複数の Issue に分解しカンバン上で管理する

上記を実践するためのツールとして Storybook を採用しています。コンポーネントを個別に実装していった際の「実際に動く場所がなくてレビューできない」という問題を解消するために利用しています。これによって小さく実装し、小さくリリースしていくことができています。また、コンポーネントを1つずつ PR にしていくことでレビュー時の観点が絞られるので (コンポーネントとしての設計・コード・UI の挙動) 指摘や修正が広範囲にならずに済みますし、ページを組み立てた際のレビューもページとしての仕様が担保できているか、を中心にレビューすれば良くなります。

非同期コミュニケーションの回数が増えると作業が中断されたりボールが返ってくるまでに時間がかかったりするので分解できるものは分解して、というのが基本方針です。

テストケースの考慮不足に気づく

テストに関しても同様で「開発フローに組み込む」「レビューで活用する」といった考えでやっています。これを実現するためにチームでは Codecov を利用しています。テストは実装されているか、どのケースを考慮してテストを書いたか、テスト済み以外のコードでテストすべき点はないか、などを見つけるために有効です。

f:id:dachi023:20210830151249p:plain
テストが当該行を通過していない時に出るメッセージ

網羅率に関するルールなどは特に明記してありませんが「アプリケーション全体としてある程度書けていること」をチェックするために全体の網羅率が 90% を切った時に CI が赤くなるよう設定しています *1

機械的に判定 (カバレッジが担保できているかチェック) するだけでなく「仕様に対して適切なテストができているか」をレビューするところまでをセットとして捉えています。

まとめ

本記事で挙げたようなツールの運用がストップしてしまうのは「プロダクト開発に必須でない (= プロダクションで動かない)」という意識なのではないかと思っています。

短期的に考えたらあまり効果的ではないかもしれないです。しかし、チームとして過去のコードを活かしながら何年も開発し続けるためには重要な要素です。入れるだけではなく、しっかりと活用するところまでイメージしながら開発していきましょう。

*1:厳密に設定しているわけではなく、都度様子見ながら運用しています

Slack上での出欠確認を楽にする「点呼さん」というbotをつくりました

点呼さんのイメージ
点呼さんのイメージ

こんにちは。CTOをやっている @itosho です。コネヒトでは「Beyond a Tech Company」というテックビジョンを掲げているのですが、テックカンパニーの条件の一つとして「社内の人がテクノロジーの恩恵を受けている」ということを重要視しています。ですので、以前からインナーコミュニケーションを盛り上げるための開発社内ツールの開発を積極的に行っています。というわけで、この記事では最近開発したSlack Botである「点呼さん」の紹介をしたいと思います。

点呼さんとは?

コネヒトでは昔からSlack上でイベントの出欠をとることが多く、こんな感じで「参加」「不参加」のリアクションをつけてもらうことで出欠確認を行ってきました。

f:id:itosho525:20210730133947p:plain
出欠確認の様子

社員の数が少ないうちはこのやり方で問題なかったのですが、嬉しいことに社員の数がどんどん増えてきた結果、誰がリアクションしていないかを確認してリマインドを送ったり、誰が参加者なのかを把握したりする手間が増えてきました。解決策として、tmpチャンネルを用意して、対応を行った人から抜けるという方法も一部では行っていますが、ものによってはToo Muchなケースも多くありました。もちろん、このまま人力で頑張るのはスケールしないやり方ですし、何よりテックカンパニー感がありません。

そこで開発したのが「点呼さん」です。点呼さんはSlackのリアクションで出欠をとる時のリマインドや集計のサポートをしてくれるSlack Botです。

使い方

基本的な使い方

まずは点呼さんを出欠確認を行いたいチャンネルにinviteします。

f:id:itosho525:20210730135510j:plain
点呼さんをinviteする

その後、こんな感じのフォーマットで点呼さんにmentionを送ります。(最後のモザイクがかかっている部分はSlack内のメッセージURLです)

f:id:itosho525:20210730135934j:plain
点呼さんにmentionを送る

そうすると、点呼さんから対象のSlackメッセージにリアクションをまだつけていない人のリストが返ってきます。(モザイクがかかっている部分はユーザーIDの一覧です)

f:id:itosho525:20210730140428j:plain
点呼さんからリストが届く

あとはコードブロックの部分をコピペしてもらえば、リマインドなど自由にメッセージを送ることが出来ます。

ちなみに点呼さんの返事をDMにしている点とそのまま点呼さんがリマインドしないようにしている点は意図的で、人によっては自分のユーザー名をキーワード通知している可能性があるのと時間帯を気にせず使えるようにしてもらいたいというのが理由の一つです。その代わり、コピペですぐメッセージを送れるようにしています。

発展的な使い方

@点呼さん 教えて!◯◯ SlackURL が基本的なフォーマットになっていて、リアクションをしていない人のリストを作る以外にも…

  • @点呼さん 教えて!参加する人 SlackURL とすると参加する人のリスト
  • @点呼さん 教えて!不参加の人 SlackURL とすると不参加の人のリスト
  • @点呼さん 教えて!リアクション名 SlackURL とすると特定のリアクションをした人のリスト
  • 例えば、doneリアクションをした人みたいな使い方を想定

といったようなリストの確認も出来ます。

また、抽出する対象をチャンネルにjoinしている人ではなく、特定のSlackグループで絞り込むことも出来ます。ユースケースとしては、たくさん人がいるチャンネルだけど、開発組織の歓迎会の出欠をエンジニアやデザイナーにリマインドしたい時なんかを想定しています。

f:id:itosho525:20210730142746j:plain
グループで絞り込みを行いたい時

点呼さんの実装

特殊なことはしておらずSlack Botをつくって、そこから上述の仕様を実現するためにSlack APIを適宜叩いています。Goで書いているのでSlackのAPI連携やメンション時のイベントハンドリングなどは slack-go/slackを利用させてもらっています。

コネヒト独自の仕様のところは参考にならないと思いますが、SlackのAPI連携している箇所はもしかしたら参考になる部分もあるかもしれないので、コード片を晒しておきます。プロダクションコードではないので、ゆるく書いている箇所もありますが、NYSL的な感じで自由に使ってください。

// slack-go/slack をimportしている前提です

// BotのSlackクライアントとuser APIのSlackクライアントが存在するのでこんな書き方になっています
type User struct {
    Client *slack.Client 
}

//  全ユーザーを取得する処理
func (u User) GetAllUsers() (users map[string]string, err error) {
    res, err := u.Client.GetUsers()
    if err != nil {
        return users, err
    }

    users = map[string]string{}
    for _, v := range res {
        if !v.IsBot {
            users[v.ID] = v.Profile.DisplayName
        }
    }

    return users, nil
}

// 指定したチャンネルにjoinしているユーザーを取得する
func (u User) GetUserIDsInChannel(channelID string) (userIDs []string, err error) {
    params := &slack.GetUsersInConversationParameters{ChannelID: channelID, Limit: 1000}
    res, _, err := u.Client.GetUsersInConversation(params)
    if err != nil {
        return userIDs, err
    }

    return res, nil
}

// 指定したSlackグループ名(人間が利用するもの)からグループID(APIで利用するもの)を取得する
func (u User) GetGroupIDs(groupHandles map[string]bool) (groupIDs []string, err error) {
    res, err := u.Client.GetUserGroups()
    if err != nil {
        return groupIDs, err
    }

    for _, v := range res {
        if _, ok := groupHandles[v.Handle]; ok {
            groupIDs = append(groupIDs, v.ID)
        }
    }

    return groupIDs, nil
}

// 指定したグループIDに所属するユーザーID一覧を取得する
func (u User) GetUserIDsInGroups(groupIDs []string) (userIDs []string, err error) {
    for _, v := range groupIDs {
        res, err := u.Client.GetUserGroupMembers(v)
        if err != nil {
            return userIDs, err
        }
        userIDs = append(userIDs, res...)
    }

    return sliceUnique(userIDs), nil
}

// 指定したSlackメッセージ(チャンネルIDとタイムスタンプ)から指定したリアクションをしたユーザーID一覧を取得する
func (u User) GetActionedUserIDs(channelID string, timestamp string, reactionName string) (userIDs []string, err error) {
    item := slack.ItemRef{Channel: channelID, Timestamp: timestamp}
    params := slack.GetReactionsParameters{Full: true}
    res, err := u.Client.GetReactions(item, params)
    if err != nil {
        return userIDs, err
    }
    if len(res) < 1 {
        return userIDs, errors.New("unexpected length of GetConversationReplies")
    }

    if reactionName == "" { // The target is all reaction.
        for _, reaction := range res {
            userIDs = append(userIDs, reaction.Users...)
        }
        userIDs = sliceUnique(userIDs)
    } else {
        for _, reaction := range res {
            if reaction.Name == reactionName {
                userIDs = reaction.Users
                break
            }
        }
    }

    return userIDs, nil
}

実装時に意識をした点

カッとなって勢いで作った部分もあるのですが、その中でもユーザーに入力を求める類のSlack BotはUNIXコマンドに似たところがあると思うので、Unixの哲学を意識し、出来る限りシンプルさを保つようにしました。

具体的には、先ほど述べたようにリマインド機能やDMではないチャンネルで結果を返すことなども検討したのですが、それを実現しようとするとオプションがどんどん増えていってしまいインターフェイスも複雑になります。そうすると、結果的に何でも出来るけど何が出来るかわからないツールになるリスクがあるので、オプションは最小限に留めるようにしました。そして、出欠確認のリマインドを楽にするという課題にフォーカスし @点呼さん 教えて!まだの人 SlackURL だけ覚えれば、まずはBotが使えるという状態を作り、そこでこのBotの利便性を感じてもらってから発展的な使い方をしてもらえるような工夫を行いました。

実際、利用してくれる方はちょくちょくいるので、ある程度のその設計は上手くいったのかなと思っています…!

最後に

会社が大きくなると、事業だけではなく、組織の面でも様々な課題が生まれます。そういった課題もテクノロジーやエンジニアリングの力で解決出来る会社が、冒頭にも述べたようにテックカンパニーだと僕は考えているので、引き続き、技術で勝っていくことで、プロダクトのユーザーだけではなく、社内の人も少しでも幸せにしていくぞ!

というわけで、コネヒトでは自分の書いたコードで社会や組織を良くしていきたいエンジニアを大大大募集しております! 1mmでも興味がありましたら、下記のWantedlyから「話を聞きに行きたい」ボタンをポチっと押していただくか、僕にDMを雑に送っていただいても構いませんので、まずはカジュアルにお話させていただければと思います!

hrmos.co

カスタムコンテナイメージを用いたデータ分析環境共通化Tips(ローカルPC&AWS SageMaker Studio)

みなさんこんにちは。たかぱい(@takapy0210)です。
気づけばもう6月ですね。2021年の半分が過ぎようとしています。もう半分.....もう...。

はじめに

みなさんデータ分析環境はどのように構築していますか?
Gunosyさんのブログ*1にもあるように、環境構築方法は様々あると思います。

本エントリでは、ローカルとクラウド(AWS SageMaker Studio)のデータ分析環境をコンテナで構築し、環境差分の無い快適なデータ分析ライフを過ごすTipsについてご紹介しようと思います。

これにより軽量な分析はローカル環境でサクッと、重めの分析はクラウド環境(AWS SageMaker Studio)で強いコンピューティングリソースを用いてじっくりとやる、といったことを環境差分を気にせず行うことができます。

ちなみに、ローカルで起動する際はdocker compose up -d jupyterlabコマンドを叩くだけ、SageMakerで起動する際はGUI上でポチポチっと数回クリックすると同じ環境が起動できたりします。

dockerなどの詳細には触れませんのでdockerドキュメントやgoogleなどで調べてみてください。


目次


AWS SageMaker Studioとは

まずAWS SageMaker Studioについて簡単にご紹介します。(以降の記載はSageMakerに省略します)

SageMakerは、機械学習のための統合開発環境 (IDE) と謳われており、慣れ親しんだnotebookを起動してデータ分析・モデル構築ができるのはもちろん、ホストされたエンドポイントにモデルのデプロイするといったことも可能なマネージドサービスです。(公式ドキュメント

デフォルトではAWSが用意してくれているコンテナイメージをベースにnotebookを起動できます。

コンピューティングリソースに関しても様々な種類のインスタンスが用意されており、使いたいインスタンスをGUI上で選択し、数分待つとnotebookが起動できたりと、手軽に環境構築することができます。

しかし、用意されているコンテナイメージでは使いたいライブラリがインストールされていなかったり、違うpythonのバージョンを使いたい、といった要望もあると思います。
(SageMakerを起動する時に毎回pip installしても良いのですが、なかなか煩雑ですよね...)

 そこで、カスタムコンテナイメージを用いることで上記のような課題を解決できます。

カスタムコンテナイメージを用いるメリット

上記でも述べましたが、AWSが用意しているコンテナイメージに存在しないライブラリだったり、使いたいpythonのバージョンをSageMaker上で使用することができます。

また、複数人のMLエンジニアやデータサイエンティストがいる場合においては、dockerfile群を共有しdocker compose buildするだけで手間なく同じ分析環境が構築でき、「Aさんの環境とBさんの環境で分析結果や挙動が異なる・・・」みたいなことを防ぐこともできます。

使用するDockerfile群について

まずはローカル環境を構築するために、以下3つのファイルを用意します。

  • Dockerfile
  • docker-compose.yml
  • jupyter_notebook_config.py

以降でそれぞれのファイルの詳細についてご紹介します。
(掲載しているコードはサンプルとして適宜省略しています)

Dockerfile

ポイントは以下2点です。

  • jupyter_notebook_config.pyファイルを用いることでシンプルなコマンドでjupyterが起動できるようにする(jupyter notebook --port 8888 --ip="0.0.0.0" --allow-rootみたいな長ったらしいコマンド叩くの嫌ですよね...)

  • python3 -m ipykernel install --sys-prefixコマンドでSageMakerから認識できるようにする。

FROM python:3.8

LABEL hoge <hoge@fuga.com>

RUN apt-get -y update && apt-get install -y --no-install-recommends \
        mecab \
        libmecab-dev \
        mecab-ipadic \
        mecab-ipadic-utf8 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*
    
# jupyter lab
RUN pip3 install -U pip && \
    pip3 install jupyterlab && \
    pip3 install jupyterlab-git && \
    mkdir ~/.jupyter
COPY ./jupyter_notebook_config.py /root/.jupyter/jupyter_notebook_config.py

# requirements.lockにインストールしたいライブラリを記載する
# python3 -m ipykernel install --sys-prefix がないと、SageMakerがpython3.8を認識してくれないので注意
COPY requirements.lock /tmp/requirements.lock
RUN python3 -m pip install -r /tmp/requirements.lock && \
    python3 -m ipykernel install --sys-prefix && \
    rm /tmp/requirements.lock && \
    rm -rf /root/.cache

COPY working /opt/program/working
WORKDIR /opt/program/working

docker-compose.yml

下記のようなイメージです。

version: '3.4'

x-template: &template
  build:
    context: .
  volumes:
    - ./working:/opt/program/working:cached

services:
  jupyterlab:
    container_name: 'jupyterlab'
    image: jupyterlab:latest
    user: root
    ports:
      - "8999:8999"
    command: jupyter lab --allow-root
    <<: *template

jupyter_notebook_config.py

下記のようなイメージです。
c.NotebookApp.passwordにはjupyterlab起動時に入力するパスワードを設定できます。(本サンプルでは「password」となっています)

c = get_config()
c.NotebookApp.ip = '0.0.0.0'
c.NotebookApp.open_browser = False
c.NotebookApp.port = 8999
c.NotebookApp.notebook_dir = '/opt/program/working/'
c.LabApp.user_settings_dir = '/opt/program/working/jupyterlab/user-settings'
c.LabApp.workspaces_dir = '/opt/program/jupyterlab/workspaces'
c.NotebookApp.password = u'sha1:63cae364b3cd:c4319cba1eeb1bcf011a7d3fabd6448f95ae18c5'

ここまで準備ができたら、あとはdocker compose up -d jupyterlabコマンドを実行した後、ローカルのブラウザでhttp://127.0.0.1:8999/labにアクセスすればjupyterLabが使えます。

次に、このコンテナイメージをSageMaker上で使う方法についてご紹介します。

SageMaker でカスタムコンテナイメージを起動する方法

SageMakerで使用するためには、追加で以下2つのファイルが必要になります。

  • app-image-config-input.json
  • update-domain-input.json

それぞれの役割について簡単にご紹介します。

app-image-config-input.json

このconfigファイルには、SageMakerで使用する際のカーネル名やマウントディレクトリを定義します。
KernelSpecsNameに設定する値は、コンテナイメージをローカルで起動した後jupyter-kernelspec listを実行した際に表示されるカーネルの中から選択する必要があります。(詳しい手順はこちらをご参照ください)

{
    "AppImageConfigName": "ml-image-name",
    "KernelGatewayImageConfig": {
        "KernelSpecs": [
            {
                "Name": "python3",
                "DisplayName": "Python 3"
            }
        ],
        "FileSystemConfig": {
            "MountPath": "/root/data",
            "DefaultUid": 0,
            "DefaultGid": 0
        }
    }
}

update-domain-input.json

これはコンテナイメージをドメイン(≒ Sagamaker Studio)にアタッチするために必要なファイルです。

DomainIdには、AWSコンソール上で表示されるStudio IDを設定してください。

また、AppImageConfigName には、app-image-config-input.jsonで定義したものと同値を設定する必要があります。

{
    "DomainId": "d-hogehoge",
    "DefaultUserSettings": {
        "KernelGatewayAppSettings": {
            "CustomImages": [
                {
                    "ImageName": "ml-analysis",
                    "AppImageConfigName": "ml-image-name"
                }
            ]
        }
    }
}

SageMakerへ登録

上記2つのファイルを作成できたら、あとは以下のようなスクリプトを実行して、コンテナのビルド→ECR push→SageMakerへのアタッチを行います。

#!/bin/bash

REGION="AWSのリージョン"
ACCOUNT_ID="AWSのアカウントID"
REPOSITORY_NAME="ml-analysis"
TAG_NAME="ml-analysis"
ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/service-role/hoge"

# ビルド
docker build -t ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}:${TAG_NAME} .

# ログイン
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com

# push
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}:${TAG_NAME}

# コンテナイメージを Amazon SageMaker に登録
aws --region ${REGION} sagemaker create-image \
    --image-name ${TAG_NAME} \
    --role-arn ${ROLE_ARN}
aws --region ${REGION} sagemaker create-image-version \
    --image-name ${TAG_NAME} \
    --base-image "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${REPOSITORY_NAME}:${TAG_NAME}"

# AppImageConfigの作成(「file:」以降にファイルパスを指定)
aws --region ${REGION} sagemaker create-app-image-config --cli-input-json file://sagemaker/app-image-config-input.json

# コンテナイメージをSagamakerにアタッチ(「file:」以降にファイルパスを指定)
aws --region ${REGION} sagemaker update-domain --cli-input-json file://sagemaker/update-domain-input.json

ここまでで、下記のようにSageMakerコンソール上からイメージがアタッチされていることを確認できます。

あとはSageMaker Studioを起動して、アタッチされたイメージを選択してnotebookを起動するだけで、ローカルと同一環境がSageMaker上で実現できます 🎉🎉🎉

SageMakerを使う際に気をつけたいこと

前述してきたように、SageMakerを使う際のメリットはいくつかあります。

  • 豊富なコンピューティングリソースを用いて分析・モデリングできる
  • notebookが動く環境が既に用意されているので環境構築が比較的容易
  • 付随するマネージドサービスが豊富にある(本記事では触れませんが、FeatureStore, Experiments, Pipelineなどがあります)

しかし、注意点もあるので最後にまとめておこうと思います。

油断すると結構お金がかかる

はい。油断すると結構お金がかかります(汗)

SageMakerは従量課金のマネージドサービスで、主に以下2点で課金されます。

  • notebookなどのインスタンス起動時間
  • マウントされているEFSの容量

notebookなどのインスタンス起動時間

使用していないインスタンスは停止させたり、業務終了時にインスタンスを落とすなどの対策すれば大丈夫だと思います。
例えば、ml.t3.2xlarge(8vCUP/メモリ32GiB)のインスタンスは0.522USD/hなので、1ヶ月(約720h)起動させたままにすると375USD(約4万円)コストがかかります。

「深夜に終わる想定の学習回しちゃったら朝までお金かかっちゃうのか...」と思われる方もいると思いますが、SageMakerには ProcessingJobTrainingJobといった機能もあり、これらはJobが終了すると自動的にインスタンスが落ちてくれるので、併せて使ってみると良いと思います。(この辺りのことも後日記事にできればと思っています)

(詳細な料金は公式ドキュメントを参照してください)

マウントされているEFSの容量

こちらは容量単位で課金されます。

ここで注意したいのが、SageMaker notebook上からファイルを削除すると、notebook上からは削除されているように見えるのですがTrashに残っていたりします。

特に機械学習やデータ分析においては容量の大きいファイルを多用することになると思うので、これを放置しておくと無駄に課金されてしまうリスクがあります。

SageMaker上でターミナルを起動しdf -hコマンドを実行すると現在どのくらいの容量を使っているかが把握できるので、定期的にチェックすることをオススメします。
(下記例だと、936MBのEFSボリュームを使っていることになります)

$ df -h
Filesystem         Size  Used Avail Use% Mounted on
overlay             17G   52K   17G   1% /
tmpfs               64M     0   64M   0% /dev
tmpfs              1.9G     0  1.9G   0% /sys/fs/cgroup
shm                 64M     0   64M   0% /dev/shm
127.0.0.1:/200005  8.0E  936M  8.0E   1% /home/sagemaker-user  # ここがnotebookにマウントされているEFSの容量
/dev/nvme0n1p1      83G  8.8G   75G  11% /etc/hosts
devtmpfs           1.9G     0  1.9G   0% /dev/tty
tmpfs              1.9G     0  1.9G   0% /proc/acpi
tmpfs              1.9G     0  1.9G   0% /sys/firmware

上記で表示される容量にはTrash(ゴミ箱)にあるファイルも含まれているので、notebook上から削除したファイルはrmコマンドで完全に削除する必要があります。
なので、不要なファイルをSageMaker notebook上から削除した際にはrmコマンドも実行するように意識しておくと良いです。

# ゴミ箱のファイル一覧
$ ls -al -h  ~/.local/share/Trash/files

# ゴミ箱のファイルを全て削除
rm -rf ~/.local/share/Trash/files/*

(EFSの詳細な料金については公式ドキュメントを参照してください)

最後に

最近はAWSからも機械学習系のサービスが頻繁にローンチ・アップデートされており、SageMakerもここ数年でかなり機能強化されています。 とはいえ、まだまだ公開されている事例が少ないとも感じているので、業務で得た知見などは積極的にアウトプットしていければと思っています。

みなさんも良い分析ライフをお楽しみください!

家族ノートを支えるBigQuery+StepFunctionsで作るデータレイク

こんにちは。インフラエンジニアの永井(shnagai)です。

最近、家族ノートという「ママリ」内の検索データとQ&Aデータ(現在開発中)を可視化したデータ分析サービスの立ち上げに携わっています。

info-kazokunote.mamari.jp

今回は、家族ノートで使っているデータ基盤の一部であるBigQuery+StepFunctionsで作ったデータレイクの仕組みについてご紹介します。

内容は、ざっくりとこんな話を書こうと思います。

  • データ基盤作りに至った経緯
  • AWS→BigQueryにデータ移送するアーキテクチャのpros&cons
  • StepFunctions+Embulk(Fargate)を利用したデータレイクの仕組み

データ基盤作りに至った経緯

コネヒトには大きく分けると2つのデータセットがあります。

  • DB(Aurora)にあるアプリケーションのデータ(業務データやマスターデータ)
  • BigQueryにある行動ログや集計データ

今回家族ノートで使うデータとしてこの2つのデータセットをかけ合わせる必要があり、 データ結合の手段としては、下記2つの方法を検討しました。

  • ①バッチ処理を使いアプリケーション側でデータの結合を行い新しいデータセットを作るパターン
  • ②データマートを用意しアプリケーションはデータ結合は意識せずにデータを利用するパターン

MLプロダクト等で、①のパターンの処理がいくつか動いているのですが、同じようなデータセットを作るバッチ処理が各所で動くのは望ましい状態ではないという課題があり、これを機にデータ基盤を作ることにしました。

と言っても、いきなり完璧なデータ基盤を作るのは現実的ではないのでスコープを絞り小さくデータ基盤を作ることから始めました。 こんな要件を定義しています。

  • 家族ノートのMVPリリースに必要なデータマートを用意するのがゴール
    • BigQuery上にデータレイク/データウェアハウス/データマートを用意
    • データマートとしてはBigQueryのViewを利用しアプリケーションから参照する
  • データマートに必要なデータレイクを最小限でBigQuery上に整えていく

この構成を考えるにあたり下記の書籍が非常に参考になりました。

データマネジメントが30分でわかる本 | ゆずたそ, はせりょ, ゆずたそ | 経営情報システム | Kindleストア | Amazon

Viewでデータマートを作るには、BigQuery上に必要なデータが揃っている(データレイク)必要があります。 以降は、Aurora(AWS上)にある業務データをBigQueryに移送するデータレイク作成の仕組みについて検討及び実装したの内容を紹介していきます。

AWS→BigQueryにデータ移送するアーキテクチャのpros&cons

AWS(Aurora)からBigQueryにデータを移送する手段ですが、AWS純正のものがあればそれを使うのが一番筋がいいだろうと考え、BigQueryにインテグレートできるような機能を探しましたが今のところはなさそうでした。

そこで、データの抽出/変換/移送を行うETLツールとそれを動かすワークフローについて、AWSで構成を作るにはどんなパターンがあるかをざっくりpros&cons形式で出し比較検討しました。

※個人の主観が入ってますので参考程度に

目指す状態

アーキテクチャ選定においては、下記観点を満たせる構成をゴールに設定しました。

  • 開発者もしくは必要な人が誰でも新しいETL処理を追加・削除出来る
    • コード化されておりかつ簡単なDSLで記載出来る方がよりよい
    • インフラ or データエンジニアしか管理・更新出来ない状態にはしない
  • できるだけワークフローの運用コストが低い
    • マネージドサービスもしくはそれに親しい運用コストが理想
    • 専属で面倒見る人まだいないので、オレオレの運用が発生しないという部分のウエイトを上げる

※構成比較の全体像 f:id:nagais:20210517095316p:plain

ETLツール

コード管理の容易さとプラガブルな構成による柔軟性からEmbulkを採用しました。

Embulk 

  • pros
    • 豊富なプラグインで柔軟な処理
      • フィルタである程度整形も出来る
    • 社外での活用事例も多数あり信頼出来る
    • yamlで簡単な記述
    • 処理をembulk内で完結出来る
    • 使い慣れてるのはある(うちでも既に運用実績はあり)
  • cons
    • Embulkの運用(バージョン管理,エラー時は調査が必要)
      • 重要な意味を持ち出した時にメンテ続けるのは結構大変ではある
    • Embulk実行リソース(Fargateにすることでサーバレスには出来る)

Glue+S3+DataTransferService(BigQuery)

  • pros
    • 全てマネージドサービスなので運用コストが低い
      • どんな処理をするかを定義すれば後はおまかせ
  • cons
    • 各マネージドサービスのコード管理が必要(terraformでやれそう)
    • Glue, DataTransferServiceそれぞれの制約と向き合う必要
    • DataTransferServiceの知見がなく事例も薄い(ちょい検証して実地で知見をためてくアプローチ)
    • DataTransferServiceはスケジュールベースでしか動かないかも(リトライ考えるとトリガ形式にしたい)

Glue+S3+Lambda+GCS+CloudFunctions

  • pros
    • GCSにcsvを置くとBQにデータ流すという構図が作れる
    • s3に置くまでとGCS後の世界が分けれるので応用は効く
      • s3にファイルを置けばデータソースが何であれ同じ仕組みでBQに取り込めるみたいな
  • cons
    • ピタゴラスイッチになるので、構成を把握するのが大変
    • 扱う要素が多くなるので管理コストは高い
      • 全部terraformで賄わないと変更箇所多数で運用破綻するかもな

ワークフロー

運用コストの低さをワークフローエンジンに求めたかったのが大きくStepFunctionsを採用しました。 DigDagでもFargateをキックする構成で近い構成を取ることはできそうだったのですが。DigDag自体の運用が残る点がネックとなりました。

DigDag

  • pros
    • 全てDigDagのDSLでコード管理
    • Embulkと好相性(Fargateキックパターンもあり)
    • BigQueryのジョブも管理出来る可能性ある(やってみないとわからないけど)
    • 管理画面もある
  • cons
    • 学習コスト
    • DigDag自体は常に存在している必要がある(ECSサービス化)
    • DigDagデプロイするときには注意が必要なのかな??(検証してみる必要がある)
  • 検討の中で追記
  • デプロイにしても何にしてもワークフロー自体をうまく回すことを意識して運用しないといけないよな・・・
  • コンテナでやる場合には、セッションやデータを管理する必要がある(イミュータブル対応)
  • 永続ディスクが必要(ECSでボリュームマウント)
  • PostgresSQLをRDSで立ててそこにデータを保存するようにする

StepFunctions

  • pros
    • ワークフローの運用は不要
    • 使い慣れている
    • AWSコンソール上からだがリトライやジョブ可視化可能
    • AWSリソースとフレンドリ
      • AWSサービス使うとかなった際に楽
  • cons
    • コード管理しないと運用破綻する
      • DWH構想用のコード化
  • 検討の中で追記
  • AWS SAMでStepFunctionsとEventBridgeを管理すればコード管理出来る
  • 共通Dockerイメージ作って、SFで Iterator を使うことで、呼び出し時にテーブル名を指定する形にすれば * 取得の時に簡素に書くことも可能かもを検証

StepFunctions+Embulk(ECS×Fargate)を利用したデータレイクの仕組み

f:id:nagais:20210517102541p:plain

コード管理

誰でも簡単にデータレイクに手をいれれる構成にこだわっていたので、データレイクに関わる必要なリソースはすべて1リポジトリで管理する構成にしました。

  • AWSリソース(StepFunctions,EventBridge等)はSAM(AWS サーバーレスアプリケーションモデル)を使って管理
  • データレイク処理追加時に1リポジトリの修正で済むようにembulkのコードと同じGitHubリポジトリで管理しています。
  • 基本的に新しいテーブルをデータレイクに追加したい時は、コピペベースでPR作ることで追加ができるようにしています。
  • デプロイも自動化しており、embulkのDockerイメージのbuild&pushとSAMのデプロイがmainマージ時に実行されます。

ディレクトリ構成はこんな感じです。

embulk(embulkの定義)
 |--conf/ (embulkの定義)★
sam(AWS SAMの定義)
 |--statemachine/ (StepFunctionsの定義)★
 |--functions/ (Lambdaの定義)
 |--env
   |--dev(dev環境用の定義)
     |--samconfig.toml (SAMデプロイ時の設定)
     |--template.yaml (SAMの定義 ※CloudFormationにジェネレートされる)
   |--prd(prd環境用の定義)
     |--samconfig.toml (SAMデプロイ時の設定)
     |--template.yaml (SAMの定義 ※CloudFormationにジェネレートされる)

StepFunctions

できるだけ運用コストを低くするために、

【ポイント】

  • StepFunctionsのステートマシンはデータソース単位で用意
    • 並列(Parallel)でFargateタスク(embulkプロセス)を呼び出すことでエラー時の他への影響を極力小さくする
  • コスト削減のためにFargate Spotを100%利用するクラスタで動かす
  • Fargateタスクなので、処理の干渉が起きない
    • 夜間なので今の所問題になっていないがデータソースに一気に繋ぐとデータソース側がパンクする可能性があるが、その時はステートマシン内のフローを組み替えることで対応予定
  • StepFunctions側でリトライ制御もいれており、単純リトライでもエラーになった際はエラー通知

参考までにSAMで管理している実際のステートマシンのコードの一部をお見せします。 VSCodeのプラグインを使うことでステートマシンを描画できるのはめちゃくちゃ便利でした。

{
    "Comment": "Definition of Embulk StateMachine",
    "StartAt": "Parallel",
    "States": {
      "Parallel": {
        "Type": "Parallel",
        "Next": "Final",
        "Catch": [
          {
            "ErrorEquals": [
              "States.ALL"
            ],
            "Next": "NotifySlackFailure"
          }
        ],
        "Branches": [
          {
            "StartAt": "users",
            "States": {
              "users": {
                "Type": "Task",
                "Resource": "arn:aws:states:::ecs:runTask.sync",
                "Parameters": {
                  "LaunchType": "FARGATE",
                  "Cluster": "${EcsCluster}",
                  "TaskDefinition": "${TaskDefinition}",
                  "NetworkConfiguration": {
                    "AwsvpcConfiguration": {
                      "Subnets": [
                        "${Subnets}"
                      ],
                      "SecurityGroups": [
                        "${SecurityGroups}"
                      ],
                      "AssignPublicIp": "ENABLED"
                    }
                  },
                  "Overrides": {
                    "ContainerOverrides": [
                      {
                        "Name": "${ContainerName}",
                        "Command": [
                          "/embulk/bin/embulk",
                          "run",
                          "conf/hoge/users_bigquery.yml.liquid"
                        ]
                      }
                    ]
                  }
                },
                "End": true,
                "Retry": [
                  {
                    "ErrorEquals": [
                      "States.ALL"
                    ],
                    "IntervalSeconds": 3,
                    "BackoffRate": 2,
                    "MaxAttempts": 1
                  }
                ]
              }
            }
          },
          {
            "StartAt": "children",
            "States": {
              "children": {
                "Type": "Task",
                "Resource": "arn:aws:states:::ecs:runTask.sync",
                "Parameters": {
                  "LaunchType": "FARGATE",
                  "Cluster": "${EcsCluster}",
                  "TaskDefinition": "${TaskDefinition}",
                  "NetworkConfiguration": {
                    "AwsvpcConfiguration": {
                      "Subnets": [
                        "${Subnets}"
                      ],
                      "SecurityGroups": [
                        "${SecurityGroups}"
                      ],
                      "AssignPublicIp": "ENABLED"
                    }
                  },
                  "Overrides": {
                    "ContainerOverrides": [
                      {
                        "Name": "${ContainerName}",
                        "Command": [
                          "/embulk/bin/embulk",
                          "run",
                          "conf/hoge/children_bigquery.yml.liquid"
                        ]
                      }
                    ]
                  }
                },
                "End": true,
                "Retry": [
                  {
                    "ErrorEquals": [
                      "States.ALL"
                    ],
                    "IntervalSeconds": 3,
                    "BackoffRate": 2,
                    "MaxAttempts": 1
                  }
                ]
              }
            }
          },

※embulkについては、すでにwebにも様々な事例があり特殊なことはしていないので詳細は省略します。

おわりに

この構成を作ってから半年以上経過していますが、今のところ特段大きな問題が起きておらず、意図した通りに低い運用コストで毎日動いています。 ※Fargateのエフェメラルストレージが200GBになったのも追い風になっています(20GBだったのでいつか限界に達したら手を加えなければと思っていた..)

StepFunctionsは他の用途でも活用しているのですが、AWSでワークフローを動かす時の選択肢としてかなり優秀だなと感じています。 AWSでワークフローを組む必要がある際には、一番の選択肢として考えていいのではないでしょうか。

最後に、今回紹介したデータを元にして開発している家族ノートを一緒に育てていってくれるエンジニアを募集しています。 少しでも興味をもたれた方は、ぜひ一度お話させてもらえるとうれしいです。

hrmos.co

Storybookを利用した開発フローの設計

こんにちは、リードエンジニアの @dachi_023 です。最近書きたいことが多くて今月3本目の記事です。今回はレビュー時にStorybookを使っている話です。チームによっては使うのが当たり前くらいになってきているツールでもありますが今一度「なぜ入れているのか」「どう役に立つのか」といった観点で振り返ってもらえればいいなと思っています。

新規開発時の課題

これまでUIの実装を始める時はボタンやラベルといった粒度のコンポーネントを実装して後からレイアウトを組んだりコンポーネントを埋め込んだりする、という流れで進めていました。しかし、新規アプリケーションで先にコンポーネントを実装してしまうと実際に動作しているページがなく、レビュー時に確認しづらいという課題がありました。

レビューのためのStorybook

この問題を解決するためコンポーネント実装時にStorybookへコンポーネントを追加しておき、描画されたコンポーネントを使って動作を見てもらうのはどうか?と思いやってみることにしました。また、コネヒトでのStorybookはUIカタログとして参照するのが主で、テストやレビューで使うこともほとんどなかったのでもっと活用できたらいいなという想いもありました。

ちなみに私の場合はコンポーネントを実装したらローカルで適当なページを用意してそこに描画して試す、みたいなことをやっていました。こういう非効率なやり方はどんどん改善されるべきだと思います(自戒)。

開発フローを改善する

UI開発時のフロー

  1. コンポーネントの実装(テスト、UI実装)
  2. Storybookにコンポーネントを追加
  3. Pull Requestを作成・レビュー依頼

これまでコードを書いてすぐレビューを依頼していたところをStorybookへの追加までをセットでPull Requestを作成するルールにしてみました。その結果、当初想定していた以上にいい効果がありました。

レビュー時の指摘がしやすい

今回の大目的であった部分は無事改善されました。Storybookを見ながらUIに対する指摘ができるのがレビュー時にとても便利で、Propsをその場で編集することで想定外の挙動をしないかなどを確認できるようになりました。他にも @storybook/addon-a11y を使うことでコンポーネントごとのWebアクセシビリティが担保されているかの確認ができるので、機械的に判定できる部分の指摘を減らせました。

Propsの編集 a11yチェック
Props の操作 @storybook/addon-a11y

Storybookのアドオンは他にもあるので目を通してみて「うちのチームはこれ入れると便利そう」といったものを見つけてみるといいかもしれません。また、他社事例なども非常に参考になるので「Storybook 運用」などで検索するのもおすすめです。

Addons | Storybook

UIのパターン漏れに気づきやすい

「エラー時に色が変わる対応が漏れていた」といったミスにレビューを出す前からある程度気付けるようになりました。過去にこういったことが何度かあったのですが、Storybookを使うようにしてからは Story を追加するにあたってどういうパターンがあるかを事前に洗い出すようにしたのでミスを大幅に減らせました。

通常時のUI エラー時のUI
通常時 エラー時

Storybookの運用が安定する

必ずStorybookに追加するようにしたのでこれまで起きていた追加・修正漏れがなくなりました。開発フローの改善をするつもりでしたが結果としてStorybookの運用フローも整いました。カタログ用途として運用しているとCIが失敗するわけでもないので追加し忘れがちだったのですが解消されました。

まとめ

スナップショットテストやカタログなどチームによって用途は異なりますが、コンポーネント単位でのレビュー時に動作確認をする場としても活用できるよという話でした。また、Storybookの追加漏れ防止など運用面でもいい効果があったのでちゃんと運用されずに困っている人にもオススメです。

この記事はShodoで執筆されました

KDDIグループでテックカンファレンスを開催しました

こんにちは、リードエンジニアの @dachi_023 です。2021/03/17(水)にKDDIグループ6社合同でテックカンファレンスを開催したので弊社から登壇したメンバーの発表内容を掲載しています。また、私が運営メンバーとして参加させてもらったのでその感想も少し載せています。

https://kgdc.connpass.com/event/203487/

KGDC Tech Conference

KDDI Group Developer Community

KGDCは「KDDI Group Developer Community」の略です。

KDDIグループ各社のエンジニアが集って技術的な発信をしていこう!という取り組みです。今回が初開催だったのですが、当日は多くの方が視聴してくださりとてもうれしかったです。視聴いただいた皆さんありがとうございました。

パネルディスカッション

パネルディスカッションは2テーマでの開催となっており、それぞれ技術・開発組織に関するものとなっています。弊社からはCTOの @itosho が技術枠で登壇しました。技術関連の意思決定で重要にしていることなど、会社や事業によって大きく変わってくるんだなというのを強く感じました。

セッション・LT

弊社メンバーのスライド一覧です。connpassに会全体の発表資料も一部ですが下記より閲覧できます。

https://kgdc.connpass.com/event/203487/presentation/

運営メンバーに入ってみての感想

今回は企画メンバーの1人として参加させてもらったのですが、大人数で運営するような会に参加したことがなかったので新鮮な気持ちでした。また、会全体のテーマ・コンテンツを同じチームの皆さんと一緒に考えたりするのも楽しかったです。KDDIグループ同士ではあったのですが交流する機会がほとんどなかったので、新しい取り組みを通して新たな機会が生まれたというのも非常に良かったです。

さいごに

初めてのカンファレンス運営でしたがとても楽しく取り組むことができました。今後も続けていく予定ですのでぜひconnpassグループのメンバーになって開催通知を受け取ってもらえるとありがたいです!よろしくお願いします。

https://kgdc.connpass.com/

この記事はShodoで執筆されました。