$shibayu36->blog;

株式会社はてなでエンジニアをしています。プログラミングや読書のことなどについて書いています。

開発チームの責務を「エンジニアリング観点でのサービス継続リスクをコントロールしながら、開発速度を最大化する」としてみた話

最近開発チームの改善を行う時に、どういう目的で開発チーム改善を行うのかや、開発チームの責務は何なのかについて悩んでいた。色々本を参考にしながら、自分の中でしっくり来た責務があったので、ブログにまとめておく。

まず自分の中で、開発チームの責務は次のものであると言語化した。

  • エンジニアリング観点でのサービス継続リスクをコントロールしながら、開発速度を最大化する

なぜこの責務としたか

まず現代のソフトウェア開発においては、非常に不確実な状況で、顧客にとって価値があるものが何かを探索しながら、高速に価値を創出・提供しなければならない。これを満たすためには、「正しいものをつくる」ということと、「正しくつくる」ということの両輪を回す必要がある。

この時、プロダクトオーナー側と開発チーム側で分業するとすれば、やはり開発チームは「正しくつくる」ことに焦点を当てて責務を持つと良いと考えた。つまり開発速度(価値提供速度や試行回数)を最大化することを第一優先とする。

一方で、何も考えずに開発速度を高めすぎると、エンジニアリング面でサービスが継続困難になるようなリスクが高くなりすぎるかもしれない。例えば属人性を高めれば開発速度は高められるかもしれないが、コア部分で特定の人しか分からない実装が出来てしまうと、その人が退職した時にサービスの開発ができない状態に陥ってしまうかもしれない。例えば、開発速度を重視するあまり、サービスの可用性を犠牲にしてしまうと、信頼できるサービスとみなされず、サービス自体が頓挫してしまうかもしれない。例えば、リリースに間に合わせることを意識するあまり、セキュリティの考慮が漏れると、ユーザーに不利益を与え、サービスを閉じなければならないかもしれない。こうなると困るので、開発速度は最大化しながら、リスクはコントロール可能な状態に制御しておく必要がある。

以上より、最初に書いた「エンジニアリング観点でのサービス継続リスクをコントロールしながら、開発速度を最大化する」を開発チームの責務とすれば良いと考えた。

ちなみにこの責務に込めた他の意味としては

  • エンジニア観点ではなくエンジニアリング観点
    • 総合的な開発能力の話をしているので、エンジニアだけでなく、他の開発に携わる職種(企画、デザイナ、翻訳担当など)全体のエンジニアリング能力に焦点を当てる
  • リスクを減らすことを責務とするのではなく、リスクのコントロールを責務とする
    • リスクを0にすることは出来ないし、小さくしようとしすぎると多大なコストがかかってしまう。結果として開発速度も落ちてしまう
    • あくまで開発速度の向上を第一とし、リスクはコントロールして許容範囲を超えないようにする
    • これはSREにおける、信頼性と開発速度の関係を意識した

この責務を意識した3つの改善の軸

さらにこの責務を意識した上で、自分が改善を提案する時は3つの軸に分けて考えてみている。

  • 1) 開発速度が最大化できているか
  • 2) エンジニアリング観点でサービス継続リスクをコントロールできているか
  • 3) 正しいものを作れているか

1と2については責務をそのまま分割した形にしている。3については先程プロダクトオーナーと分業した責務だが、

  • 変に分断を作りたくない。それぞれが越境し、顧客の価値最大化を全員で目指したい
  • 開発速度を最大化しても間違ったものを作っていたら意味がないので、開発チームから気づいたものがあれば「正しいものを作る」ための提案をするべき

と考え、あえて改善の軸に入れている。

この3つの軸に分けて考えた時、チーム改善を行う時に施策に一本筋が通るように感じ、いろんなレイヤーで改善提案をしやすくなったと感じている。例えば

  • デプロイの自動化
    • 開発パフォーマンスに有意に影響を与える「変更のリードタイム」「平均修復時間」「デプロイ頻度」(LeanとDevOpsの科学で言及されている)という指標に良い影響を与えるため、特に開発速度の軸の改善になるだろう
  • ソフトウェアアーキテクチャ設計
    • 今後柔軟に変更出来る状態にしておくと、開発速度の軸に好影響を与える
    • しかし、早すぎる最適化をしすぎると、サービス継続リスクを下げようとしすぎて、むしろ開発速度を落とすことに繋がるので注意する
  • レビューフローを改善する
    • レビューが遅れると「変更のリードタイム」に悪影響があるため、改善することで開発速度を上げられる
    • もし一部のメンバーしかレビューをしていないと属人性の問題を抱えるかもしれない。リスクをコントロールするため、レビューによって属人性をある程度に保つ
  • スプリント会におけるタスクの入荷フローの改善
    • タスクの入荷フローに問題を抱えると、顧客に最大の価値をもたらす開発を行えなくなってしまう。ここを改善することで、より正しい優先度で開発ができるようになる
  • 採用の改善
    • 優秀なリソース確保は、開発速度をスケールさせるために必要である
    • また採用力が相対的に低まると、サービスを継続させるための最低限の人員も確保できなくなるかもしれない

