【対応急げ!】個人情報保護法改正に伴うCookie同意の対応について

こんにちは。エンジニアの濱田 (@hamakou108) です。

2022年4月に施行された個人情報保護法改正についてご存知でしょうか? 法改正に伴い、 Cookie 等の取り扱いについてユーザに確認を取ることが必須になりました。 弊社プラットフォームのM&Aクラウドでは、個人情報保護法改正にともなって Cookie policy, Privacy policy を改訂するとともに、ポップアップを表示することでユーザに明示的にCookieの取扱について確認するようにしています。

サイトに埋め込まれた Cookie 同意ポップアップ

背景

個人情報保護法の改正

2022年4月に改正個人情報保護法が施行されました。 この法改正には、個人関連情報 1 の第三者提供の制限等が付け加えられており、第三者が個人関連情報を個人データとして取得することが想定される場合の確認義務が定められています。

専門家ではないので厳密なことは言えませんが、 Web サイト上で Cookie 等を介して第三者にデータを提供する前に、多くのケースにおいてユーザーの同意を得る必要が生じています。

Cookie 同意のツールの環境

海外でも GDPR 等の個人データ保護規則の遵守に関する関心が高まっており、様々な同意管理プラットフォーム(以下 CMP と表記)が出現しています。 また Google Tag Manager (以下 GTM と表記)などの既存ツールも同意モードに対応しつつあります。

CMP の特徴と使わなかった理由

CMP を使うと、ユーザー同意を得るためのポップアップを埋め込む、サードパーティースクリプトの実行を制御するといった機能が簡単に実現できます。 多くの CMP は様々な要件に対応するために豊富な機能を提供している一方、その分定額課金となっています。 弊社のケースではシンプルに同意する・同意しないの二択を選択できれば良かったため、自前で実装してもペイするだろうと判断し、 CMP の利用は見送りました。

Google Tag Manager の同意モード概要

GTM では同意の状況に応じてGTMタグを発火させるかブロックさせるべきか選択できます。 例えば以下のGTMタグですと、赤枠で囲った部分が該当の設定になります。

同意設定が行われた GTM のタグ

「追加同意チェック」のところにユーザに許可されるべき同意モードを設定しておくと、それらの同意がされていない場合にはGTMが発火しないようにできます。これにより、該当のスクリプトがHTMLに差し込まれないので、当然ユーザの個人関連情報を送信することはなくなります。

対応

GTMの同意モードを用いることで、包括的にスクリプトを差し込む/差し込まないを管理できることがわかりました。弊社ではサードパーティスクリプトをGTMに乗せることを原則とし、GTMの同意モードで管理することにしました。

Google Tag Manager の設定

まずソースコード中で追加していたスクリプトタグは GTM に移行しました 2 。 元々なぜソースコード中に埋め込んでいたかと言うと、ユーザー ID など、ソースコード上でなければ取得できない値を使用するからでした。 今回はソースコード上で window のプロパティにこれらの値を仕込み、 GTM からその値を参照する形を取りました。

次に GTM の同意設定です。 任意のタグについて、設定の「追加同意チェック」から「タグの配信時に追加同意が必要」をチェックして必要な同意を追加します。 3 この設定により、ユーザーから同意を得られていない場合はタグが発火しないようになります。

また同意タイプを設定することで、ユーザーが行った同意の種類(機能性 Cookie には同意する、広告には同意しないなど)に応じてタグの発火を制御できます。 そのタグが発火する条件として適切な同意を設定することで、ユーザーが何について同意するか細かく選択できる仕様を実現できるでしょう。

追加同意チェックを設定する

ちなみに上記のようにタグごとに設定することもできますが、複数のタグをまとめて設定変更することも可能です。

盾マークのボタンを選択

複数選択してまとめて同意設定ができる

Cookie 同意状況の初期化

GTM に現在の同意状況を読み込んでもらうには、 window.dataLayer にデータを追加します。 各サイトに以下のようなコードを埋め込み、ページ表示時に同意状況が初期化されるようにしています。

