振り返り方法の紹介

backcheck事業部の前田です。

backcheck開発チームでは2週間ごとに振り返りを実施しています。
わりといい感じなので、手法と気をつけているポイントを紹介します。

・・・と言いつつも、特に凄いことをやっているわけではなく、とても一般的な方法です。

振り返りの目的

詳細な話を始める前に、振り返りの目的をまとめておきましょう。

なぜ振り返りをするのでしょうか。
それは、プロセスや文化、組織をより良くするためです。
では、なぜプロセスや文化、組織を良くしないといけないのでしょうか。
それは、プロダクトの価値は、プロセスや文化、組織に反映されるからです。

ソフトウェアの価値は「技術力」によってのみ支えられていると、エンジニアは盲信しがちです。
それは間違っていないのですが、プロセスも同等に重要です。
たとえば誰も要らない機能を「とてもすごい技術力」で作ったとしても、プロダクトの価値は全く上がらないでしょう。
むしろ価値としては下がることもあるでしょう。

また、技術力は抜きにして、無駄な作業を減らせばその分、価値のある機能を作る時間を増やせるかも知れません。

成功循環モデルでも、「結果の質を上げるためにはまず関係の質を上げろ」という話が出てきます。

f:id:chiroruxx:20200712124039p:plain

このように、プロセス改善をすることで、より良いものを、より早く、より高品質で作れるようになります。

振り返りの手法

KPTを使用しています。
KPTを使用している理由としては、「一番メジャーだから」です。
なぜメジャーなものが良いのかというと、情報量が多いからです。
KPTについて気になることがあったとき、メンバーは自分で検索をして調べることができます。
逆にFun Done Learnなどのマイナーな方法を使用すると、まだまだ文献が出回っていないので、「調べたけどよくわからなかった」となりがちです。
ファシリテータに知識が充分にあり、振り返りに慣れている場合にはマイナーな方法でも良いかもしれませんが、私のチームは振り返り初心者なので、メジャーな方法を選択しました。

KPTの概要

ざっとKPTの概要をまとめます。
まず、以下のものをメンバーに挙げてもらい、共有します。

Keep: 良かったこと・継続したいこと
Problem: つらかったこと・問題だなと感じたこと

その結果から、

Try: 改善したいこと・試してみたいこと

を導き出し、改善策を見つけます。

振り返りの参加者

開発メンバーとプロダクトマネージャーで行っています。
また、振り返りで参加できる人数のは3~5人までにしています。
2人以下の場合は、あまり相乗効果を期待できないので、形式張らずにラフな話し合いにします。
6人以上になると発言の機会が減ったり、ダラダラした雰囲気が出ます。
その場合は2チームに分け、後で合流して共有をします。

振り返りの時間

5人で2週間分の振り返りで、60分程度です。
始めたばかりの頃は100分程度かかっていました。

振り返りの流れ

以下の流れでやっています。

  1. (場の設定)
  2. 前回のTryの復習
  3. Keepの共有
  4. Problemの共有
  5. 改善したい項目を決める
  6. Tryの作成
  7. まとめ

ひとつずつ見ていきましょう。

場の設定

振り返りに限らず、ミーティングをする場合は「場の設定」を行います。
アイスブレイクなんかがそうですね。

アジャイル・レトロスペクティブ」という本ではアイスブレイクは振り返りの場の設定に望ましくないと書かれていますが、私は全然アリだと思います。
むしろ紹介されている「チェックイン」の手法を使用したりすると、日本人にあまりない文化のため、メンバーの頭に「?」が浮かんだ状態になったりします。

私はここでの目的は

  • リラックスする
  • 発言しやすくする

などを置いています。
特に「一度も発言していない状態から発言する」というのは心理的にやりにくいので、場の設定のタイミングで全員一度は何かしらの発言をしてもらうようにしています。
「ooさんは今日のお昼、何食べました?」とかでオッケーです。

・・・と、長々と書きましたが、私のチームでは省略することが多々あります。
私のチームでは振り返りは、いわゆる「ミーティングデー」の一環として行われるためです。
振り返りの前のミーティングで既に場が設定されている場合、場の設定はスキップしてしまいます。

前回のTryの復習

前回の議事録を見ながら、

  • 前回のTryをやったか
  • 前回のTryをやってどのように変化したか

を確認します。
ここでのゴールを「Tryをやったかどうか」と誤認してしまう人が多いのですが、ここでのゴールは「Tryをやった結果、改善されたか」です。
ここで「毎回Tryを忘れる」という場合は、Tryをやる仕組みや習慣づくりについて話したほうが良いでしょう。
「忙しくてできなかった」という場合は、「木こりのジレンマ」になっている可能性があります。
Tryのコストが高いか、業務との優先順位をどのように付けるかについて話しましょう。

Keepの共有

以下の手順で行っています。

  1. それぞれでKeepを書き出す(3分)
  2. Keepの共有

以下、注意している点です。

Keepの対象

そのまま和訳すれば「継続」ですが、ここでは単純に「良かったこと」も挙げてもらっています。

振り返りは「反省会」ムードになりがちです。
良かったことを先に挙げてもらうことで、振り返り全体の空気感を明るくし、前向きに振り返りをできる環境にする側面もあります。

Keepの書き方

なるべく付箋など、文字数に上限を決められるもので書きます。
Keepに文字を書きすぎてしまうと、後で全体を俯瞰して見づらくなってしまいます。
「最小限でかつ伝わる」書き方で書くようにします。

Keepを書くとき

そのまま開始するとなぜか「他の人と話してはいけない」という暗黙のルールが形成されるので、 「他の人と相談しても全然OKです!!」ということは明示的に伝えます。

共有方法

順番を決めて、最初の人が1枚共有したら次の人が1枚共有し、さらに次の人が・・・と続けていきます。最後の人まで回ったら最初の人に戻り、全員がすべてのKeepを共有するまで行います。

共有のタイミングでは、長く話しがちになります。
「その人のすべてのKeepを共有してから次の人が共有する」という手法を取ると長く話しやすくなってしまうので、「1枚共有したら次の人」で、必要・重要なことのみを共有してもらいます。

Problemの共有

以下の手順で行っています。

  1. それぞれでProblemを書き出す(3分)
  2. Problemの共有

基本的にはKeepの共有と同じことに注意していきます。
Problemではそれに追加して以下を注意します。

解決策は出さない

