モブプログラミングはじめました

はじめに

こんにちは。エンジニアの津崎です。 皆さんモビングしてますか? 一人でコーディング、もしくはペアプロでしょうか。

ちなみに、この記事も初の試みとしてモブプログラミングによって作成されています(笑) モブブロギングです。

共同編集者の、やも(@yamotuki)さん、はまちゃん(@hamakou108)、ありがとうございます。この場を借りてお礼申し上げます🙇‍♂️

M&Aクラウドの開発チームのうち、僕が所属する3名構成のサブチームにてモブプログラミングを試験的に導入しています。 今日は、1ヶ月ほどモブプログラミングを経験した上での学びを共有します。

モブプログラミング

モブプログラミング(モビング、以下モブプロ)とは、3人以上のエンジニアで1つのプログラムを書き、チームで成果物を完成させる、チーム作業のテクニックです。

なぜモブプログラミングか?

開発チームではコミュニケーションコストを減らすために部分的にペアプログラミング(以下ペアプロ)を導入していましたが、ペアプロを行っているのは主に、レビューのタイミングと、作業が詰まってしまったときに限られていました。 一部ペアプロを行うことでベロシティ(スプリント内の消費ストーリーポイント)が高まる傾向にあることはわかっていたので、ペアプロの機会を増やせば、もっとベロシティが上がるのではないか? と考え、3人で同時に作業を進めるモブプログラミングを試してみることにしました。

モブプログラミング ベストプラクティス

僕たちのチームではモブプログラミングの導入にあたって、「モブプログラミング ベストプラクティス」を参考にしています。

「モブプログラミング ベストプラクティス」では、同じ部屋にメンバーが集まり、1台のPC、1台のキーボードを共有するスタイルでモブプログラミングをすることが前提になっています。

モブプログラミングでは「タイピスト」と「その他のモブ」という役割に分かれて作業を行います。タイピストは、その他のモブの指示に従ってコーディングを行います。タイピストが暴走して勝手にコーディングすることは許されません。 タイピストは、ただの入力者ではなく、その他のモブの「スマートアシスト」です。 一字一句指示していると時間がかかりすぎてしまうので、齟齬が起こらない範囲でざっくりとした粒度で指示し、作業を行います。

初めてのモビングセッションは2時間程度から始めることが推奨されています。 1つのモビングセッションを10分の小さなモビングインターバルに分割します。 それぞれのインターバルでタイピストを交代しながらモブプログラミングを進めていきます。

最後に20分ほどで締めくくりでレトロスペクティブ(振り返り)を実施し、次のモビングセッションに向けて改善できることを話し合い、モビングセッションを終えます。

実際にやってみて感じたモブプログラミングのメリット

複数人で問題解決した方がより良い答えがでる

これは体感として、話し合いの中でいいアイデアが浮かぶことが多いなと感じています。 1人でタスクを終わらせることばかり考えて実装していると、つい安直で長期の保守性の悪い手法を選択しがちです。そういった思考で作ったPull Requestでも「既に書かれてしまっているから」と多少品質が低くてもマージされてしまう、そういうことはよくあるのではないでしょうか。 一方でモブプロで方針を話し合うと、「このタスクの目的ってなんだっけ?」から始まります。その目的に叶う最も良いと思われる設計方針を複数人で話し合うことになります。場当たり的な対応なんて恥ずかしいので出づらくなります。

キーパーソンに頼らない

1人でタスクを進める前提だと、どうしても得意な人に特定のタスクが偏りがちです。 その人だけが詳しくなっていき、その他の人は情報共有を受けるだけとなり、より属人化が進んでいきます。 モブプロで進めることで、チームでタスクに当たるので特定の誰かに情報が偏ることが減ります。 プラクティスとして「ちょっと付いていけなくなってもタイピストをやればOK」というものがあります。タイピストは必然的に他の人から指示を受けて書くことになるので、知見を得られやすいポジションになります。 これにより、各人のスキルアップに繋がることになります。

ペアプロより中断しにくい

1人より2人、2人より3人で話している方が外部からの妨害は入りにくいものです。 また、3人で進めている場合には、そのうち1人ミーティングで抜けたとしてもモブプロは続いていきます。 これにより成果物の完成速度が安定します。

レビュー高速化

「レビューお願いします」「リマインドですが、こちらレビューお願いします!」 「ここ修正してください」「修正しました。確認お願いします!」「ここもお願いします」「修正しました!再度確認お願いします」・・・

モブプロを初めてからのコードレビューは爆速化しました。事前にモブプログラミングをしているため、コードレビューでは「余計なものが入っていないかな?」という確認をさっとするだけでよいのですぐに終わります。 レビューしてもらうまで待ったり、リマインドしたり(されたり)する必要もありません。

技術の共有化

コーディングのテクニックや、ツールのショートカットなど、GitHub上のプルリクエストのコードには現れないスキルを間近に見ることができます。 同じような仕事をしていると思っていた同僚がすごいショートカットを使っていた時の驚きをぜひ体感してみてください。

未知領域、苦手領域のキャッチアップが楽に

メンバー内に得意な人がいれば、リードしてもらうことで一人で進めるより理解が進みます。

実際にやってみて感じたモブプログラミングのデメリット

技術を深ぼる時間が取りづらい

1人でやっている時にはタスク完了にはそこまで影響しないけど技術的に気になったことを深掘りする時間が取りやすいです。自分がきちんと全て理解していなくてもタスクをDONEにすることができるので理解が曖昧なまま終わってしまうことがあります。 この問題には強い気持ちで「ここの技術的背景をもうちょっと深掘りたい」とチームに提案することが必要です。案外他のメンバーも曖昧になっているところで、全員の技術理解を深めることになるので積極的に提案していきましょう。

傍観者モードになってしまう

これは僕だけかもしれませんが、自分以外のモブが議論して自分以外のモブがタイピングしている状況では、気を抜くと簡単に傍観者モードになってしまい、話についていけなくなってしまいます。 適度に休憩しリセットする。タイピストを適切に回し、集中を切らさないようにするなどの対策が必要です。 (原則に則ればタイピストは 10分交代なのですが、後述の理由でなかなか難しいケースがあります)

リモートでのモブプログラミング

PhpStorm Code With Me の活用

Code With Me: JetBrains が提供する共同プログラミングサービス

僕たちチームでは、リモートでの作業が主なため、PhpStormの共同プログラミング機能、「Code With Me」を使ってモブプログラミングを行なっています。

Code With Me は、誰か1人がホストとなって、その人のPhpStormに接続する形で共同編集を行います。Googleドキュメントやスプレッドシートを複数人で触ったことがある方はイメージできるかと思いますが、利用者ごとに複数のカーソルが存在し、それぞれがコーディング作業できます。

(弊害)平行作業できてしまう

コーディングをしていると、「平行作業した方が早くない?」となり、タイピスト以外がタイピングしたくなる時があります。 並行で作業をしてしまうと、何がどのように変更されたのかわからなくなるため原則禁止ですが、僕たちは以下のルールでその他のモブがタイピングするのを許可しています。

  • ちょっとしたタイプミスや誤字脱字の修正
  • 単純な作業の分業
  • タインピングのための参考コードやURLの入力
(弊害)タイピストが偏ってしまうケースがある

PhpStorm上で完結する作業であれば、タイピストがシームレスに交代できますが、フロントエンドのマークアップ作業でコーディング→ビルド→ブラウザでの動作確認をやるケースなどでは、PhpStormのホストが作業しないと効率が悪くなってしまいます。後者のケースでは、どうしても数十分間タイピストが固定になることがあります。

(弊害)ホストのスイッチングコスト問題

PhpStorm の Code with Me は、2022/05/13現時点ではホストへの接続にやや時間がかかります。 コードはホストとなる人の環境にしかありませんので、会議でホストが抜けると、その環境で動作確認ができなくなってしまう問題があります。 その緩和策としては、テストを潤沢に書くことで手動動作確認を減らし、コード上だけで完結させることがある程度ワークしました。

また、誰かの環境である程度書いたコードを、なんらかの事情で他の人の環境で続きを書きたいケースでは、新しいホストの環境にコードをフェッチする必要があります。これが地味にめんどくさく、ホスト交代をする頻度が下がりがちです。

Slack Huddleとの併用

動作確認などのコーディング以外の作業を行う際は画面共有が必要です。 僕たちはSlackのHuddleにて通話や画面共有をおこなっています。Code With Meにも通話、画面共有機能があるのですが、僕たちは普段Slackを利用することが多いためSlackの方を利用しています。

休憩に対する考え方

複数人で作業を進めていると、どうしても話しながら休憩するタイミングを逸してしまいます。 自分が疲れていても、他の人はまだいけそうかな?など忖度してしまうとなかなか休憩しようよ、とは言いづらいものです。 これはチームが成熟していくとお互いの状態を見て休憩をうまく取っていけるようなのですが、現時点ではルールを作って対応しています。

  • 10分でタイピスト交代(ベストプラクティス準拠)
    • アレクサやスマホのタイマーをセットしてアラームをみんなに聞こえるようにします
  • 30分に一回 10分ほど休憩 (これは最近厳密ではなく、区切りのいいところで休憩しがち)
  • 2時間ぐらいごとに20分ほどの長い休憩
    • (休憩中はチャットの確認なども含む)

タスクの進捗に関して