export class GoogleTagManager {
  private setConsentMode() {
    // Define dataLayer and the gtag function.
    window.dataLayer = window.dataLayer || []

    function gtag() { window.dataLayer.push(arguments) }

    // デフォルトではすべての同意タイプについて 'denied' を設定
    gtag('consent', 'default', {
      ad_storage: 'denied',
      analytics_storage: 'denied'
    })

    // localStorage に過去の同意記録が残っていた場合に 'granted' を設定(詳細は後述)
    if (localStorage.getItem('cookie-consent-granted')) {
      gtag('consent', 'update', {
        ad_storage: 'granted',
        analytics_storage: 'granted'
      })
    }

    window.dataLayer.push({
      event: 'default_consent'
    })
  }
}

consent default を設定するタイミングについては注意が必要です。後述する Consent Initialization トリガーで発火するCookie同意ポップアップの中で 同意状況の初期化をしようとしたのですがそれは上手くいきませんでした。 それは、consent defualt の設定は必ずGTM自身のスクリプトを読み込む前に設定しておく必要があるためです。ドキュメントの中で(2022/05/27時点では文章中のタブの中に隠れていて分かりづらいのですが)以下のように書かれています。

サイトのすべてのページで、タグが呼び出される前に以下を行う必要があります。
dataLayer オブジェクトが定義されていることを確認する。
gtag() 関数が定義されていることを確認する。
gtag('consent', ...) コマンドを使用して測定機能を設定する。
dataLayer.push() を使用して default_consent イベントを送信する。

Cookie 同意ポップアップ

Cookie 同意ポップアップは各サイト共通の独立した社内ライブラリとして開発して、 GTM を使って各サイトに埋め込んでいます。

実装詳細

Cookie 同意ポップアップのメインの実装は以下の通りです。他にこれを読み込む薄い index.ts や scss もありますが、本筋に無関係なので省略します。

export class CookieConsentPopup {
    private popupElement: Node
    private storageKey = 'cookie-consent-granted'

    private static gtag(..._: any) {
        (window as any).dataLayer = (window as any).dataLayer || []
        // オリジナルのgtagファンクションを参考に arguments を直接使うように実装。
        // @ts-ignore
        (window as any).dataLayer.push(arguments)
    }

    private static gtmEventPush(eventName: string) {
        (window as any).dataLayer = (window as any).dataLayer || []
        (window as any).dataLayer.push({
            event: eventName
        })
    }

    constructor() {
        this.init()
    }

    private init() {
        const isAlreadyConsentGranted = localStorage.getItem(this.storageKey)
        if (isAlreadyConsentGranted) {
            return
        }

        this.showPopup()

        document.querySelector('.cookie-consent-popup__disagree').addEventListener('click', () => {
            this.disagree()
        })
        document.querySelector('.cookie-consent-popup__agree').addEventListener('click', () => {
            this.agree()
        })
    }

    private showPopup() {
        const popupElement = document.createElement('div') as HTMLDivElement
        this.popupElement = popupElement
        popupElement.innerHTML = `
<div class="cookie-consent-popup">
  <div class="cookie-consent-popup__content">
    <p>当サイトを引き続きご利用いただく場合は、当社のプライバシーポリシー及びCookieガイドラインをよくお読みいただき、これらに対する「同意する」ボタンを押して下さい。</p>
    <div class="cookie-consent-popup__button_wrapper">
        <button class="cookie-consent-popup__disagree">同意しない</button>
        <button class="cookie-consent-popup__agree">同意する</button>
    </div>
  </div>
</div>`
        document.body.appendChild(popupElement)
    }

    public agree() {
        CookieConsentPopup.gtag('consent', 'update', {
            'ad_storage': 'granted',
            'analytics_storage': 'granted',
        })

        this.destroy()
        localStorage.setItem(this.storageKey, 'true')
        // gtm event. Trigger for many gtm tags.
        CookieConsentPopup.gtmEventPush('cookieConsentGrantedGtmEvent')
    }

    public disagree() {
        this.destroy()
    }

    private destroy() {
        document.body.removeChild(this.popupElement)
    }
}
Cookie 同意ポップアップを表示するトリガー

GTMにおいて今回のような同意モードに関するトリガーはConsent Initialization - All Pages を使用するのがベストプラクティスだとされています。

「同意の初期化」トリガーは、他のトリガーが起動する前にすべての同意設定が適用されるように設計されています。

Cookie同意された時にGTMのタグを発火させる