問題を考えると一緒に解決策も提示したくなりますが、ここでは解決策は出さないようにします。
基本的に1人で考えた解決策よりも、複数人で考えた解決策のほうが優れています。
三人寄れば文殊の知恵ですね。

なので、解決策はTryを作成する際に、チームで考えるようにします。

特定の人への攻撃

(私のチームでは発生してませんが・・・)

「ooさんのせいで」など書かれているものが共有された場合は要注意です。
基本的に「チーム vs 問題」という構図を目指していくので、このような場合は、その問題の背景にあるプロセスや慣習の問題を考えるように話の方向性を導きます。

あまりにこのケースが多いようであれば、振り返り後に個人的に説得をするか、全体で「なぜ個人を攻撃してはいけないか」を説明し、ルール化したほうが良いです。

改善したい項目を決める

挙がったKeep、Problemから改善したい項目を決めます。
ひとり3票で、得票数の多い1~2つについてTryを考えていきます。

このプロセスはTryを絞るだけでなく、「個人の関心事」を「チームの関心事」に変化させる役割もあります。
たまにTryを考える際に「私のためにみんなに負担かけさせたくないです」のような事を仰る方がいます。
投票という行為を通じて「あなたのためじゃなくてチームのためにやるんですよ」という流れを作ります。

以下、注意点です。

KeepもOK

改善というとProblemに目が行きがちですが、Keepに票を入れても全然OKです。
Keepが選ばれた場合は、「それをより良くするためにはどうするか」がTryになります。

Tryにできるのは2つまで

挙がった問題すべてに対処したくなりますが、グッとこらえて対応するものを絞ります。

すべてに対応しようとすると、ひとつひとつの精度が落ち、「Tryだけどやれなかった」ものが多くなりがちになります。
また、人間は大量の変化には馴染みづらいので、文化や慣習にならずにただ「Tryを実行しただけ」になってしまいます。
さらに、プロセス改善をしすぎると、その分、業務する時間が減り、「プロセス改善してたので業務が進みませんでした」のような事象が発生します。

そのため、なるべくやる価値の高い変化だけに絞り込みます。

もし選ばれなかったものの中に重要なものが紛れている場合、次回にも同じものがKeepかProblemに挙がってくるはずです。 そしてそれが本当に重要なのであれば、次回、投票で選ばれ、改善されるはずです。

Tryの作成

投票で選ばれたものについて、「どのようにすれば改善できるか」をチームで考え、改善項目を出していきます。

ここでは、以下の流れで行っています。

  1. 投票で選ばれたものの事象についての深堀りと理解
  2. Tryを作成する

以下、注意点です。

事象についての共有理解をつくる

事象については誰かが観測してKeep, Problemに挙げたものです。
そして、投票を通じてチームがその事象に関心を持っていることがわかっています。

KeepやProblemを共有する場ではなるべく必要最小限の共有に抑えていたので、ここで詳細な情報をチームで把握します。
この事象を挙げた人に詳しく話してもらったり、他の人からその事象がどのように見えているかについて話すと良いです。
必要であれば「5つのなぜ」や「根本原因分析」などのテクニックを使用するのも良いでしょう。

「チーム vs 問題」を意識する

「ooさんのせいで」のような話になると裁判になってしまうので、なるべく「チーム vs 問題」になるようにします。
「私の能力が低かったので」も同じです。チームの話にシフトさせていきましょう。

基本的に、人間性や能力などは前提条件なので覆すことができません。仕組みやプロセスを調整して問題解決できるようにしていきます。

発散して収束させる

一般的に、「最初に出された解決策はあまり役に立たない」と言われています。
Tryを作成する際は、ブレストの要領でまず広く解決策を募ります。
ここでの敷居はなるべく下げたほうが良いため、ファシリテータは率先して敷居を下げられるような解決策を出していくと良いでしょう。

ある程度、解決策が出揃ったら、解決策をまとめていきます。
実現不可能なものや効率性の悪いものを落としたり、複数のアイデアを合体させたりします。

最終的に、各事象に対してのTryは1~2つ、全体でのTryは3つまでに収まるように調整します。

ベイビーステップを意識する

作成したTryが大きすぎる場合(次の振り返りまでの完了しない場合)は、そのTryを分解し、「最初の一歩」の部分をTryとします。

振り返りのまとめ

最後に、Tryのもととなった事象とTryの内容をまとめ、振り返りを終了します。

記事のまとめ

backcheck開発チームで行っている振り返りの方法をまとめてみました。

改めてKPTの構図を見てみると、「デザインのダブルダイヤモンド」のような、2回の発散と収束があることがわかりますね。

KPTは一見簡単そうに見えますが、結構考えるべきポイントが多く、難しかったりします。
この記事がみなさんのKPTライフの参考になれば幸いです。

Vue.js $emit 使わないで props で method 渡したほうが良くない?

これは、個人ブログ からの転用です ROXXに入って学んだことの1つです。

概要

Vue.js で 親コンポーネントの method 実行させたい場合、$emit 使ってイベントを発火させるより、
props に method をコールバックとして登録しておいて実行させたほうが以下のメリット上げられるので「こっちのほうが良くね?」って話です

  • props の成約をつけられる(requird, etc)
  • $emit の文字列を管理しなくていい
  • IDEで補完が効く

実際のコード

ボタン押したらカウントアップしていくようなやつ f:id:akki_megane:20200708203420p:plain

呼び出し側の親コンポーネント

<template>
    <div>
        <div>{{ count }}</div>
        <child
                :handle-add-number="addCount"
                @addNumber="addCount"
        />
    </div>
</template>

<script>
    import child from "./child";

    export default {
        components: {child},
        data: () => ({
            count: 0
        }),
        methods: {
            addCount(number) {
                return this.count += number
            }
        }
    }
</script>


コンポーネント

<template>
    <button @click="countUpProps()">count up props</button>
    <button @click="countUpEmit()">count up emit</button>
</template>

<script>
    export default {
        props: {
            handleAddNumber: {
                type: Function,
                required: true // required をつけて必須に
            }
        },
        methods: {
            // Prop で渡した function を実行
            countUpProps() {
                this.handleAddNumber(1)
            },
            // emit を使って function を実行
            countUpEmit() {
                // emit の event は 文字列で管理
                this.$emit('addNumber', 1)
            }
        }
    }
</script>


props で定義しておけばこんなふうにIDEで補完が効きます f:id:akki_megane:20200708203603p:plain