個々人でタスクをやっている時には、3人チームでベロシティ(1週間に消化できるストーリーポイント)は10pt程度だったのですが、モブプロを始めてからは概ね8pt程度になりました。1/3まで低下することもあり得ましたが、そこまでは低下しませんでした。 それは1人で進めているときには以下のような時間があったのが減ったためだと考えてます。

  • 設計方針を決めるのに時間がかかる
  • 細かい実装でいちいち詰まる
  • プルリクエストのタイミングで前提共有をして全部ひっくり返る
  • 実装詳細の共有の時間

一方で、1人で考えている時にはなかなか取られなかったであろうシンプルな設計方針をチームで考え出せたりします。シンプルな設計に加えて、その設計を確実にそのチームメンバー全員が理解しているのも長期でのリターンとなると考えています。

おわりに

モブプログラミングを実施してみて、当初期待していたチームのベロシティ向上という期待に反してベロシティが下がってしまいましたが、 知識共有や大きなバグの防止、慎重な設計による長期的なリターンなどを考えると、ベロシティ以上の便益があると感じています。 今後、モブプロのやり方を改善していく上で、ベロシティの回復、そして向上を目指しています。

個人的には1人で黙々作業するよりも、チームで話し合いながら作業するほうが楽しいので、チームで工夫しながらより良い開発をおこなっていけるよう今後も試行錯誤を続けていきます。

採用中です

組織拡大!急成長中スタートアップでエンジニアリングマネージャーを募集!! - 株式会社M&AクラウドのWebエンジニアの採用 - Wantedly

Amazon CognitoとCloudFrontで特定のユーザのみが閲覧できる仕組みを作る

こんにちは、久保田(@kubotak_public)です

今回の記事はAmazon CognitoとCloudFrontを利用して特定のユーザのみが閲覧できる仕組みを作る(表題どおり)となります。
弊社での利用シーンとしてSchemaSpyで生成したER図(というよりドキュメント)を特定のユーザ、つまり弊社の人間のみが閲覧できる仕組みを作りたいなという動機で作成しました。
例えばフロントエンドのStorybookなども社内展開する際にはS3に置いたものをどうにかアクセス制限して提供したい・・・みたいなニーズってあると思うのですが、まさにそういう場合にうってつけではないかと思います。

Amazon Cognito

そもそもCognitoとは?という方向けに説明しますと、Auth0Firebase Authenticationに代表されるIDaaSと呼ばれるたぐいのサービスです。
認証の仕組みやユーザーの管理などを内包するサービスです。

全体像

まずは全体像を共有します。
CloudFrontを経由してS3の静的データを配信する素朴な構成の中に、CloudFrontにLambda@Edgeを紐付けて前段に認証の仕組みを挟んでおります。
また、CognitoにはGoogle認証を紐付けてGoogleアカウントによるログインができるようにしています。

① CloudFront

もう少し詳細に説明していきたいと思います。
まずはCloudFrontがS3を参照して静的データを返すという素朴な構成です。
しかし、CloudFrontのビヘイビアで関数の関連付けを行い、ビューワーリクエスでLambda関数が起動するように設定します。
このようにCloudFrontでLambda関数を紐付けるものがLambda@Edgedです。

② Lambda@Edge

CloudFrontで紐付けているLambda関数はLambda@Edgeとして利用するので以下の成約があることに注意が必要です。

  • Lambda@EdgeはバージニアリージョンのLambda関数しか設定できない
  • Lambda@Edgeは環境変数が使えない
  • Lambda@Edgeは5s以内にレスポンスを返さなければならない

さて、このLambda@EdgeではCognitoに連携して以下の処理を行います。

  1. 認証済みかどうかチェック
  2. 未認証の場合はGoogleログインを促す
  3. 認証済みの場合はリクエストを許可する

これを実装しましょう・・・といっても全然お手軽感ないですよね。安心してください。便利なライブラリがあります。

github.com

awslabsが提供してるLambda@Edge用のCognito認証ライブラリを使うと簡単に上記仕組みが実装できます。

※ライブラリのREADMEの通り

const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
  // Replace these parameter values with those of your own environment
  region: 'us-east-1', // user pool region
  userPoolId: 'us-east-1_tyo1a1FHH', // user pool ID
  userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
  userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', // user pool domain
});

exports.handler = async (request) => authenticator.handle(request);

先述の通り、Lambda@Edgeでは環境変数が使えないのでここではベタ書きする必要があります。

③ Cognito

続いてはCognitoでGoogle認証を追加します。 また、Google認証時に特定のドメインのみをサインアップ対象とするために一工夫します。

CognitoでGoogle認証を追加する方法はクラスメソッドさんの記事を参照ください。

dev.classmethod.jp

サインアップ時に特定の処理を挟む場合はLambda関数を指定することができます。
後述でLambda関数については紹介するのでここでは登録の方法のみ紹介です。 CognitoではこのようにイベントにフックするようにLambda関数を登録することができます。

GCP

Google認証を行うのでGCPの設定も必要ですが、これに関しては先述のクラスメソッドさんの記事に紹介がありますので割愛します。

⑤ Lambda

Cognitoのサインアップ時に登録するLambda関数を作成します。
こちらはLambda@Edgeではないので東京リージョンで作成しても問題ありません。

この関数はPythonで作成しています。

def lambda_handler(event, context):
    print(event)
    email = event["request"]["userAttributes"]["email"]
    print(email)
    domain = email.split('@')[1]
    if "example.com" == domain:
        print("OK!")
        return event
    raise Exception("bye-bye!")

とてもシンプルな実装です。
Google認証後に受け取ったメールアドレスのドメイン部分を抜き出して特定のドメインであるかどうかをチェックします。
一致した場合はそのまま受け取ったイベントを返し、そうでない場合は例外となります。
この例ではexamile.comドメインのメールユーザーのみサインアップが許可されることになります。

さいごに

いかがでしょうか?
実装自体はほぼやってないと言っても過言ではないレベルでAWSのサービスを組み合わせただけで認証機能を実現できました。
最初はER図用に作った仕組みではありましたが、開発環境のアクセス制御に関しても現在ではこの仕組で運用しています。
BASIC認証等ではなく、Cognitoによるユーザー認証になるので退職者が出た場合にも安全に運用を続けることができますね。

今回の記事はここまで!

PHPerKaigi2022イベントレポート(M&Aクラウドから6人登壇しました!)

こんにちは。エンジニアの塚原(@AkitoTsukahara)です。 先日、開催されましたPHPerKaigi2022(4/9 ~ 11)のイベントレポートになります。 弊社メンバーの発表スライド紹介に加えて、個人的に印象的だった発表をまとめさせていただきました。

PHPerKaigi2022は3日間の開催でオフラインとオンラインのハイブリット開催となっておりました。私はオンラインで参加させていただいていただきましたが、オンライン上でもオフラインに負けないぐらい盛り上がっており、カンファレンスの熱量を久しぶりに感じることができる素敵なイベントでした。

また今回のPHPerKaigiには弊社から6人のエンジニアが登壇させていただきました🎉 弊社メンバーは「全員インフルエンサー」のバリューを胸に、エンジニアであるなら発表で参加したい! そんな気持ちでプロポーザルを提出しております。 イベントが近づくにつれてSlack上では発表スライドをまだ用意できていないエンジニアが焦り始めたり、出来上がったスライドをメンバー同士でレビューしあったりと、社内でも盛り上がりを見せていました。メンバーが一生懸命作成したスライドを1つ1つご紹介させていただきます🙋‍♂️

弊社のメンバーが発表したスライド