Cookie 同意されていない時点では、All Pages のトリガーのうち「タグの配信時に追加同意が必要」なトリガーは発火がブロックされています。 ページが表示し終わった後にユーザがボタンクリックで同意した場合でも、すでにAll Pages のトリガーはGTMのContainer Loadedのタイミングで実行されているので、ユーザが同意したタイミングではすでに発火タイミングを逃してしまっている状態です。 このため、ボタンクリックのタイミングで発火する追加のトリガーを設定しておく必要があります。それが前述の実装コードにおけるCookieConsentGrantedGtmEventイベントです。 このイベントをAll Pages のトリガーを持つタグに追加で設定しておけば、ユーザが同意したタイミングでタグを発火させることができます。

トリガー設定例

タグへのトリガー追加例

例外として組み込み同意モードがあるGoogle 系のスクリプトの扱い

google 系のスクリプトは「組み込み同意チェック」という機能が最初から入っています。 組み込み同意チェックが入っているかどうかはGTMタグの設定の中の”同意設定”の項目を見るとわかります。 以下の画像のものですと、 ad_storage と analytics_storage については組み込みされており、これらの同意がされなければ個人関連情報を送信しないようになっています。詳細についてはドキュメントを参照してください。

組み込み同意チェックがある場合、追加同意チェックは不要

このような組み込み同意チェックがあるケースだと、GTMタグ自体は同意モードの発火してスクリプトが差し込まれても問題がないため、追加同意チェックの設定は「追加同意は不要」という設定にしておきます。

最後に

いかがでしたか? 個人情報保護法改正に伴って同じように Cookie 同意の対応を検討されている方の参考になれば嬉しいです。

弊社ではエンジニアを積極採用中です!

www.wantedly.com

www.wantedly.com


  1. 生存する個人に関する情報であって、個人情報、仮名加工情報及び匿名加工情報のいずれにも該当しないものと定義されています。

  2. JS だけでなく CSS も含むコードは GTM に移行するのがハードだったので、移行していません。これらは localStorage の値を直接参照して同意済みかどうか条件分岐させています。

  3. タグの追加同意チェックを設定するには、コンテナの設定で「同意の概要を有効にする」にチェックを入れる必要があります。

プログラムを消すライブラリrice-ballを作った

皆さんおはこんばんにちは、ゆいです

今回はTypeScriptでライブラリを作ったのでその話をしようと思います。

www.npmjs.com

これなあに?

rice-ballは特定のコメントパターンに基づいてコードを削除するTypeScript製のライブラリになります。

rice-ballと言う名前の由来は私の推している声優さんの高田憂希さんが演じるTokyo 7th シスターズ天堂寺ムスビと言うキャラクターのムスビと言う名前からおむすび->rice ballと言う感じで着けました。

777☆SISTERSしか勝たん

以下のようにコメント記述してnpmでインスコしたrice-ballを実行することで指定したコードブロックを削除したりファイル毎削除することができます コメントのパターンはREADMEを参照ください。

befor:

export default function example(): void {
  const scream = '高田憂希しか好きじゃない'
  /* rice-ball start example-flag */
  console.log(scream)
  /* rice-ball end example-flag */
}

after:

export default function example(): void {
  const scream = '高田憂希しか好きじゃない'
}

なんで作ったの?

※なぜコードを削除するライブラリが欲しかったのかはphp-delの過去記事で詳しく書かれているのでこちらを参照ください。

久保田さんの開発されたphp-delはvueやjs, tsなどのファイルに対応しておらず、 フロントエンドのコード削除が手作業でやる必要があったので、基本コンセプトは同じ内容でテキストファイルであれば基本的に全部サポートするライブラリを作ってしまえば良いじゃんと思い開発したと言うところです。

OSSを開発してみての感想

初めてOSSとしてnpmで配信するライブラリを開発してみてこんな事を感じました

とりあえず作ってみるでも良い

これは久保田さんもphp-delの記事でも書かれていますが、万人ウケするものではなく、自分たちのニーズに応える物でも良いという風に感じました。 規模が小さくペルソナが身内だとしても、ニーズに応えられるツールを開発すると言うのはとても刺激的で、開発意欲も湧いて楽しく開発することができました。

ちょっとした便利ツールなど小物でも実際に作ってみると言う部分は中々難しいところだと思いますが、作り始めてしまえば小さな物でもニーズに応えることはできるのだと言うのを改めて実感しました。

最後に

少しでもいいなと思ってもらえたらstarつけてください!

そして最後に、高田憂希しか好きじゃない!

以上になります。

現在採用中です!

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

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

はじめに

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

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

共同編集者の、やも(@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!の藍井すずちゃんです