こんなかんじで、props を使うと、
requird で縛れて、method の渡し忘れを防げたり、
$emit の文字列管理しなくていいかつ、IDEで補完が効くのでその分typo が防げる という点で開発しやすくなるかなと思います。

TS でやると

Vue も Vue3 から、TypeScript を正式にサポートということで未来を見据えて、
同じ処理を Vue3 + TypeScript で書いてみます


呼び出し側の親コンポーネント

<template>
    <div>
        <p>{{ count }}</p>
        <child
                :handle-add-num="addNumber"
                @addNumber="addNumber"
        />

    </div>
</template>

<script lang="ts">
    import {defineComponent, ref} from 'vue';
    import child from "./child.vue";
    import AddNumberInterface from "../types/AddNumberInterface"; //function の Interface

    export default defineComponent({
        components: {
            child
        },
        setup() {

            const count = ref<number>(0)

            //Interface を指定して関数を定義
            const addNumber: AddNumberInterface = (num: number) => {
                return count.value += num
            }

            return {
                //data
                count,

                //function
                addNumber
            }
        }
    })
</script>


props で渡す関数の Interface 定義

export default interface AddNumberInterface {
    (num: number): number
}


コンポーネント

<template>
    <div>
        <button @click="countUpProps()">count up props</button>
        <button @click="countUpEmit()">count up emit</button>
    </div>

</template>

<script lang="ts">
    import { PropType, defineComponent , SetupContext} from 'vue';
    import AddNumberInterface from "../types/AddNumberInterface";

    type Props = {
        handleAddNum: AddNumberInterface; //Prop の Interface を定義
    }

    export default defineComponent({
        props: {
            handleAddNum: {
                // PropType を使って Prop の type に使いたい関数のInterface を指定
                type: Function as PropType<AddNumberInterface>,
                required: true
            }
        },
        setup(props: Props, context: SetupContext) {
            const countUpProps = () => {
                props.handleAddNum(1)
            }

            const countUpEmit = () => {
                context.emit('addNumber', 1)
            }

            return {
                countUpProps,
                countUpEmit,
            }
        }
    })
</script>


PropType(これは Vue2.6 からあったはず?) と TypeScript を使うことにより、props の Typeを独自のInterfaceに変更することができ、
どんな functionを渡せばいいか、明示することができました!
「Vue ぽくない」とか言われそうですが、 という概念が好きな私にとっては、とても書きやすく感じました。

まとめ

公式のリファレンスや、色々な書籍でもVueで親のmethod を実行した場合は、$emit を使う、
というのは当たり前のように記載されていますが、実際書き比べてみたり、実際のコードを運用してみた観点からしても、
props で method を渡したほが、明示的かつケアレスミスを減らすことができて、とても良いと感じています。
$emit を使う意義を感じなくなってきているので、$emit を使う理由や、もっとよい方法があれば教えてもらえるとありがたいです。
未来のことはわかりませんが、Vue3 からは TypeScript サポートされより型を意識した開発をするように今後なっていくのだとしたら、 上記のような書き方はさらに恩恵を受けそうだなと思っています。

ISMSの内部監査に向けて行ったこと

CTO室情報システム担当の吉澤です!

前回ISMSとPマークの違いについて 書かせていただきました。 今回も引き続き紹介させていただきます。

 

先日、ISMSの内部監査を行いました。ROXXでは2回目、私のキャリアとしては初の監査となりました。

今回の記事ではこの内部監査にお話します。

監査に必要なタスクの洗い出し


監査に備え、必要なタスクを洗い出しました。

マインドマップを利用したので、イメージをつかんでいただければと思います。

 

f:id:wakanayoshizawa:20200618101425p:plain

このマインドマップで全体のタスク量を把握しつつ順に準備を進めていきました。

 

各部署へのヒアリング内容

具体的な業務は各部署ごとにヒアリングをしながら項目の確認をすすめていきます。

ヒアリング内容は主に3点を担当しました。

3点とはいえ業務としては多岐にわたっていたので、作業として最も時間を要したリスク管理について書いてみたいと思います。

リスク管理について リスクアセスメントでは、生じる恐れがあるすべてのリスクを想定します。

次に、リスクごとに発生頻度、業務・会社への影響度を設定していきます。

そして、発生頻度と影響度が高いリスクについて、対策を年内実施する計画としてタスクに落とし込みます。

今回は内部監査の日程が控えていたため、発生頻度と影響度が高いと想定されたもののうち、内部監査までに対応できそうなものを中心にすすめてきました。

承認フローの見直しなど内部監査に間に合わない、長期的に実施するべきもの については年次の計画の中で対応してく予定です。

 

内部監査を実施した印象

 

私の実務としてはリスク管理に関してオペレーションに最も時間がかかったのですが、内部監査では、情報区分、教育、手順やプライバシーポリシー及び情報セキュリティ方針の運用状況について指摘される事項が多かったです。

事前の自分のイメージと現実のずれも感じとても勉強になりました。

最後に ROXXではAgent Bank、Back Checkとサービス及び組織が急拡大しています。

このような状況の中でISMSの内部監査を体験できたことは、自分にとって大きな経験となりました。

ISMSについては引き続き本番の審査、年次計画もあるので、継続的な運用に努めていきます。

LaravelをECS上で運用するTips

こちらの記事は下記ブログと同じものになります。 kotamat.com

今まではEC2上でLaravelを動かしてきたが、CVEの対応など、定期的にミドルウェアをアップデートする仕組みとして、VMレベルでのプロビジョニングをするのが大変になってきたので、Dockerコンテナ上で動く仕組みを考える必要が出てきた。

Dockerコンテナ上で動かす仕組みとしてPreview環境ではk8sを採用しているものの、メンテナンス性において社内にECSを実際に運用したことのあるメンバーがいるという観点から、安全をとってECSの採用を検討した。

ECSをLaravelで採用する上で、特に運用面にていくつか考慮しなければならない点があったので、本記事でまとめる。

TL; DR

  • ログ出力のため、標準出力・標準エラーへの反映を行う
  • Terraform上で.envを作成、S3にあげてからGithubActionsで取り回す
  • コンテナのデプロイ後を、CloudWatch Event + Lambdaで検知する
  • Batchのコンテナサービス化か、Scheduled Task + On-demand migrationか
  • キューワーカーはServiceのサイドカーで起動

以下上記を解説していく

ログ

Laravel側