このようにTechなレイヤー、開発フローのレイヤー、ヒューマンマネジメントのレイヤーなど、どのレイヤーでも3つの軸を意識すると改善の目的がぶれないようになっている。

まとめ

今回は開発チームの責務とは何かについて自分なりに考え、一旦「エンジニアリング観点でのサービス継続リスクをコントロールしながら、開発速度を最大化する」とおいたことについて書いてみた。改善の軸を3つに分けて考えるようにしたことで、改善の目的を見失わずに一貫性を持って改善提案を出来つつある。

今後は開発のパフォーマンスを定量的に見えるように、「変更のリードタイム」「平均修復時間」「デプロイ頻度」の可視化や、エラーバジェットの可視化などを進めていきたいと思っている。

参考

blog.shibayu36.org

blog.shibayu36.org

blog.shibayu36.org

ReactのuseEffect/useLayoutEffectやレンダリングの実行順について調べた

useEffect/useLayoutEffect周りで絶賛ハマり中で、また React17におけるuseEffectの破壊的変更を理解する | Zenn の記事が面白かったので、自分の手元で動かしてみて理解を深めてみた。調査したことを自分用のメモとして取っておく。React 17でuseEffect周りで破壊的な変更が入るということなので、Reactのバージョンは17.0.0-rc.1で試した。

気になっていたこと

  • 親子のコンポーネントでuseEffectがどの順で実行されているのか
  • useLayoutEffectが途中にあるとどのような挙動をするか
  • useEffect/useLayoutEffectとレンダリングやDOMへの反映の実行順序

調査メモ

shibayu36/typescript-playground/react-playground あたりで試した。react-playground ディレクトリ以下でyarn installしてyarn startしたら試せる。

以下2つのエンドポイントを実装し、行き来しながらどのようなログが出るか見た。

/ -> /effect-order へアクセスした時は以下のようなログが出る。この時、LayoutEffect useLayoutEffect では同期的に2秒待ちを入れていたので、実際に画面に表示されるのは2秒後となった。Child useEffectは2秒待ちを入れていたが、画面表示には影響せず、その代わり後続のuseEffectは遅延された。

EffectOrder rendering
Effect rendering
LayoutEffect rendering
Child rendering
Home ref null
Child ref <div>​Child Component​</div>​
LayoutEffect ref <div>​…​</div>​
LayoutEffect useLayoutEffect
Effect ref <div>​…​</div>​
EffectOrder ref <div>​…​</div>​
Child useEffect
Effect useEffect
EffectOrder useEffect

/effect-order -> / のときは以下のようなログが出る。こちらもuseLayoutEffectのcleanupに2秒待ちを入れていたのでそれが実行されるまでDOMへの反映も遅延された。

Home rendering
EffectOrder ref null
Effect ref null
LayoutEffect useLayoutEffect cleanup
LayoutEffect ref null
Child ref null
Home ref <header class=​"App-header">​…​</header>​
EffectOrder useEffect cleanup
Effect useEffect cleanup
Child useEffect cleanup

この実行から分かったことは

  • Virtual DOMの構築はたぶん「... rendering」というログのところで実行されていそうなので、useEffect/useLayoutEffect関係なく作られてそう
  • 実際のDOM構築はたぶん「... ref」というログのところで実行されていそうなので、useLayoutEffectはこのタイミングで実行される。実際のDOM構築が全て終わらないと画面表示出来ないので、useLayoutEffectで重い処理をするとユーザーへの描画タイミングが遅れる
  • useEffectはDOM構築が終了した後に実行されるので表示には影響しない。マウント時は子から親の順で実行され、アンマウント時は親から子の順で実行される。途中で重い処理が行われていると後続のuseEffectは遅延する。

フェーズとしては、Virtual DOM構築フェーズ、実際のDOM構築 & useLayoutEffect実行フェーズ、useEffect実行フェーズがある感じか?

まとめ

ReactのuseEffect/useLayoutEffectやレンダリングの実行順について調べたので自分用のメモを残してみた。Reactのドキュメントに書いてあるような内容を再確認しただけにはなったが、自分の手を動かしたためか、もっと実感を持って理解できたような気がする。

参考文献

ALBで特定のpathのときだけCognito認証を通す構成をaws-cdkで作る

管理画面の時だけ認証をかけたいみたいな要件はよくある。今回はその要件をALBで特定のpathの時だけCognito認証を通すという構成で実装してみたのでメモ。構成はaws-cdkを使った。

やりたいこと

  • /admin/以下の場合はCognitoの認証をかけ、許可したユーザーしかアクセスできないようにする
  • それ以外のパスではアプリケーションに認証なしで繋がるようにする