(レギュラートークPHPコードを消すライブラリを作った

speakerdeck.com

「全員インフルエンサー」のバリューの旗振り役、久保田さんによるスライドです。 弊社のプロダクトではfeature toggleを活用していて、使われなくなった分岐をphp-delでサクッと削除できるようになりました。チームの生産性がかなりカイゼンされたおすすめのライブラリです。

github.com

登壇者からのコメント

私だけ事前録画だったんで当日涼しい顔してみんなの発表みてました。でも事前録画は結構寂しいので来年はリアル登壇したい!

(LT)Predefined Interfacesを使って便利な独自クラスを作りましょう!

speakerdeck.com

みなさんはPHPerKaigi冊子は見ましたか?あの1面いっぱいに写し出されていた我らがCTO荒井さんによるスライドです。 Predefined Interfacesについて皆さんはご存じですか?よく知らない、利用したことがないとという方はスライドをチェックしてみましょう

登壇者からのコメント

Predefined Interfacesを調べていたら、UnitEnumとかBackedEnumとかあって、早くEnum使って開発できるようにPHPのバージョンをあげようと思いましたね。

(LT)PHPスカラー型をクラスでラップして便利に使えるようにするライブラリ「Stannum」を作った話

speakerdeck.com

まだ入社して3ヶ月という事実を感じさせないくらい大活躍中の大石さんによるスライドです。 「こんなコードを書きたいんだ!」というモチベーションから自作のライブラリを作り上げた内容になっています。 個人的にはライブラリの名前からも大石さんのアイドル愛を感じました

github.com

登壇者からのコメント

初めての登壇でめちゃくちゃ緊張しました。ちなみにライブラリの名前の由来は金属のスズですが、実は私が好きなアイドル(Appare!の藍井すず)から取ったものでもあります!是非皆さんライブラリ使っていただけるとうれしいです!!

(LT)PHPの緩やかな比較の実態

speakerdeck.com

最年少でなんと20歳の國村さんによるPHPの緩やかな比較に関するスライドです。

言語仕様に関する説明はドキュメントを読めば良いのですが、最も正確なドキュメントは何でしょう。 それはソースコードです。

いやー、かっこいいですね。私はPHP自体を形成するのソースコードは読んだことなかったですし、読み解き方もスライドで紹介してくださっているので勉強になりました。気になる方はぜひスライドをチェックしてみてください。

登壇者からのコメント

初めての登壇超緊張しました。反応が見れなくて寂しかったので来年はぜひリアル登壇したい!

(LT)【Laravel】サクッとN + 1問題を見つけて倒しチャオ!

speakerdeck.com

チームのムードメーカー津崎さんによるN +1 問題に関する発表です。私が見ていた中ではLTの中で一番盛り上がっていのではないか?という位に会場が湧いた発表になっていました。チケットをお持ちの方はぜひニコニコで実際の発表内容を見ていただきたいです

登壇者からのコメント

みんなもタイムオーバーになって会場を沸かせよう!

(LT)気づいた時にリファクタしよう!Laravelのデータベースクエリを最適化するTips

speakerdeck.com

登壇者からのコメント

こちらは私のLaravelのデータベースクエリを最適化Tipsを紹介するスライドになります。ふりかえると折角のLTだったのでもう少しエンタメに振った内容にしてもよかったかな?と思っています。Laravel開発をしている時にコードレビューのヒントにもなると思うので、一読いただけると嬉しいです。

個人的に印象的だった発表

予防に勝る防御なし - 堅牢なコードを導く様々な設計のヒント

speakerdeck.com

テスト駆動開発で有名な@t_wadaさんのセッションです。 リアルタイムではt_wadaさんのセッションを始めてで、サンプルコードがPHPというなんてお得体験なんだろうと思いながら視聴していました。個人的にはPHPが型厳格な方向に進んでだ恩恵でt_wadaさんセッションが生まれたのでは?と思いましたw

堅牢な設計について分かりやすく、具体的な説明はその日からコードを書く時には意識したいものばかりでしたね。 まだご覧になっていない方にはおすすめです

チームの仕事はまわっていたけど、メンバーはそれぞれモヤモヤを抱えていた話──40名の大規模開発チームで1on1ログを公開してみた

speakerdeck.com

@vacchoさんによるチームビルディングの発表です。 私は先月から専任スクラムマスターをやらせていただいているので、テック寄りの発表以外にもチームで取り組むプロダクト開発に関する発表を視聴していました。チームの課題をメンバーとの議論から少しづつ見える化していくアプローチは、そうですよねーと相槌を打ちながら聞いていましたw また1o1ログを公開・非公開のバランスを取りながら、残していく気遣いは私も意識していきたいなと感じました。

最後に

いかがだったでしょうか? 今回は予定が合わなくて参加できなかったり、終わってからイベントに気づいた方でもスライド資料を読むだけでも新しい学びや気づきがあると思います。気になったセッションがあれば、どんどんスライドを読んで、そして次回はPHPerKaigiに参加しましょう! 来年もPHPerKaigiが開催予定があると実行委員長の長谷川さんがおっしゃていましたので、次回も弊社メンバーは全員プロポーザルを出して、「全員インフルエンサー」を発揮していきたいと思います!(個人的にはLTではなく通常のセッション枠の登壇を狙っていきます!💪)

また、弊社ではエンジニアの採用を行なっています。 スタートアップ企業でのWeb開発に興味のある方は、ぜひカジュアルにご応募ください🤝

www.wantedly.com

www.wantedly.com

www.wantedly.com

20%税金ルールとインフラタスクの優先度定量化の試み

f:id:yamotuki:20220415125932p:plain こんにちは。エンジニアの鈴木(@yamotuki)です。
本日はインフラタスクの優先度の定量化の試みについて書いていきたいと思います。
ここでいうインフラタスクとは以下のようなタスクが含まれます。

  • 可用性と信頼性に関わる障害対応, バグ対応
  • ベロシティとストレスに関わる業務効率化(DX: Developer eXperience)
  • セキュリティやライブラリバージョンアップなど

これらのタスクについて「何を一番優先して取り組んでいくべきか」という優先度について長く頭を悩ませていましたが、私の中で一定の答えが出たので共有いたします。

20%税金ルールについて

私はインフラタスクは概ね20%は必ず時間を割かないといけない税金のようなものだと考えています。 税金を払わなければ将来にツケを回すことになり、利息が付いて後で降ってきます。 20%のアイディアはThe DevOps ハンドブックで紹介されているものでした。 書籍では以下のように書かれています。

組織が”20%税”を支払わなければ、全てのサイクルを技術的負債の返済に充てなければならなくなるようなところまで、技術的な負債が膨らむ。 サービスがどんどん不安定になっていくため、ある時点で突然機能を追加できなくなる。全てのエンジニアがシステムの信頼性の改善や問題点の回避のために追われるようになってしまう。

今の状況が特に悪い場合には、20%を30%以上にしなければならないかもしれません。 しかし、20%よりはるかに小さい割合でなんとかなると思っているチームを見ると心配になってきますよ。

この話を根拠に、jira の「インフラ」エピック(ラベルようなもの)が付いているタスクが、全てのタスクの消化量の何%程度になっているかざっくり計算し、低くなりすぎる場合にはPdMとエンジニアが交渉するようになっています。

ただ、やりたいことというのは無限に増えていくものです。20%を闇雲にやっていけばいいわけではなく、重要なものから優先順位をつけてやっていく必要があると考えています。

優先度定量化について

定量化の導入検討

以下に定量化の試みの初期検討資料の冒頭を引用します。

これまで、インフラタスクの優先度つけは、特定の人が属人的に優先度を決めていた。
その優先度付けのプロセスはブラックボックスであり、非効率であった。

特定の指標に基づいて優先度を定量化することでバックログの並び順を変えることができれば、属人化を排除して、開発チームの誰もが納得できる優先度になるのではないか。

この記事の最初に書いたように、「インフラ」に含まれるタスクは広範囲に渡ります。 種々のタスクを並列に優先度比較できることを目指しました。

タスクの分類とスコア

インフラタスクを発生している現象ベースでまず以下のように分類してみました。

  • 障害が発生しているか
  • 業務に支障が発生しているか
  • 攻撃が容易な状態にあるか
  • ライブラリが古いか

その分類の中で、例えば「障害」であれば以下のように傾斜をつけることにしました。

  • (障害)障害が既に起こっている
  • (障害)障害がいつでも起こりうる
  • (障害)障害が起こりうる可能性は高くない
  • (障害)障害が起こりうる可能性は低い
  • (障害)障害が起こってから対応しても問題ない

業務効率化については以下のような形です。

  • (効率化)無いと特定のタスクを進められない
  • (効率化)著しく業務に支障がある
  • (効率化)蓄積すると業務に支障がある

タスクのインパク

発生している現象に対して、その現象がどの程度インパクトがあるかを考えれば優先度を決定できるのではないか、と考えました。 インパクトについては、まずは”影響範囲が広いか”が重要だと思います。しかし、業務改善に関わるタスクについては開発者のみに関わるためユーザに関わるものと同列に比較することは難しいです。そこで、「開発者への心理的影響が大きいか」という視点も入れることにしました。ストレスが下がるような改善をすれば業務効率が上がり、間接的に良い影響が広範囲に出るであろうという仮説です。

例えば「障害」であれば以下のようにインパクトの傾斜を設定しました。

  • (障害)ユーザ行動を大きく阻害する
  • (障害)ユーザ行動を稀に阻害する
  • (障害)ユーザ行動を阻害しない

業務効率化については以下のような形です。

  • (効率化)解決されると高ストレスが解消される
  • (効率化)ややストレスが解消される
  • (効率化)そこまでストレスはかかっていない

スコアリング方針

上記の”発生している現象”と”インパクト”をそれぞれスコアをつけて、掛け算したら最終スコアが出るのでは、とシンプルに考えました。初期アイディアとしてはセキュリティ領域でよく使われているCVSSスコアを参考にしましたが、正直なところ跡形もありません。

具体的にはスコアリングは以下の掛け算で行われます。

  • 障害対応、バグ対応(可用性、信頼性)
    • 障害発生状況 * 影響度
    • 例えば、メール障害であれば(障害)障害が既に起こっている * (障害)ユーザ行動を稀に阻害する
  • 業務効率化(業務ベロシティ、ストレス)
    • 業務支障度 * ストレス
    • 例えば、AIテストツールの導入に関しては (効率化)著しく業務に支障がある * (効率化)解決されると高ストレスが解消される
      • ※最終的には現時点ではコストに合わないとなり、導入は見送りました
  • セキュリティ
    • 攻撃容易性 * 影響度
  • 保守(バージョンアップ系)
    • ライブラリの古さ * 影響度

スコアリング計算実務

計算は単純な掛け算なのでスプレッドシートで行うこととしました。変更も容易です。 実際に使用しているものから公開できる部分だけを切り出し、公開用スプレッドシートを作成しました。ご興味がある方はご自身の環境にコピーし、実際にタスクについてスコアリングしてみてください。

docs.google.com

スプレットシートの内部の構造について軽く説明します。

  • 青い背景領域: 選択肢の定義とそのスコア定義
  • 黄い背景領域: 対象となるタスクリストと、選択肢を二つ選択してスコア計算する領域
    • 順番のイメージを持ってもらうために、外部に公開しても差し支えないだろうと思われるタスクのみ残してあります(出してヤバそうなのがあったらこっそり教えてね!)。
    • 選択肢は、同じタスク分類二つの軸を選択する。発生している現象軸が (障害)障害が既に起こっているなら、インパクト軸にも同じ接頭の(障害)を持つ(障害)ユーザ行動を稀に阻害するを選択する
  • 緑の背景領域: スコアリングの結果、タスクをソートした領域

※各タスクのスコアは良い間隔で分布しているわけではなく、素点数値の微調整によりほんの 0.01 点だけ差があるということも多く発生しています。この数値の絶対値に大きな意味はなく、並び順を決めるためだけに使用されています。「人間が都度判断するとしたらこういう並び順になるだろう」という並び順になるために、素点は微調整をしています。会社によって素点はかなり変わってくると思います。

運用について

以下のようなSlackワークフローを用います。エンジニアは選択肢を選んで理由を書いて投稿します。そうするとその内容がチャンネルに自動投稿され、NoCodeツールであるZapierがキャッチしてJira issueを自動生成します。

f:id:yamotuki:20220415125312p:plain
Slackワークフロー

作られた Jira Issue を、作成したエンジニアが上記スプレッドシートを使用してスコアリングします。最後に(残念ながら)手動でJiraのバックログを並び替えてもらいます。

終わりに

元々は特定のエンジニアが頭を悩ませて優先度を決めていたインフラタスクでしたが、同種のタスクは毎回同程度の優先度になることにより判断コストが劇的に下がりました。 また、各エンジニアは優先度を最初から考えるわけではなく、分類だけ考えればいいので判断は難しくありません。
バックログの並び替えまで各エンジニアにやってもらうことができるようになったので、エンジニア人数が増えてインフラタスクが増えても運用を続けることができてスケーラブルな仕組みとなりました。

採用してます!

スケーラブルなチームの仕組みを一緒に考えてくれるエンジニアリングマネージャー募集しています!

組織拡大!急成長中スタートアップでエンジニアリングマネージャーを募集!! - 株式会社M&AクラウドのWebエンジニアの採用 - Wantedly

他にもデータエンジニアと機械学習エンジニアが積極採用中職種です。

ECS Execを使ってECS環境に入ってみる

f:id:fyui001:20220406154645p:plain

みなさんどうもこんにちは。 エンジニアのゆい(@fyui_001)です。

前回に引き続きEB(Elastic Beanstalk)からECSに移行したプロジェクトでの取り組みを紹介します。

今回はECSのコンテナにアクセスするためのECS ExecというAWSのサービスを使ってECS上で動いているコンテナに入る方法についてお話しようと思います。

前回の記事はこちら!

Laravel on ECSで動かすQueueとScheduleワーカー

背景

弊社ではデプロイ時にデータ投入のための一度限りのバッチ実行をEBにSSHで入り行っていたのですが、ECSに移行し、どうやってサービスの実行環境に入りリリース作業を行うかと言う課題がありました。

Dockerコンテナにsshエージェントを入れるのはイケてないなと言う判断があり、なにか手段がないか調べいていたところECS Execにたどり着きました。

AWSにはデバッグ用にECS Execと言うサービスが用意されており、ECS Execを使うことでコンテナのポートを開けてsshしたりsshキーの管理をすることなく直接ECS環境のコンテナで作業することができます。

公式ドキュメントはこちら

ECS ExecでECS環境に入る

前提条件

以下がマシンにインストールされている必要があります。

※session managerが入ってないと使えないのでちゃんとインストールして設定が必要です。

またSSMサービスに必要なアクセス許可をコンテナに付与するために、タスクIAMロールを設定する必要があります。

Fargateのコンテナにアクセス

入りたいコンテナのタスクIDを調べる

ecs execにはtask-idを指定しないと行けないですが、コンソールで見つけられなかったので以下がCLIで覗く方法です。

下記コマンドでクラスター名とサービス名を指定するとタスクのリストを出してくれるので見てます。

# 環境毎にクラスタ名とサービス名は違うので適宜いい感じに書き換えてください。
aws ecs list-tasks --cluster {cluster-name} --service-name {service-name}

こんな感じに出てきます。 クラスター名から先の文字列がタスクIDなのでちょっとメモっておく。

f:id:fyui001:20220406182521p:plain

コンテナの中に飛び込む

次のコマンドを実行してコンテナに入ります。 taskオプションにさっき調べたタスクIDを渡して、containerオプションでコンテナ名を指定します。 詳しくは公式ドキュメントを参照。

aws ecs execute-command --cluster {cluster-name} \
    --task {task-id} \
    --container {container-name} \
    --interactive \
    --command "/bin/sh"

実際に入ってみる

※ タスクIDはちゃんとAWS CLIで確認して実行しましょう。(下記コマンドをコピペするべからず

aws ecs execute-command --cluster ecs-cluster \
    --task ************************* \
    --container app \
    --interactive \
    --command "/bin/sh"

こんな感じになっていれば成功!

f:id:fyui001:20220406182606p:plain

コンテナに飛び込んだときのお作法

ECS Execで飛び込むとルートでログインさせられるので下記コマンドで適宜ユーザーを切り替えるべし。

実際に動いてるコンテナで実行ユーザーを変えている場合、ルートユーザのまま実行すると内部のパーミッションと所有権が狂ってしまうことがあるので、適宜ユーザに切り替えて実行するしたほうが安全です。(時と場合によりけり)。

su app

ちゃんと動くか

ちゃんとtinkerも動くので問題なさそうです。

image.png (11.1 kB)

最後に

今回はECS環境のコンテナに入れるECS Execについて紹介させていただきました。 ECSを使っている場合で直接環境に入るいい感じの方法を探してる方の参考になれば幸いです。

M&Aクラウドでは現在エンジニアやPdMなど幅広く募集中です!!もし興味を持ってくださった方はお気軽にお声がけください!

www.wantedly.com

www.wantedly.com

www.wantedly.com

Laravel on ECSで動かすQueueとScheduleワーカー

f:id:kubotak:20220323214652p:plain

皆さんこんにちは。kubotak(@kubotak_public)です。

この記事ではLaravelをECS Fargateで動かす際のQueueとScheduleに関して、弊社で行った知見を紹介したいと思います。

Laravel on ECSに関しては以下の記事も是非どうぞ

※なお、本稿においてはLaravel8系を利用しています。(おそらくLaravel9系でも問題ありません)

ECS FargateでQueueを動かす

弊社ではもともとAWS ElasticBeanstalkのWorker環境(以下EB Worker)でQueueおよびScheduleを実行していました。
この環境を簡単に説明すると、EB WorkerはSQSと接続されていて、SQSにメッセージが送られるとEB Worker環境のlocalhostの指定したエンドポイントにPOSTでリクエストしてくれる仕組みを持っています。
この仕組を利用して、HTTPリクエストとしてQueueを処理できるライブラリによって実行していました。

詳しくは導入当初の記事を参照ください。

tech.macloud.jp

ECS Fargateでは類似の環境を用意することができないのでLaravelのArtisanによるQueueのデーモンコマンドを利用することにしました。

Queues - Laravel - The PHP Framework For Web Artisans

DockerコンテナのエントリーポイントでこのArtisanコマンドを実行するだけで良さそうですね。
しかし、意外な落とし穴がありました。

EB Workerで動かしていた際は、HTTPリクエストによる処理のためQueueが失敗した場合はステータスコード500のエラーとしてレスポンスを返し、それを受け取ったEB WorkerはQueueメッセージを消費せず可視性タイムアウトになり再度Queueに詰み直されるという挙動で動いています。
そしてSQS側のメッセージの保持数の上限、つまりリトライ回数を超えるとDead Letter Queue(以下DLQ)に送られる仕組みになっています。

f:id:kubotak:20220323213812j:plain

しかし、ArtisanコマンドではQueueが失敗した場合、Laravel独自のリトライ処理を経て、それでも実行できない場合はfailed_jobとして扱われるような仕組みになっています。
ここで重要なのは、Queueが失敗してfailed扱いになった場合に、SQSに対して該当のメッセージを削除するロジックが入っていることです。
つまりSQSから見た場合、メッセージは正常に処理されているものとして扱われます。

今までの運用方法を変えたくなかったため、引き続きDLQを利用した仕組みに乗せたいと思いました。
DLQにも再送の仕組みがあるので失敗したJobの再実行は可能ですし、死活監視などもDLQを対象にCloudWatch Alarmを仕込んでいるので引き続き同じ仕組みにしたい意図がありました。

そこで今回はQueueServiceProviderを独自に上書きして登録するように変更しました。

app/Providers/Sqs/QueueServiceProvider.php

<?php
declare(strict_types=1);

namespace App\Providers\Sqs;

use Illuminate\Queue\Failed\NullFailedJobProvider;
use Illuminate\Queue\QueueServiceProvider as PackageQueueServiceProvider;

class QueueServiceProvider extends PackageQueueServiceProvider
{
    protected function registerSqsConnector($manager): void
    {
        $manager->addConnector('sqs', function () {
            return new SqsConnector;
        });
    }


    protected function registerFailedJobServices(): void
    {
        // FYI SQSを利用する際はfailed_jobを利用しないでDead Letter Queueを使う
        $this->app->singleton('queue.failer', function () {
            return new NullFailedJobProvider;
        });
    }
}

IlluminateのQueueServiceProviderクラスを継承して上書きしたいメソッドのみ変更しています。
まず、SQSコネクタを独自のクラスに変更しています。
そしてfailed_jobが不要になるのでNullFailedJobProviderを利用すようにしています。

app/Providers/Sqs/SqsConnector.php

<?php
// 略
class SqsConnector extends PackageSqsConnector
{
    public function connect(array $config)
    {
        $config = $this->getDefaultConfiguration($config);

        if (! empty($config['key']) && ! empty($config['secret'])) {
            $config['credentials'] = Arr::only($config, ['key', 'secret', 'token']);
        }

        return new SqsQueueWithDLQ(
            new SqsClient($config),
            $config['queue'],
            $config['prefix'] ?? '',
            $config['suffix'] ?? '',
            $config['after_commit'] ?? null
        );
    }
}

SQSコネクタもProvider同様にIlluminateのライブラリを継承して必要なメソッドのみ上書きします。
ここではSqsQueueWithDLQクラスを使うように変更しています。

app/Providers/Sqs/SqsQueueWithDLQ.php

<?php
// 略
class SqsQueueWithDLQ extends SqsQueue
{
    public function pop($queue = null)
    {
        $response = $this->sqs->receiveMessage([
            'QueueUrl' => $queue = $this->getQueue($queue),
            'AttributeNames' => ['ApproximateReceiveCount'],
        ]);

        if (! is_null($response['Messages']) && count($response['Messages']) > 0) {
            return new SqsJobWithDLQ(
                $this->container, $this->sqs, $response['Messages'][0],
                $this->connectionName, $queue
            );
        }
    }
}

app/Providers/Sqs/SqsJobWithDLQ.php

<?php
// 略
class SqsJobWithDLQ extends SqsJob
{
    public function fail($e = null): void
    {
        $this->markAsFailed();

        if ($this->isDeleted()) {
            return;
        }

        try {
            // FYI 失敗した場合SQSからメッセージを削除しないで可視性タイムアウトを待つ挙動にする
            // $this->delete();

            $this->failed($e);
        } finally {
            $this->resolve(Dispatcher::class)->dispatch(new JobFailed(
                $this->connectionName, $this, $e ?: new ManuallyFailedException
            ));
        }
    }
}

やりたかったことはSqsJobクラスのfailメソッドで$this->deleteを呼んでいる箇所を消したかっただけです。
これでQueue(Job)が失敗した場合にSQSメッセージを削除しない挙動になります。

最後にProviderの登録を独自のものに差し替えます。

config/app.php

<?php
// 略
// Illuminate\Queue\QueueServiceProvider::class, FYI 独自のProviderを使うためコメントアウト
App\Providers\Sqs\QueueServiceProvider::class,

ECS FargateでScheduleを動かす

LaravelのScheduleといえば任意の時間に特定のコマンドを実行してくれる便利な機能です。

Task Scheduling - Laravel - The PHP Framework For Web Artisans

この機能はphp artisan schedule:runコマンドをcronによって毎分実行し、登録された時間のコマンドを実行してくれるという使い方が一般的です。
しかしECS Fargateでコンテナ化する際に「cronも同梱させるのか?」「リリース時にいい感じに切り替わるのか?」とう懸念を覚えました。

実はこのScheduleの機能、php artisan schedule:workというデーモンコマンドも用意されています。
ドキュメントを見る限りでは開発時に使えるコマンドとして紹介していて、本番運用向けではなさそうです。
実際にコードを見てみるとQueueとは異なりSIGTERM(Linuxの終了シグナル)を検知して安全に終了する仕組みが入っていませんでした。
そのため、安全に切り替わるように独自のScheduleコマンドを作成しました。

<?php
declare(strict_types=1);

namespace App\Console\Commands;

use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Symfony\Component\Process\Process;

/**
 * @see https://www.egeniq.com/blog/how-gracefully-stop-laravel-cli-command
 */
class GracefulScheduleWorkCommand extends Command
{
    protected $name = 'schedule:work-graceful';

    protected static $defaultName = 'schedule:work-graceful';

    protected $description = 'Start the schedule worker for graceful';

    private bool $run = true;

    /**
     * Execute the console command.
     *
     * @see https://github.com/laravel/framework/blob/9.x/src/Illuminate/Console/Scheduling/ScheduleWorkCommand.php
     * @return void
     */
    public function handle()
    {
        $this->info('Schedule worker started successfully.');

        [$lastExecutionStartedAt, $keyOfLastExecutionWithOutput, $executions] = [null, null, []];

        if ($this->supportsAsyncSignals()) {
            $this->listenForSignals();
        }

        // NOTE PHPプロセスが実行中もしくはタスクが実行中かどうか
        while (
            $this->run ||
            ! $this->canBeStopped($executions)
        ) {
            usleep(100 * 1000);

            $now = CarbonImmutable::now();
            if (
                $this->run &&
                $now->second === 0 &&
                ! $now->startOfMinute()->equalTo($lastExecutionStartedAt)
            ) {
                $executions[] = $execution = new Process([PHP_BINARY, 'artisan', 'schedule:run']);

                $execution->start();

                $lastExecutionStartedAt = CarbonImmutable::now()->startOfMinute();
            }

           // 略
        }
    }

    private function canBeStopped(array $executions): bool
    {
        /** @var Process $execution */
        foreach ($executions as $execution) {
            // NOTE 実行中のタスクがあれば止められない
            if ($execution->isRunning()) {
                $this->info($execution->getCommandLine() . ': running!');
                return false;
            }
        }
        $this->info('Executions is nothing.');
        return true;
    }

    private function listenForSignals(): void
    {
        pcntl_async_signals(true);

        pcntl_signal(SIGINT, [$this, 'shutdown']); // Call $this->shutdown() on SIGINT
        pcntl_signal(SIGTERM, [$this, 'shutdown']); // Call $this->shutdown() on SIGTERM

        $this->info('Ready to work gracefully.');
    }

    /**
     * @see https://github.com/laravel/framework/blob/9.x/src/Illuminate/Queue/Worker.php#L696-L699
     */
    private function supportsAsyncSignals(): bool
    {
        return extension_loaded('pcntl');
    }

    public function shutdown(): void
    {
        $this->info('Gracefully stopping worker...');

        // When set to false, worker will finish current item and stop.
        $this->run = false;
    }
}

処理自体はIlluminateのScheduleWorkCommandクラスとほぼ同じ実装になり、whileの条件でSIGTERM等を受け付けた場合にfalseになり緩やかに終了するようになっています。
とはいえ、runInBackgroundなどで実行されるコマンドであれば別プロセスで動くはずなのであまり意味はないかもしれませんが、cronで動かす場合でも同じことは言えるのではないかと思います。

移行してみて

今回は初めてECSでLaravelのWorker処理を動かすにあたって色々模索してみました。
似たような事例があまり見られなかったので他の方はどうやって運用しているのか気になります!

また、Queueワーカーに関して言うとSQSのメッセージ数に応じてスケールするようにオートスケール設定を入れたのでコンテナ化の恩恵を非常に感じていますが、Scheduleワーカーは常に高いスペックのコンテナ1台で動かしているのでこれだけEC2インスタンスのほうが良さそうだな・・・と思っている次第です。
LaravelをECSで運用している方、知見交換ぜひお願いします!

では今回の記事はここまで。

AWS CDK v2 の変更点5選!

f:id:fyui001:20220401112414p:plain

みなさんどうもこんにちは。 エンジニアのゆい(@fyui_001)です。

🌊乗るしかないこのビックウェーブに🌊

皆さんはAWS CDKはご存知でしょうか? 一言で言えば使い慣れたプログラミング言語AWSリソースをプロビジョニングできるツールキットです。

前回の記事に引き続き、サービスをEBからECSに移行したプロジェクトの取り組みを紹介します。 このプロジェクトで新たに導入したAWS CDKでv2がリリースされていたので、今回はAWS CDKのv1とv2の差分について書いていこうと思います。

第一弾の記事はこちら!

背景

今までM&AクラウドではAWS CDK含めTerraformなどのIaCツールを導入しておらず、AWSリソースの操作は基本的にAWSコンソール上で行っていました。

ではなぜサービスのECS移行に伴いAWS CDKを導入したかと言うと、AWSリソース変更をする際に証跡作り・確認用のためにAWSコンソールのキャプチャを撮りslackやesaなどに添付する非常に面倒な作業があったり、手順書があちこちにあり煩雑で手間がかかるなどの課題がありました。

IaCを導入することでインフラの設定がコードベースで定義できるので、GitHubのPRがそのまま証跡になりレビュー・確認等もGitHub上で行えるため一々スクリーンショットを撮りesaやslackに貼り付ける作業を省くことができるようになりました。

今回サービスのECSでは完全移行で新規リソースを構築する必要があり、IaCの構築の自動化やバージョン管理などの恩恵が受けられるタイミングだったので、最初はECS移行分のリソースのみ部分導入に至りました。

AWSに対応してるIaCツールはTerraformやAnsibleなど複数ありますが、今回AWS CDKを選択した理由は大体以下の通りです。

  • TypeScriptやPythonJava・.NET等の言ったプログラミング言語で記述できる
  • コード記述量が少なく書ける
  • AWS公式ツールでサポートが受けられる
  • 本体の安定性はさておき終了したとしてもCloudFormation管理に戻すことができる
  • 私がAWS CDKの実務経験があった
  • 個人的に開発したECS環境を構築するAWS CDKのプログラムがあり、開発コストを抑えることが可能だった

個人的に開発していたAWS CDKの資産ですが、1年ほど前に開発したもので、その時はまだv1でしか開発しておらず、今回久々にAWS CDKの情報を調べていたら昨年12月にv2がリリースされていたので、ついでにv2のマイグレーションも行いました。 今回はv1とv2で何が変わったのかを大まかにお話しようと思います。

AWS CDKって何?

What is the AWS CDK?

従来から提供されているCloudFormationは、JSONYAMLと言った構造化ファイルでリソースを定義しますが、CDKでは前述した通りプログラミング言語で利用できるのが大きな違いと言えます。

v1とv2の差分

Release v2.0.0 · aws/aws-cdk

alpha版が出てから1年がかりでつい最近の2021年12月にv2.0.0がリリースされました。

主な差分を簡単に列挙すると以下の通りで、v2になってCDKの各リソースに破壊的な変更が入ったとかではありません。

AWS Construct Libraryが1つのパッケージになった

v1では各サービス毎にパッケージが分割されていて、使いたいサービスのパッケージを都度都度ダウンロードしないといけなくて、新しいサービスを追加するときに一々npm installとかpip installしないと行けなかったのが、aws-cdk-libと言う一つのパッケージにまとめられ、これ1つですべてのAWS Construct Libraryにアクセスできるようになったので少し扱いやすくなりました。

個人的にこれは結構うれしい変更です。 @aws-cdk/aws-ec2'@aws-cdk/core' といったパッケージを必要に応じて個別にインストール必要がなくなりaws-cdk-libを入れれば解決です。

DeprecatedなAPIが削除された

v1で非推奨になっていた多くのプロパティやメソッドが完全に削除されました。 v1で非推奨なAPIを使用している場合、v2にマイグレーションするにはアプリケーションとライブラリを更新して、代替のAPIに切り替える必要があります。

Feature flagsの新しい動作がデフォルトに

v1で導入されていたFeature flagsですが、v2ではすべてのFeature flagsが有効になりました。 AWS CDKアプリケーションをv1からv2に移行する場合は、cdk diffコマンドを実行して、アプリケーションへの影響を確認することをお勧めされてます。

ExperimentalなAPIのライフサイクルが新しくなった

v2から新しいライフサイクルを導入され、新しい実験的なConstructライブラリは、メインのaws-cdk-libライブラリから完全に独立した@aws-cdk-experiments配下に移動され、@aws-cdk-experiments/aws-xxxのように実験的ながわかる名前で配布され、0.x系のバージョン番号がつけられます。 APIがstableになった際にaws-cdk-libに移動されるようになりました。

新しいブートストラップリソースがデフォルトになった

CDKを初めて使う時はcdk bootstrap を実行してデプロイ用のS3バケットを作成する必要がありますが、cdk bootstrap で生成されるリソースが変更されました。 前述のFeature Flagsを使って手動で新しいブートストラップリソースにオプトインしてない場合、AWSアカウントとリージョン毎にAWS CDK CLIcdk bootstrapを実行し再度ブートストラップする必要があります。

さいごに

色々とCDKのことを書きたかったですが、長くなるので今回はAWS CDKのv1とv2の差分を紹介させていただきました。

最後に... M&Aクラウドでは現在エンジニアやPdMなど幅広く募集中です!!もし興味を持ってくださった方はお気軽にお声がけください!

www.wantedly.com

www.wantedly.com

www.wantedly.com

M&Aクラウドを丸ごとAmazon Elastic Container Service(ECS)に移行しました!〜コンテナイメージ作り編〜

f:id:kazuki09:20220324123449p:plain

おはようこんにちはこんばんは!エンジニアの大石です。

弊社のサービスを丸ごとAmazon Elastic Container Service(ECS)に移行したので、何回かに分けてその取り組みを紹介したいと思います!

今回は第一回、コンテナイメージ作りについてお話したいと思います。

はじめに

ECSへの移行に至ったきっかけ

弊社のサービスは元々AWS Elastic Beanstalkというサービスの上にLaravelのアプリケーションを載せて動かしていました。Elastic Beanstalkとは簡単に言うとEC2インスタンス上に一通り揃ったアプリケーションの実行環境を作れてデプロイが簡単に出来るサービスのことですね。

一見簡単に運用できるように見えるElastic Beanstalkですが、初回の導入は簡単な反面、どうしてもEC2なのであとあとPHPのバージョンアップなどのミドルウェアの更新があると環境を作り直すのが大変だったりします。

そこで、環境をコードで構築してイメージにアプリケーションと実行環境をひとくくりにしてデプロイ出来るコンテナベースの環境(ECS、Lambda、EKSなどなど)は環境の変化に柔軟に対応出来るため、レガシーからの脱却と将来的な投資を込めて今回ECSに移行するに至りました。

コンテナって?コンテナイメージって?

簡単に説明しますと、コンテナイメージはコンテナを立ち上げるためにテンプレートのようなもので、コンテナはイメージから起動した実体... といった形です。

aws.amazon.com

AWSの公式の記事にとても分かりやすい解説があるので、今回は細かい話は置いておいてイメージ作りにフォーカスしてお話したいと思います。

コンテナイメージに入れるものを整理する

Laravelを動かすぞ!!といっても大元にphp-fpmが必要だったり、アプリケーションから依存するpeclモジュールが必要だったり... などなどアプリケーションの構成によって必要なものが変わってきます。

そこで、まずアプリケーションを動かすのに何が必要かを洗い出していきましょう。

弊社のアプリケーションを例に挙げると、以下のものが必要であることが分かりました。

  • アプリケーションのLaravelのコードそのもの
  • php-fpm
  • nginx
  • composer
  • aptで導入できる各種ライブラリ(libpng, libjpegなど)
  • PHPの各種extension(pdo_mysql, opcacheなど)
  • AWS CloudWatch Agent(アプリケーションのログの転送に使用)

必要なものが分かってきたので、実際に必要なコンテナを配置していきます。

そして、弊社のアプリケーションの場合、最終的なコンテナやECSのサービスの構成としてはこのような形になりました。

f:id:kazuki09:20220324144444p:plain
構成図

コンテナイメージに関してはアプリケーションが入ったイメージとリバースプロキシ(nginx)が入ったイメージの2つが必要なことが分かってきましたね!

ちなみに、運用のコストを削減するためにLaravelのアプリケーションが入ったイメージは1つの共通のイメージを使用して、コンテナ起動時に起動コマンドを書き換える形で動かしています。

余談ですが、ECSでコンテナの実行コマンドを上書きする方法、Laravelのschedule workerやqueue workerをコンテナ上でいい感じに(しっかり安全にシャットダウンするなどなど)動かす方法については別途記事にする予定なのでお楽しみに!

コンテナイメージを作る

ECSといっても通常のDocker同様で、Dockerfileを書いてDockerイメージを組み立ててそれを何らかのコンテナレポジトリ(今回の場合はECR)にpushして使う形になります。

nginxのイメージを作る

Docker Hubを見に行くとnginxのイメージ(https://hub.docker.com/_/nginx)は既に用意されているので、それを元に必要な設定ファイルをひとまとめにしたイメージを作りました。

FROM nginx:1.21.6

COPY ./docker-prod/nginx/nginx.conf /etc/nginx/
COPY ./docker-prod/nginx/default.conf /etc/nginx/conf.d/

とてもかんたんですね!

あらかじめ用意しておいた設定ファイルをコピーして起動すればすぐにnginxが使える状態にしておきました。

アプリケーションのイメージを作る

こちらも同様にPHPの実行環境が入ったイメージ(https://hub.docker.com/_/php)が公式から提供されているので、ウェブサーバを動かす前提になっているphp-fpmのイメージを使って組み上げていきます。

といってもこちらはアプリケーションコンテナのビルドになり、ブログに載せるにはあまりにも長くなってしまうので、要所のみ記載します。

ちなみに、おおまかなイメージの作り方はこちらの記事が分かりやすくておすすめです。

www.digitalocean.com

Composerをインストールする

COPY --from=composer:2.2.7 /usr/bin/composer /usr/bin/composer

実はこれもCOPYコマンドで超簡単に1行で出来ちゃったりします。公開されているイメージから特定のファイルだけを持ってくることが出来るので、curlでダウンロードしてきて...のようなことは実は要らなかったりします。

qiita.com

AWS CloudWatch Agentをコンテナに入れる

弊社のアプリケーションではファイル上に書き出したログを収集してCloudWatchに転送するといったことをElasticBeanstalkで運用していた時代は行ってましたが、これを継続して行いたいのでAgentを入れてあげて転送できるようにしました。

(CloudWatch Logsにアプリケーションから直接AWSAPIを通してログを転送する方法もありますが、ログを書き込む度に毎回通信が生じてしまいAPIのレートリミットに引っかかるといった問題が起こりがちなので、一旦ファイルに書き込んでおいて裏側でAgentから転送する方法が個人的にはおすすめです)

入れるといっても今回もCOPY技で簡単に入れてあげます。

COPY --from=amazon/cloudwatch-agent:1.247350.0b251780 /opt/aws/amazon-cloudwatch-agent /opt/aws/amazon-cloudwatch-agent
COPY docker-prod/app/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
COPY docker-prod/app/cloudwatch-agent-common-config.toml /opt/aws/amazon-cloudwatch-agent/etc/common-config.toml

設定ファイルの記載方法については公式ドキュメントを参照して書けば完了です!

docs.aws.amazon.com

ちなみに、CloudWatch AgentはENTRYPOINTに指定されているスクリプトを書き換えて裏側でnohupで立ち上げてあげることで、Agentのことを意識せずに動かせるようにしてあります。

#!/bin/sh
set -e

# Agentを裏側で実行する
nohup /opt/aws/amazon-cloudwatch-agent/bin/start-amazon-cloudwatch-agent > /dev/null 2> /dev/null &

# first arg is `-f` or `--some-option`
if [ "${1#-}" != "$1" ]; then
        set -- php-fpm "$@"
fi

# ユーザ切り替え後に/dev/stderrと/dev/stdoutをappユーザから触れないのでパーミッションを変えておく
chmod 777 /dev/stderr
chmod 777 /dev/stdout

# appユーザで指定されたコマンドを実行する(php-fpmなど)
sudo -u app "$@"

USER appなどでDockerfileの最後にユーザを切り替えてしまうとAgentが正常に作動しないので、rootとして最初は実行して実アプリケーションはユーザを降格して動かすという技を使って動かしています。

ENTRYPOINTのスクリプトはDockerfile内でCOPYして書き換えてあげると簡単に動かせる状態になります。

COPY --chmod=775 docker-prod/app/docker-php-entrypoint /usr/local/bin/docker-php-entrypoint

ビルドが遅い.... 遅すぎるぞ!!!!!

さて、弊社のアプリケーションのデプロイはCircleCIから行っている都合でビルドもCI上で行っていますが、どうしてもCIサーバ上だとライブラリのダウンロードが遅かったりPHPのエクステンションのビルドが重たかったりします。

レイヤーキャッシュ*1をしっかり効かせるのも大事ですが、CircleCIのビルドサーバからキャッシュが時間が経つと揮発してしまう*2ことがありデプロイの度にとても待たされることが多くありました。

そこで、レイヤーキャッシュのみならずBuildKit*3も有効化してマルチステージビルドにしている箇所についてはしっかり並列ビルドが走るようにしました。

qiita.com

composer installをマルチステージビルドに持っていってみる

今回は並列で処理できそうなcomposerのパッケージのダウンロードと展開処理を並列で実行できるようにしてみました。

FROM composer:2.2.7 AS composer-cache
WORKDIR /var/www/html

# キャッシュ作成用にcomposer.jsonとcomposer.lockだけ持ってくる
COPY composer.json composer.lock /var/www/html/

# ダウンロードと展開のみ行う
RUN composer install --no-scripts --no-autoloader --ignore-platform-reqs

FROM php:7.4-fpm
WORKDIR /var/www/html

# ここで重たいモジュールのビルド処理などを行う
# ~~各インストール、ユーザ作成処理は省略~~

# キャッシュ展開!!!
COPY --from=composer-cache --chown=app:app /var/www/html /var/www/html

# アプリケーションコードのコピーとcomposer install実行
COPY --chown=app:app . /var/www/html
RUN composer install

BuildKitが有効化された状態でこれを実行するとダウンロードは並行して裏側で行ってくれて、 composer install は実質オートローダの生成のみで爆速で終わるので依存ライブラリが多い方は試してみると良いかと思います。

f:id:kazuki09:20220323192529p:plain

高速化の結果、レイヤーキャッシュが効いた状態で2分程度(キャッシュなしで5分程度)でビルドが完了するようになりました。

何もしていないときは10分かかっていたビルドが5分、キャッシュありで2分まで縮められたので大きな進歩です!!!

僕はいつもなが〜〜いDockerイメージのビルドが走ってる間は推しのアイドル*4のことを考えながら過ごしてましたが、流石にその時間が長すぎるのも辛いので高速化して浮いた時間で仕事を早く終わらせてアイドル現場に行ったほうが良いですね!

(完全に余談ですが、最近弊社では #アイドル部 が出来ました!!好きな人が勝手に集まってやっている小さな部活動ですが、地下アイドルなど様々なアイドルが好きなひとが集まってます!)

さいごに

Dockerやコンテナの運用は非常に奥が深く語ると記事に書ききれない程ハマりポイント、気をつけなければならないポイントがあったりするので、記事では移行にあたり特筆すべきものだけ紹介させていただきました。

最後に... M&Aクラウドでは現在エンジニアやPdMなど幅広く募集中です!!もし興味を持ってくださった方はお気軽にお声がけください!

www.wantedly.com

www.wantedly.com

www.wantedly.com

*1:イメージビルドの過程で変更が無いレイヤを再利用する仕組みのこと https://circleci.com/docs/ja/2.0/docker-layer-caching/

*2:CircleCIのドキュメントによると3日で揮発するとのことです https://circleci.com/docs/ja/2.0/docker-layer-caching/#how-dlc-works

*3:並列的な依存関係の解決などを備えたビルドツールキットのこと。オプトインすることで使えるようになる。 https://docs.docker.jp/v19.03/develop/develop-images/build_enhancements.html

*4:筆者の推しはAppare!の藍井すずちゃんです

スタートアップのデザイナーに必要なマインドセット

f:id:mskikd:20220311190649p:plain

どうも。 M&Aクラウドのデザイナー、池田です。 今回初めて開発者ブログに執筆することになりました。 Tech寄りの話題を書きたかったのですが、デザイナーが書くTechな知見なんてたかが知れてる(ド偏見)ので、スタートアップを数社渡り歩いてきた僕だからこそ書けるような内容にしたいと思いました。

いろいろ悩んだのですが、やっぱりマインドかなと思ったので、今回はマインドセットについて書いていきます。

なぜマインドセットなのか?

中小規模のスタートアップにおいて、デザイナーが抱えやすい大きな悩みってなんだと思いますか?

僕が思うにそれは

孤独

だと思うのです。

スタートアップにおいてデザインの需要は高くなり続けていると感じつつも、デザイナーのリソースを確保できている企業は少ないように感じます。

どの企業もデザイナーの採用に苦戦している、もしくはデザイナー採用の優先順位が低いからか、インハウスデザイナーが1人もしくは2人、という企業が経験上多かった気がしてます。

そんな孤独と戦うデザイナーが安定したパフォーマンスを発揮するために必要だと思っているマインドセットを教えます。

完璧主義はゴミ箱へ捨てよう。スピード>>>>>こだわり

デザイナーにありがちなのですが、自分が納得いくクオリティでないとリリースはおろか、レビューにすら出さないという人、結構いるんですよね。

そもそもここで言うクオリティとはなんなのか?これをきちんと言語化できるのであればそこにこだわるのは重要だと思います。ですがここを言語化できる方は経験上そんなに多くありません。もっと言えば言語化できてもただの主観的感想である場合が多いです。

こだりが過ぎてリリースが遅れるのは本末転倒です。ましてやスタートアップであれば尚更です。

そのこだわりはユーザーと会社を幸せにするのか?というのは自問自答していきたいですね。

関心を向けて欲しいなら、まずは自分から歩み寄る。

これは人間関係全般に言えますが、相手を振り向かせたいのであれば、まずは自分から関心を向けることが非常に重要です。

むしろこれができれば孤独感とは無縁の生活を送ることができます。

しかし業務内容やスキームがまったく違うもの全てに興味関心を向けるのは、キレイごと抜きにしてまず無理だと思います。

なので、まずは人に関心を向けましょう

オススメのムーブとして推奨できるアクションは、以下の二つです。

  • デザインの手を必要としてそうな人に話しかける

  • 自分の仕事に人を巻き込む

最初はスモールアクションでいいので実践してみましょう。

思っているより人は優しい

これはまだ自分でもできていない(と思っている)のですが、社内のSlackなどで興味が働いた話題には立場役職を一旦忘れて乗っかってみることをオススメします。

多少空気を読むのは大事ですが、読み過ぎてしまうのもプレゼンス向上の妨げになると思っているので、激しく議論している場以外であれば「なんの話してるんですかー?」みたいなノリで話しかけてみるのもアリです。

自分が思っているより人は優しいと思い込めば、多少図々しくても堂々と振る舞えるはずです。

逆にここで冷たくあしらわれることが多い環境であれば、転職することを僕はオススメします。

最後に

この場でこんなことしていいのかわかんないけど、意外とアリだったりする。今までもそんなことの連続でした。

何事も自分の思い込み一つでどうにでもなっちゃうのが人生です。

デザイナーという職業に関係なく、どんな人にとっても有効なマインドセットだと思うので、一度脳内にインストールしてみることをオススメします。

最近新しい仲間が増えて、ようやくデザイナーチームが結成できました。僕より優秀で経験豊富な方がジョインしてくれたので本当に嬉しく思います。

おまけ

M&Aクラウドでは現在エンジニアやPdMなど幅広く募集中です。お気軽にお声がけください。

www.wantedly.com

www.wantedly.com

www.wantedly.com

【入社エントリ】スタートアップとM&Aと私。

f:id:mac_tokumoto:20220225001050p:plain

結論、この記事を3行で言うと。

  • 私徳本はExit戦略がないまま起業して、失敗した。
  • その後別会社でPMI、これまた失敗。
  • 「どんな終わりを迎えたいのか」は何を始めるにしても大事。

挨拶と駄文

こんにちは。M&Aクラウドでプロダクトマネージャをしております、とくちゃん(@PdMtokuchan)と申します。このブログを見てくださってる熱烈なM&Aクラウドマニアの方にとっては周知のことかと存じますが、M&Aクラウドは「テクノロジーの力でM&A流通革命を」をミッションに、M&Aのマッチングプラットフォームを運営している会社です。手前味噌ながら当サービスも順調に成長を続け、昨年には10億円の資金調達も完了し「今グイグイきてますね」「バキバキっすね」「コッカラッス」と方々からご声援を賜っておりまして、大変誇らしい気持ちと「俺まだ何もしてないんだけどな」という後ろめたさの間で日々奮闘しております。

note.com

そんなM&Aクラウドで私が普段やっていることは、非常に大雑把にいうと「解くべき課題は何かを明らかにするお仕事」でして、いわゆるユーザインタビューを実施したり、ビジネスサイドのメンバーと共にオペレーションを回したりしながら「あれからぼくたちは、何かを解決できたかなあ...」と頭を抱えながら「解くべき課題は何か」ずっと考えております。実はM&Aという世界は、レガシーかつ非常にブラックボックスな部分が多く、まだまだ課題がたくさん存在しています。そしてそれらの多くは、未だ解決の糸口が見つかっていない混沌とした世界なのです...。私も入社して半年が経ちますが、まだ結果を出せていないことに加え、事業ドメインの難解さに頭が追いつかないシーンも多々あり、悪戦苦闘の毎日で、毎晩ガチで泣いてます。

さて、この記事は入社エントリということですが、私がM&Aクラウドに入社したのは2021年8月ですから、入社エントリを書くにはあまりにも遅すぎるのではないかと、そんな声も聞こえてきます。が、私だって自分語りをさせてほしいよと。ちょっとくらい自分の話をしてもいいじゃないですかと、ワガママを許していただきまして、筆を走らせております。開発メンバーのみんな、ブログを書かせてくれてありがとうございます。

先にこの記事のキーワードをお伝えしておくと「Exit戦略の欠如」です。これが、現時点で私が強く感じている問題意識であり、私自身大切にしたいと考えているテーマでもあります。

スタートアップと私

私事ながら今年で齢30を迎えるのですが、就職をして初めて正社員として働き始めたのが28歳からでして、ここ数年で「お金もらって休めるだと!?有給やべえ!」「タイムカード切るってボタン押すことなのか」など、いわゆる会社として当たり前に存在している概念を肌で感じ、プリキュアくらい目をキラキラさせながら仕事をしておりました。それまでは何をしていたかというと、5年も通った大学を5年連続で単位を落とした化学Ⅰに心を折られ退学し、ほぼ未経験にも関わらずフリーランスWebデザイナーを名乗り「なんでも作れます」と大風呂敷を広げながらコーポレートサイトやLPをゴリゴリ作ってなんとかその日暮らしの日銭を稼ぐようなスタイルで生存していました。2015年、23歳のときです。キャッシュフローも一切安定してないので「次の入金まで残高17円、あと1週間どうする?」みたいな日々で、金ないね🤗ってことでバンド友達(当時バンドやってたので)と高円寺の高架下で明け方まで「弾き語りして投げ銭もらうまで帰れまテン」を開催し、泥酔寸前のおっちゃんから1万円投げられるという奇跡を起こすなど、もう今思い返しても大変怪しい奴だったと思います。靴工場に住んでましたし。

tokyo-style.cc

そんな怪しい私を面白がってくれていた大学時代の先輩に誘われる形で一緒に会社を作りました。2017年、25歳の時です。当時はKURASHIRUやDELISH KITCHENなど「動画メディアがやべえ」みたいな流れが出来ていた頃で、同じようなジャンルのメディア同士でもガンガン調達してて、純広告の単価も再生数あたり十数円みたいな、今考えるとかなり過大評価されてた市場だったと思うんですが、とにかく動画の波がやばいんだってことで我々も動画メディアにかけましょうとなり、漠然と市場規模もそこそこありそうなフィットネス・ダイエット市場を狙って上場目指そうぜということでダイエット動画メディアをインスタで立ち上げることになりました。これが1つ目の「Exit戦略の欠如」です。

メディアの方も運良く成長し続けることができ、国内でフォロワー数が最も多いダイエット動画メディアへと育てることができました。その影響もあって、動画メディアを立ち上げたいクライアントのご相談も増え、会社としても少しずつ成長し、ご縁があって初めての資金調達をさせていただく機会もいただき「なんかよくわからんが最近良いこと続いてるし俺たちいけるのでは?」みたいな傲慢な気持ちが生まれてしまったのもこの頃だったと思います。非常に漠然とした戦略として「集客装置が完成すれば、あとは何かを売れば良いだけじゃん」という超絶雑な考えが当時あって、そのシナリオ通りにはきていたのですが、「何かを売る」ということの難しさを全く理解できていなかったこと、そして会社としてどこに向かっていきたいのか明確にできないまま走り続けてしまったことが仇となり、結果として非常に中途半端な形で様々なサービス・商品を生み出し、そして明確な撤退基準もないまま立ち上げたサービスや商品たちはクローズさせ、メディアだけがぽつんと一人残ってしまいした。

「戦略をちゃんと描けてなかったよね」と言ってしまえばそれまでなのですが、もう少し掘り下げて考えると「会社としてどこをゴールにして向かっていくのか」≒Exit戦略を描いていく必要があったのかなと、今にして思うのです。大まかに言えば 1.経営を続ける 2.誰かに譲る 3.廃業するの3パターンがあって、その中でも上場するのか非上場のまま続けるのか、社員を後継者にするのか他社に買ってもらうのか死ぬまで経営者を続けたいのか、etc...。会社のゴールひとつとっても様々な形があるはずです。そのゴールから逆算して、どの市場に張ってどの上場企業をベンチマークし、どこを競合優位性として戦っていくのかなど、一つずつ決めていくべきでしたが、当時の私たちは漠然と「動画がキテる」とか「そこそこ市場規模が大きい」みたいなふわっとした理由で事業ドメインを決めてしまい、そこに全振りしてしまった。不幸なことにフォロワー数などメディアに係る短期的なKPIは大きく成長していていたことも、後戻りできなかった大きな要因だと感じています。Exitを考えるということは長期でものごと考えることだと言うことです。これが私にとっての初めてのスタートアップであり、大きな失敗でした。

M&Aと私

その会社では前述の通り多くの失敗を経て、私と共同創業者の方との間で「目指す方向性違ってきちゃいましたね」という話し合いをして、私は退職して一般企業に就職することにしました。今でもそうですがあらゆる部分でビジネスマンとしてのスキルや経験が乏しすぎるなという実感があったので、色々な経験してる人と働いてみたいなということで初の転職活動です。何処の馬の骨かもわからない私をご縁があって拾ってくださったゲーム開発会社がありまして、そこで1年ほどお世話になりました。

そこでは「新規事業をやるぞ!」ということでディレクター的な動きをしていたのですが、前職の共同創業者の方から「動画メディアを売りたい」という話を聞いていたのでゲーム会社の上長に「買ってみます?」とフワッと話を持っていったところ、会社としてライブ配信や動画に注力していたということも相まって事業買収へと進んでしまいました。これが2つ目の「Exit戦略の欠如」です。

当時、私の中でダイエット動画メディアが事業として成り立つ正解の一つは物販だと考えていました。これは前職でPoCとしてメディアを通してアパレルウェアなどを販売していたのですが、撤退理由が「入金サイクルが遅くて在庫積めないから」というもので、逆説的にキャッシュがあればクリアできる問題だと考えていたからです。当然それだけでうまくいく理由にはなりませんが、お金がない弱小企業にとって超絶ボトルネックであったキャッシュフローが解消される環境であれば、まだやりようがあるのではと思い企画書を持ち込み経営陣にプレゼンしました。が、結論としては「在庫ビジネスを社としては取り組まない」と跳ね返され、すでに経営陣の中で決まっていたある事業内容で進めていくぞということでPMI担当を任されることとなりました。詳細は書けないことも多いので端折りますが、いくつかの理由でプロジェクトは頓挫し、事業として再生することはないままプロジェクトは終了してしまいました。

ひとえに私の力不足に尽きるわけですが、経営陣と事業としてのゴール≒Exit戦略をうまく共有できず、プロジェクトを破綻させてしまいました。もちろん、世の中の起業家・経営者は私よりも数百倍優秀ですから、こんな話は笑われてしまうかもしれませんが、現実にPMIというものは非常に難易度が高く、どの企業も買収後どのようにして既存事業や社員とシナジーを発揮しながら成長させていくかは大きな課題と言われています。小さなプロジェクトではありますが、私自身PMIの失敗を経て、M&Aというものの難しさを強く実感した貴重な体験でした。

私はこれから

こうやって振り返ると、業界的には全くの畑違いからM&Aクラウドへやってきたわけですが、M&Aというものは私にとって大変苦い思い出であり、そして何故だかご縁を感じずにはいられないテーマなわけであります。マクロな話や業界の専門的な話は弊社が運営するメディア「Update M&Aクラウド」にお任せするとして

update.macloud.jp

note.com

update.macloud.jp

私の個人的な想いとしては、すべての起業家や経営者が正しいExit戦略を描き、彼らやステークホルダーがもっとハッピーになる世界になってほしいと強く願っています。起業をするにしても、あるいは企業を買収するにしても、何かを始めたら必ず終わりがあるはずで、きっとそれは私のような一介の社員にとっても同じくらい重要なことだと感じています。「どんな終わりを迎えたいのか」そんなふうに書くと少し大袈裟なようにも聞こえますが、私たちはみんないつか死んでしまうし、今続いてる幸せも永遠ではありません。だからこそ、目の前の小さなタスクから、家族や会社の未来、自分の人生まで「どんな終わりを迎えたいのか」ということを自問自答しながら日々生きていきたい。そんなことを考えながら、私はM&Aクラウドで出来ることを精一杯やり続け、終わりに向かって走り抜けたいと思います。なんだこの入社エントリ。よし、今日もがんばるぞ。

あと、採用も頑張ってます。(唐突)「M&Aクラウドってどんな感じ〜?😇」みたいな軽ノリで雑談するのも大歓迎ですので、wantedly経由でもTwitter経由(@PdMtokuchan)でもコンタクトとっていただければと思います。おしゃべりしましょう〜!

www.wantedly.com

www.wantedly.com