標準の stack ログチャンネルにおいては、エラーログをmonologを用いた stderr へのストリーミング送信、通常ログを laravel.log へのファイル出力としている。 一方ECSでは、docker log で出力されるログをログとして収集しているため、stderrでの出力は検知できるものの、通常のログは検知する事ができない。

下記のようにファイル出力をstdoutにリダイレクトしようと思っても、PHPがファイルを見つけられずにエラーになってしまう

FROM php:fpm-alpine

# ... 諸々のプロビジョニング

COPY . /var/www/html
RUN ln -sf /dev/stdout /var/www/html/storage/log/laravel.log

直接Laravelからstdoutに出力する方法は下記のようになる。

return [
    'default' => env('LOG_CHANNEL', 'stack'),
    'channels' => [
        'stack' => [
            'driver' => 'stack',
            'channels' => ['stderr', 'stdout'],
            'ignore_exceptions' => false,
        ],
        'stdout' => [
            'driver' => 'monolog',
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stdout',
            ],
        ],

        'stderr' => [
            'driver' => 'monolog',
            'level' => 'error',
            'handler' => StreamHandler::class,
            'formatter' => env('LOG_STDERR_FORMATTER'),
            'with' => [
                'stream' => 'php://stderr',
            ],
        ],
        // ...
];

こちらにすることによって、docker logに出力されるようになるのだが、php7.2以前を使っている場合、下記のように先頭にプロセスが表示されてしまう。

php_1    | [14-Jun-2020 14:14:18] WARNING: [pool www] child 8 said into stdout: "[2020-06-14 14:14:18] local.INFO: this is info log  "
php_1    | [14-Jun-2020 14:14:18] WARNING: [pool www] child 8 said into stderr: "[2020-06-14 14:14:18] local.ERROR: this is exception {"exception":"[object] (Exception(code: 0): this is exception at /var/www/html/routes/web.php:18)"
php_1    | [14-Jun-2020 14:14:18] WARNING: [pool www] child 8 said into stderr: "[stacktrace]"

PHP7.3以降ではPHP-FPMの設定にdecorate_workers_outputというディレクティブが生えているので、こちらをnoにすることによって表示されないようになる。

decorate_workers_output = no

ECS側

ECS側では標準のままだとログが一行ごとに出力されてしまい、スタックトレースなど複数行に渡るエラーが見えづらい。 そこで、TaskDefinitionのログ定義にて、datetime formatを指定することで、datetime formatベースでログをまとめてくれるようになる

[
  {
    // ... 諸々のコンテナの設定
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${awslog_group}",
        "awslogs-region": "${region}",
        "awslogs-datetime-format": "%Y-%m-%d %H:%M:%S",
        "awslogs-stream-prefix": "ecs"
      }
   }
  },
]

デプロイ

基本

GitHub ActionsをCDで用いている場合、GitHub Actions側では下記のようにすることで、TaskDefinitionの変更とデプロイができる

jobs:
  job:
    runs-on: ubuntu-latest
    steps:
    # ...
      - name: Change Task Definition
        id: render-td
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ steps.fetch-td.outputs.task-definition }}
          container-name: my-container-name
          image: ${{ steps.built-image.outputs.image }}

      - name: Deploy to Amazon ECS service
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.render-td.outputs.task-definition }}
          service: ecs-service
          cluster: ecs-cluster

Task Definitionに環境変数をどのように注入するのか

ここで問題になるのはTaskDefinitionの環境変数をどうするかである。 ECSを使う場合、AWSの他のサービスを使うことになると思うが、 Task Definitionで記述される情報の中には当然これらの情報に対するアクセス情報を含める必要がある。

開発環境ではLaravelと同リポジトリ.env.local のようなものを用意しておき、Dockerfile内にて

COPY .env.local .env

のようにすれば.envを適応できるが、本番環境の機微情報をバージョン管理システムにコミットするわけには行かないため、本番運用ではこの方法は使えない。

TaskDefinitionには environmentFiles という設定項目があり、S3に存在する.envファイルを環境変数として使用することができる

{
  "environmentFiles": {
    "value": "<S3のobjectのarn>",
    "type": "s3" // s3のみサポート
  }
}

Terraformで環境を構築しているのであれば、上記ファイルを動的に作成し、Taskロールに付与すればいい

resource "aws_s3_bucket_object" "env_file" {
  bucket  = var.env_file_bucket
  key     = ".env"
  content = <<ENV
DB_CONNECTION=mysql
DB_HOST=${var.db_host}
DB_PORT=3306
DB_DATABASE=${var.db_name}
DB_USERNAME=${var.db_username}
DB_PASSWORD=${var.db_password}
ENV
  acl     = "private"
}

resource "aws_iam_policy" "download_env_file" {
  policy = <<POLICY
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:ListBucket"
      ],
      "Resource": "arn:aws:s3:::${var.env_file_bucket}/.env"
    }
  ]
}
POLICY
}

resource "aws_iam_user_policy_attachment" "env_file" {
  policy_arn = aws_iam_policy.download_env_file.arn
  user       = var.iam_user_name
}

デプロイ後の通知

上記GitHub Actionsの例を用いると、 aws-actions/amazon-ecs-deploy-task-definition@v1 ではwait-for-service-stabilityをtrueにすることによって、デプロイ完了が正常に終わったかどうかを確認することができる。 ただ、GitHubActionsは実行時間の従量課金型であるため、デプロイ方法によっては必要以上の課金が発生してしまう。

CloudWatch Event + Lambdaのログを用いることによってデプロイ状況を通知することが可能なので、そちらの紹介を行う。

Terraformにて下記のようなmoduleを作成し、

tfファイル(長いので省略)

locals {
  base_name = "${var.base_name}-ecs-notification"
}
resource "aws_cloudwatch_event_rule" "main" {
  name = local.base_name
  event_pattern = jsonencode({
    "source" : [
      "aws.ecs"
    ],
    "detail-type" : [
      "ECS Task State Change"
    ],
    "detail" : {
      "clusterArn" : [
        var.cluster_arn
      ]
    }
  })
}

resource "aws_cloudwatch_log_group" "lambda_log" {
  name = "/aws/lambda/${local.base_name}-lambda"
}

resource "aws_iam_role" "lambda" {
  name = local.base_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement : [
      {
        Action = "sts:AssumeRole"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Effect = "Allow"
      }
    ]
  })
}