f:id:shiba_yu36:20200918094327p:plain

実装

aws-cdk 1.62.0を使って実装した。なお今回のコードは実際の実装をミニマムに変更しているので、動作確認はちゃんと出来ていない(たぶんそのままだと動かなそう)。雰囲気だけ感じてください。

import { IVpc, Port } from "@aws-cdk/aws-ec2";
import { Ec2Service } from "@aws-cdk/aws-ecs";
import {
  ApplicationLoadBalancer,
  ApplicationProtocol,
  ListenerAction,
  ListenerCondition,
} from "@aws-cdk/aws-elasticloadbalancingv2";
import { AuthenticateCognitoAction } from "@aws-cdk/aws-elasticloadbalancingv2-actions";
import { Construct, Duration } from "@aws-cdk/core";
import {
  UserPool,
  UserPoolClient,
  OAuthScope,
  UserPoolDomain,
  CfnUserPoolClient,
} from "@aws-cdk/aws-cognito";

export interface PathRestrictedAlbProps {
  /**
   * アプリケーション
   */
  appService: Ec2Service;

  /**
   * ALBなどを配置するvpc
   */
  vpc: IVpc;
}
export class PathRestrictedAlb extends Construct {
  constructor(
    parent: Construct,
    name: string,
    props: PathRestrictedAlbProps
  ) {
    super(parent, name);

    const applicationName = "example";

    // パスワードポリシーとか自分の用途に合わせていい感じにする
    const userPool = new UserPool(this, "UserPool", {
      userPoolName: applicationName,
      selfSignUpEnabled: false,
      signInAliases: {
        username: true,
        email: true,
      },
      standardAttributes: {
        email: {
          required: true,
        },
      },
      passwordPolicy: {
        minLength: 20,
        requireLowercase: true,
        requireUppercase: true,
        requireDigits: true,
        requireSymbols: true,
      },
    });
    const userPoolClient = new UserPoolClient(this, "UserPoolClient", {
      userPool: userPool,

      // Required minimal configuration for use with an ELB
      generateSecret: true,
      authFlows: {
        userPassword: true,
        refreshToken: true,
      },
      oAuth: {
        flows: {
          authorizationCodeGrant: true,
        },
        scopes: [OAuthScope.EMAIL],
        callbackUrls: [`https://${applicationName}.com/oauth2/idpresponse`],
      },
    });
    const cfnClient = userPoolClient.node
      .defaultChild as CfnUserPoolClient;
    cfnClient.addPropertyOverride("RefreshTokenValidity", 1);
    cfnClient.addPropertyOverride("SupportedIdentityProviders", ["COGNITO"]);

    const userPoolDomain = new UserPoolDomain(this, "UserPoolDomain", {
      userPool: userPool,
      cognitoDomain: {
        domainPrefix: applicationName,
      },
    });

    const alb = new ApplicationLoadBalancer(this, "Alb", {
      vpc: props.vpc,
      loadBalancerName: applicationName,
      internetFacing: true,
    });
    alb.connections.allowFromAnyIpv4(Port.tcp(443));

    const listener = alb.addListener("Listener", {
      port: 443,
      protocol: ApplicationProtocol.HTTPS,
      open: true,
      certificateArns: [
        // 証明書を設定する
        "...",
      ],
    });

    // デフォルトはECSへ
    const targetGroup = listener.addTargets("Target", {
      targetGroupName: applicationName,
      protocol: ApplicationProtocol.HTTP,
      port: 80,
      targets: [props.appService],
    });

    // conditionsをいい感じに設定することで特定pathだけCognito通すとかができる
    // ここでは/admin/以下の場合はCognito認証を通し、認証をパスした場合のみ
    // ECSへ到達するようにする
    listener.addAction(`Action1`, {
      action: new AuthenticateCognitoAction({
        userPool: userPool,
        userPoolClient: userPoolClient,
        userPoolDomain: userPoolDomain,
        next: ListenerAction.forward([targetGroup]),
      }),
      conditions: ListenerCondition.pathPatterns(["/admin/*"]),
      priority: 1,
    });
  }
}

あとはCognitoのユーザー管理でアクセスできる人を追加したら良い。 f:id:shiba_yu36:20200921131912p:plain

これで、/admin/以下にアクセスしたときに認証のダイアログが出て、先程追加したユーザーしかアクセスできなくなる。 f:id:shiba_yu36:20200921131953p:plain

まとめ

今回はALBで特定のpathのときだけCognito認証を通す構成をaws-cdkで作ってみた。今回は特定パス以下だけ認証を通すようにしたが、例えば管理画面はドメインごと変えたいとなった場合にもデフォルトアクション全てで認証をかけるようにすれば同じような構成で出来ると思う。

Cognitoは他にもMFAを使うなど認証系は簡単に実装できるので、自分で実装したらだるい認証系はSaaSに任せていけると良さそうだった。

参考

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-elasticloadbalancingv2-actions-readme.html