resource "aws_iam_policy" "lambda" {
  policy = jsonencode(
    {
      Version = "2012-10-17",
      Statement = [
        {
          Effect = "Allow",
          Action = [
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          Resource = [
            aws_cloudwatch_log_group.lambda_log.arn
          ]
        }
      ]
    }
  )
}
resource "aws_iam_role_policy_attachment" "lambda" {
  policy_arn = aws_iam_policy.lambda.arn
  role       = aws_iam_role.lambda.name
}

data "archive_file" "lambda" {
  source_file = "${path.module}/lambda/index.js"
  output_path = "${path.module}/lambda/index.zip"
  type        = "zip"
}

resource "aws_lambda_permission" "from_cw_event" {
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.main.function_name
  source_arn    = aws_cloudwatch_event_rule.main.arn
  principal     = "events.amazonaws.com"
}

resource "aws_lambda_function" "main" {
  function_name    = local.base_name
  handler          = "index.handler"
  role             = aws_iam_role.lambda.arn
  runtime          = "nodejs12.x"
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  environment {
    variables = {
      SLACK_CHANNEL  = var.slack_channel
      SLACK_ICON     = var.slack_icon
      SLACK_APP_NAME = local.base_name
      SLACK_HOOK_URL = var.slack_hook_url
    }
  }
}
resource "aws_cloudwatch_event_target" "main" {
  arn  = aws_lambda_function.main.arn
  rule = aws_cloudwatch_event_rule.main.name
}

以下のようなLambda関数を用意すればいい

Slack通知の関数

'use strict';

const https = require('https');
const url = require('url');

// envs
const SLACK_CHANNEL = process.env.SLACK_CHANNEL
const SLACK_ICON = process.env.SLACK_ICON
const SLACK_APP_NAME = process.env.SLACK_APP_NAME
const SLACK_HOOK_URL = process.env.SLACK_HOOK_URL

exports.handler = async (event, context, callback) => {
    const eventDetail = event.detail
    if (!eventDetail) {
        console.log("The event is not expected", event)
    }

    const message = genNotifyMessage(eventDetail)
    if (!message) {
        return
    }

    const result = await sendSlack(message)
    console.log(result)
}

function genNotifyMessage(eventDetail) {
    const targetStatuses = [
        {
            from: "PENDING",
            to: "RUNNING",
            message: `${eventDetail.group} deploy started`
        },
        {
            from: "RUNNING",
            to: "RUNNING",
            message: `${eventDetail.group} deploy completed`
        }
    ]

    const target = targetStatuses
        .find(({from, to}) => from === eventDetail.lastStatus && to === eventDetail.desiredStatus)

    return target && target.message
}

function sendSlack(message) {
    return new Promise(((resolve, reject) => {

        const formData = {
            channel: SLACK_CHANNEL,
            text: message,
            icon_emoji: SLACK_ICON,
            username: SLACK_APP_NAME,
        };

        const formDataEncripted = JSON.stringify(formData)

        const options = {
            ...url.parse(SLACK_HOOK_URL),
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
                'Content-Length': Buffer.byteLength(formDataEncripted)
            }
        };

        const req = https.request(options, function (res) {
            let chunks = []
            res.on('data', function (chunk) {
                chunks.push(chunk)
            });
            res.on('end', function () {
                console.log(chunks.join(''))
                resolve({
                    body: chunks.join(''),
                    statusCode: res.statusCode,
                    statusMessage: res.statusMessage
                })
            })
        });

        req.write(formDataEncripted);

        req.on('error', function (e) {
            console.log("Sending request error: " + e.message);
            reject(e)
        });

        req.end();
    }))
}

非同期処理系

LaravelにはSchedule taskとJob workerがある。 通常のコンテナではこれらを適切に扱うことができないので、工夫が必要

ScheduleタスクとmigrationのためのBatchサービス

他のサービスを使わない形で手っ取り早く導入するのであればこの形が一番ラク

要件としては下記

  • 起動するコンテナ数は1
  • 起動時のentrypointにてmigrationの実行と、crontabの登録を行う。
  • 通常のserviceと同じタイミングでデプロイ

こうすることによって、バッチコンテナが同時に複数立ち上がることがないため、withoutOverlappingでの制御が可能となり、マイグレーションもデプロイタイミングで実行されるため、他のマネージドサービスを使う必要はない。

やり方も、LaravelのDockerfileで指定しているentrypointに下記を追加するだけ。

# 環境変数で分岐させる場合
if [[ "$BATCH" -eq "1" ]]; then
    apk --no-cache add sudo
    COUNT=0

    while [[ $COUNT -lt 1 ]]
    do
    # DB接続の確認。副作用を起こしたくないので、statusチェックのみ
      php artisan migrate:status --database 'mysql-migrate'
      READY=$?
      if [[ $READY -eq 0 ]]; then
        COUNT=$((COUNT + 1))
      else
        # migrateされていないパターンかも知れないので、installを実行
        php artisan migrate:install --database 'mysql-migrate'
        COUNT=0
      fi
      echo "waiting for finish init process... count: ${COUNT}, ready: ${READY}"
      sleep 1
    done

    echo "migrate"
    php artisan migrate --database 'mysql-migrate' --force
    echo "* * * * * sudo -u www-data php /var/www/html/artisan schedule:run" >> /etc/crontab
fi

CloudWatchを使ったScheduleTaskとGithubActionsでのマイグレーション

よりマネージドで管理する場合は、Scheduleタスクを下記ドキュメントのやり方で毎分実行するようにするとよい。 https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/scheduled_tasks.html

マイグレーションを自動で行う場合はTaskDefinitionに下記を追加する。

jobs:
  job:
    runs-on: ubuntu-latest
    steps:
    # ...
      - name: Run migration
        run: aws ecs run-task --cluster ecs-cluster --task-definition migration-td #必要なオプションは適宜追加

マイグレーションにてAlter tableを用いたマイグレーションを行っている場合、テーブルロックにより実行が止まらない可能性があるので、自動化には注意したほうがいい。

Job Workerはメインのコンテナのサイドカーで実行

Queueを用いる場合、dequeueするコンテナが必要となる。 メインのコンテナとプロセスが別れていればいいので、キューの実行に厳密性が必要なかったり、FIFOで設定されていれば、サイドカーにしてしまって問題ない。

下記のようにentrypointをartisanでも実行可能な形にしておけば

if [[ "$1" == *artisan* ]]; then
    set -- php "$@"
else
    set -- php-fpm "$@"
fi

exec "$@"

TaskDefinitionでcommandを上書きすることでqueue worker用のコンテナを実行できる

[
  {
    "name": "${container_name}-worker",
    "command": [
      "artisan",
      "queue:work",
      "sqs",
      "--sleep=3",
      "--tries=3"
    ],
    // ... 諸々のコンテナの設定
   }
  },
]

終わりに

ECS上で本番運用する上でのトピックをいくつか紹介させてもらった。 紹介した中でもっといい方法などあればFBいただけると嬉しいです。

Github Actions でブランチの操作を行う

こちらのブログは個人ブログと同じ内容です

www.ritolab.com

少し前までは CI/CD を回そうと思ったら Circle CI や Travis CI を Github と連携させたりしていましたが、GitHub Actions が使えるようになってからは Github だけで Ci/CD も完結できるようになりました。(正式版は 2019年11月13日に提供開始)

今回は GitHub Actions を使って色々な操作を試してみたいと思います。

GitHub Actions

GitHub Actions は、Github 上でワークフローに沿ったそのリポジトリについての処理を実行させることができるサービスです。ワークフローは yml で記述します。
https://github.co.jp/features/actions

CI/CD はもちろん、そのリポジトリに関しての操作など結構色々な事が行えます。

また、よく行われているような操作に関しては、GitHub Marketplace で Action がたくさん提供されています。 自分でワークフローの全てをゴリゴリ記述しなくても、アクションを入れてしまえば済むものも多く、結構簡単に使う事ができます。

GitHub Marketplace
https://github.com/marketplace?type=actions

無料・有料それぞれで使用できる範囲

使ってみようとなると、無料だとどこまで使えるのかっていうのは気になるところです。

パブリックリポジトリに関しては、特段「課金」というものはなく無料で使えるようです。

プライベートリポジトリに関しては、無料プランを含む料金プランごとにそれぞれ「同時実行ジョブ数」「ストレージ」「実行時間」が割り当てられていて、制限を超えた場合は、課金しないとその月に関してはそれ以上使えなくなる感じになっています。

予め課金のリミットを設定しておいて、その設定金額分までは超過しても後で請求とかにもできるみたい。(リミット無制限なんて事もできる)

無料ユーザの場合、月 2000 分まで実行が可能(Linux)なので、個人で使うなら十分ですね。

ただし、消費時間の計算が、どの OS で動作させるかで異なるので、Linux 以外を使う場合は注意が必要です。 (ドキュメントに書いてある時間は Linux での動作の場合で、Windows の場合は2倍の消費、macOS の場合は 10 倍の時間消費になる。なので、無料ユーザの場合、Linux なら 2000 分、Windows なら 1000 分、macOS なら 200 分が毎月使える時間になるのか。)

どれくらい GitHub Actions を使用したかを知りたい時は、ユーザーの Settings > Billing から確認する事ができます。

f:id:ro9rito:20200608080146p:plain

ワークフローを作成する

とりあえず始めて GitHub Actions を動かしてみます。まずはワークフローを定義するためにファイルを作成します。 リポジトリに「Action」タブがあるのでこれを押下します。

f:id:ro9rito:20200608080327p:plain

ページ遷移すると、既に公開されているありがたい Workflows が沢山表示されますが、今回は使わないので set up a workflow yourself のリンクを押下します。

f:id:ro9rito:20200608080425p:plain

すると、ワークフローエディターが開くのでここから アクションを定義していく事ができます。

f:id:ro9rito:20200608080457p:plain

これをそのまま保存します。

f:id:ro9rito:20200608080526p:plain

再度 Actions タブに遷移すると、ワークフローの一覧が表示されていて、詳細を確認する事ができます。

f:id:ro9rito:20200608080600p:plain

無事に GitHub Actions を動作させる事ができました。

f:id:ro9rito:20200608080631p:plain

ちなみに今回は手っ取り早く Github 上からファイルを作成しましたが、以下のパスで yml ファイルを作成してリモートリポジトリに push するでも OK です。
.github/workflows/FILE_NAME.yml

Github Actions でブランチの操作を行う

ワークフローを定義できるようになったので、色々操作してみたいと思います。

Github Actions は公式ドキュメントが充実しているので、基本操作はそちらを読むと良いと思います。

Github Actions リファレンス
https://help.github.com/ja/actions/reference

ブランチを作成する

自動でブランチを切りたい時とかってありますよね。

dev ブランチに push したら release ブランチを作成するようにしてみます。

name: create new branch

on:
push:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- name: checkout dev
uses: actions/checkout@v2
with:
ref: dev

- name: Create new Branch
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git checkout -b release
git push origin release

step 配下に 2つのアクションを定義しました。

  • dev ブランチをチェックアウトする
  • 新しいブランチを作成し、リモートリポジトリへプッシュする

これだけで勝手にブランチを切ってくれるので簡単です。 ポイントは Github の token を env に登録しておく事くらいです。

今回は run を定義しましたが、Marketplace にもブランチを切るアクションが公開されているので、やりたい事がそれで実現できるならそちらを使っても良いと思います。

タイムゾーンを設定する

なんだかこのままでは味気ないので、自動で作成されるブランチに日付をつけるようにしてみます。

- name: Create new Branch
env:
TZ: 'Asia/Tokyo'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=`date +"%Y%m%d"`
BRANCH_NAME="release_$DATE"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"

タイムゾーンUTC なので、日本時間に合わせるために環境変数タイムゾーンを設定しています。

プルリクを作成する

ブランチ切ったらプルリク出したい欲が湧いてきたので、やってみます。

name: create branch & pull request

on:
push:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
# hub コマンドをインストールする
- name: install hub
uses: geertvdc/setup-hub@v1.0.0

# dev ブランチをチェックアウトする
- name: checkout dev
uses: actions/checkout@v2
with:
ref: dev

# ブランチを作成する
- name: Create Branch
id: create_branch
env:
TZ: 'Asia/Tokyo'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=`date +"%Y%m%d"`
BRANCH_NAME="release_$DATE"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"
echo ::set-output name=branch_name::$BRANCH_NAME

# プルリクを作成する
- name: Create Pull Request
env:
REVIEWERS: "rito328"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
run: |
hub pull-request -m "Github Actions からプルリクを出す" -b master -h $BRANCH_NAME -l "PR:draft" -r "$REVIEWERS"

プルリクをコマンドで投げるために hub コマンドをインストールしてそれでやっています。

ブランチ名をプルリクの step へ渡すために以下の事を行っています。

  • ブランチ作成 step に id を設定
  • アクションの出力パラメータを設定

出力パラメータの設定は、以下の部分です。

echo ::set-output name=branch_name::$BRANCH_NAME

これで、別の step の出力を別の step から参照する事ができるので、プルリクSTEP の env 設定時にこの出力を取り出して設定しています。

この辺りは以下のリファレンスが参考になります。

GitHub Actionsのワークフローコマンド

これを実行すると、プルリクが作成されます。

f:id:ro9rito:20200608081805p:plain

ちなみに pull request の作成も、Marketplace にアクションが多数公開されているので、気に入るものを探してみると良いと思います。

Slack 通知を行う

プルリクを出したら、それを伝えたい欲が湧いてきました。slack でこの事を通知しようと思います。

name: create branch & pull request

on:
push:
branches: [ dev ]

jobs:
build:
runs-on: ubuntu-latest

steps:
# hub コマンドをインストールする
- name: install hub
uses: geertvdc/setup-hub@v1.0.0

# dev ブランチをチェックアウトする
- name: checkout dev
uses: actions/checkout@v2
with:
ref: dev

# ブランチを作成する
- name: Create Branch
id: create_branch
env:
TZ: 'Asia/Tokyo'
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
DATE=`date +"%Y%m%d"`
BRANCH_NAME="release_$DATE"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"
echo ::set-output name=branch_name::$BRANCH_NAME

# プルリクを作成する
- name: Create Pull Request
id: create_pull_request
env:
REVIEWERS: "rito328"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
run: |
RESULT=`hub pull-request -m "Github Actions からプルリクを出す" -b master -h $BRANCH_NAME -l "PR:draft" -r "$REVIEWERS"`
echo ::set-output name=url::$RESULT

# メッセージを作成する
- name: create message
id: create_message
env:
BRANCH_NAME: ${{ steps.create_branch.outputs.branch_name }}
URL: ${{ steps.create_pull_request.outputs.url }}
run: |
MESSAGE="$BRANCH_NAME ブランチを作成してプルリクを出しました $URL"
echo ::set-output name=message::$MESSAGE

# slack で通知する
- name: Post to slack
uses: novoda/github-slack-action@master
with:
color: good
text: ${{ steps.create_message.outputs.message }}
webhook: ${{ secrets.SLACK_WEBHOOK }}

slack 通知に関しては、Marketplace にアクションが沢山公開されているので、その中から気に入ったやつを選んで使うと手っ取り早いですね。

(ただ、どのアクションもあれこれ詳細な通知を出したがるので、なかなかシンプルなやつがなかった)

GitHub Marketplaceからのアクションの利用

これもさっきのプルリクの時とさほどかわりません。 hub コマンドでプルリクを出すコマンドを実行すると、プルリクの URLが返ってくるので、それを出力してメッセージに含ませます。

slack 通知は Marketplace で公開されているアクションを使ったので、一度メッセージ作成の STEP を設けてメッセージを組み立てて、slack 通知 STEP で引数としてそれを渡しています。

これで slack 通知が行われるようになりました。

f:id:ro9rito:20200608082019p:plain

スケジュールで動作させる

これまでは、dev ブランチへの push をトリガーとしてワークフローが動作するようにしていましたが、決まった日時や曜日でこれを動作するようにしてみます。

on:
schedule:
- cron: '*/15 * * * *'

push:
branches: [ dev ]

これで、プッシュのトリガーとは別に、スケジュールでもこのワークフローが稼働するようになりました。cron と同じ書式っていうのが、見慣れているのでなかなか良いですね。

トリガー(ワークフローを実行するきっかけ)に関しては、以下に解説があります。

ワークフローをトリガーするイベント

まとめ

Github Actions が登場する前までは、別の CI サービスと連携しなければならなかったので、連携コストだったり、CI の方のサービス自体が調子悪いとかで開発に影響が出たりとか、なかなかツラいポイントもありましたが、Github Actions は簡単な上にコード管理と一気通貫で CI/CD を管理できるのでとても良いサービスだと思います。

テスト回したりデプロイしたりもまた次回やってみましょう。

GitHub Actionsのドキュメント
https://help.github.com/ja/actions

ルートによってグローバルスコープを適用する

(この記事は個人ブログの転載です。)

ルートによってグローバルスコープを適用する - あしたからがんばる

Backcheck事業部の前田です。

グローバルスコープまわりでハマっていて、PHPユーザーズSlackの皆さんに色々と助けていただきました。ありがとうございます。

多くの知見を得たので、ここにまとめておきます。

例題

Laravelでブログを作成します。 ここでの重要な要件は以下です。

  • 記事は「公開ステータス」を持ちます
  • 公開ステータスが非公開の記事は閲覧者に見えてはいけません
  • 公開ステータスが非公開の記事は管理画面では見えないといけません

グローバルスコープ

さて、「公開ステータスが非公開の記事は閲覧者に見えてはいけません」という要件を満たす簡単な方法は以下のような方法です。

<?php

$posts = Post::where('publish_status', 'published')->get();

簡単に思いつき、簡単に実装できます。
ただし、この方法の場合、記事を取得する箇所はすべてこのwhere句を書かないといけません。面倒ですね。
また、今後の機能改修を考えると、where句を入れ忘れる事故が発生しそうです。怖いですね。

そこで、グローバルスコープを適用させます。
グローバルスコープを適用することで、非公開の記事を存在しないものとして扱えます。

Post

<?php

class Post extends Model
{
    protected static function boot(): void
    {
        parent::boot();
        
        static::addGlobalScope('status', function (Builder $query): void {
            $query->where('publish_status', 'published');
        });
    }
}

PostController

<?php

$posts = Post::all();

都度where句を書かなくて良くなりました。
where句を書き忘れる心配もありませんね。

ルートによってグローバルスコープを適用させる

もうひとつの要件、「公開ステータスが非公開の記事は管理画面では見えないといけません」を考えると、現状のままでは難しいです。
グローバルスコープにより、管理画面でも非公開の記事が閲覧できなくなるからです。

<?php

$posts = Post::withoutGlobalScope('status')->get();

のようにすれば取れなくもないのですが、管理画面で毎回グローバルスコープを外す処理を書くのは面倒です。

よって、「管理画面以外ではグローバルスコープを適用させる」のようにできると良さそうです。

そこで、以下のようにしてみました。

Post

<?php

class Post extends Model
{
    protected static function boot(): void
    {
        parent::boot();
        
        if (app(PublishStatusScopeManager::class)->isActive()) {
            static::addGlobalScope('status', function (Builder $query): void {
                $query->where('publish_status', 'published');
            });
        }
    }
}

PublishStatusScopeManager をsingletonでサービスコンテナに登録しておき、管理画面以外の場合はグローバルスコープ適用フラグを設定しておく作戦です。

ちなみに、最初はこの設定はミドルウェアで行っていました。 以下のようなイメージです。

f:id:chiroruxx:20200604212300p:plain

モデル結合ルートに対応する

ですが、ミドルウェアではモデル結合ルートでグローバルスコープが効きませんでした。
以下のような流れになっていたようです。(用語は適当です)

f:id:chiroruxx:20200604212336p:plain

ここでは、以下のような流れになっています。

  1. モデル結合ルートの場合、ルートで指定されたモデルが存在するかを先にチェックします。(モデルが存在しない場合はミドルウェアを適用せずに404を返したいためだと思います)
  2. モデルの存在チェックをする際にモデルをbootします。
    • このタイミングではまだミドルウェアには到達していないので、 グローバルスコープの適用フラグは設定されていません。
    • なので、グローバルスコープが適用されません。(checkActiveする段階でsetActiveできていない)
  3. ミドルウェアでフラグを設定するも、既にbootされているので手遅れ・・・
  4. (ちなみに、コントローラからモデルを呼び出す際も、既にモデルはboot済みなのでboot処理は呼ばれません。)

ここで僕は「ぐぬぬぬ・・・」と悩んだのですが、最初に紹介したslackで「RouteMatchedイベントの後に設定すれば良いのではないか?」と教えてもらい、解決しました!(本当にありがとうございます!)

コード的にはこんな感じです。

EventServiceProvider

<?php

class EventServiceProvider extends ServiceProvider
{
    // ...省略

    public function boot()
    {
        parent::boot();

        Event::listen(RouteMatched::class, function (RouteMatched $event) {
            if (!Str::startsWith($event->route->uri, 'admin/')) {
                app(PublishedStatusScopeManager::class)->setActive();
            }
        });
    }
}

ちょっと「管理画面かどうか」のチェックは雑ですが、、、

ちなみに、イメージ的にはこんな感じの流れです。

f:id:chiroruxx:20200604212417p:plain

(UMLスキルが低くて下手っぴですが・・・)
ここでは、EventListenerはクロージャで表現しているので、クラスは作ってないです。

まとめ

「管理画面とその他の画面で取得条件を分ける」などは、よくあるケースだと思います。
今回は、以下の方法で対応しました。

  • グローバルスコープを使う
  • ルートをもとに、グローバルスコープを出し分ける
  • 出し分けのフラグはRouteMatchedイベントが発火されたタイミングで行う

オンライン/オフライン勉強会を運営してみて思うこと

こちらの記事は個人ブログの転記です オンライン/オフライン勉強会を運営してみて思うこと - 白メガネの日記

オンライン勉強会の運営してます

昨今の事情もあり、エンジニアの勉強会もオンラインで開催されることが増え、
自分自身もいくつかのオンラインイベントの運営に携わっています。

nyamucoro.connpass.com

study-in-virtual.connpass.com

オフラインとオンラインではやっぱり勝手が違うので、
運絵する上でのメリット、デメリットも違ってきます。
そんなことを運営の目線で語ってみようと思います。

ちなみにcluster ってVR空間で主にイベントしています。

cluster.mu

オフライン勉強会のメリット/デメリット

メリット

  • 通話アプリがあれば運営がオンラインでも普通に運営できる
    • 以外といける
    • discord 使ってます
  • 会場を確保しなくていい
    • オンラインでいいので実際の場所確保する必要がないのは運営としてはかなり楽
    • 会社等のコネがなければこの時点で積むことも
  • 懇親会用の料理や、お酒の手配をしなくていい
    • こちら手配も面倒ですが、参加費をとっている場合当日キャンセルは運営が負担する必要があったりする
  • 参加場所を選ばない
    • 会場までいかなくていい、場所を選ばない
    • 諸事情で、家ではなく出先で運営作業をする必要がありましが、PCがあれば問題なくできました
  • 当日キャンセルが少ない
    • 移動の手間がないため?か少ない
    • (わかる当日行きたくなくなることあるよね)
  • 気軽に参加してもらえる(やっぱり参加人数多い)
    • Live配信等もあるので、参加ハードルはオンラインより圧倒的に低いと思われる
    • 東京以外の方の多く参加してくれていてとても嬉しいです
  • 運営としてはやるたびに発見があっておもしろい
    • 問題も色々あるけどね

デメリット

  • 懇親会がない
    • 懇親会の準備は面倒だけど、こっちも本編と同じくらい大事だと思っています
  • YoutubeLive等の配信にはそこそこスペックのPCが必要
  • 参加や、登壇者環境はそれぞれ違う
    • クライアント起因によるマイクトラブルや、映像トラブルはそこそこある
  • 遭遇したことがないトラブルが起こる
    • ナレッジ不足はいなめない
    • 都度対応することがあるので勘弁して
    • 例:) 確認不足で用意したオフラインの会場が突如強制終了する

前提条件

後出しで前提条件を語りますが、
前提として運営の連携がオンラインでも取れるような信頼があること
ということが大事だと思っています。
現在参加している勉強会の運営メンバーは、基本的にオフラインのイベントで一緒に運営として参加したことがあるメンバーが多めです。
オンラインのコミュニケーションは基本、ある程度の信頼があるから成り立つものだと思っているので、初対面とかでいきなりオンラインで運営とかちょっとむずかしいのか?と思っています。

日々改善しています

ナレッジ不足はいかんせんありますが、運営でノウハウをためて
いろんな人に使ってもらおうと公開していますので、ぜひ見てもらえると嬉しいです github.com

まとめ

オンラインイベントは、オフラインイベント比べると運営負担は少ないし、
開催ハードルも若干低いのかなと感じます。 もっといろんなかたちでオフラインの勉強会が増えてほしいとも思います。

けど、それぞれいいとこもあるのでやっぱりオフラインのイベントもしたいなー、と思う今日このごろです。