Skip to content

Latest commit

 

History

History
882 lines (600 loc) · 25.1 KB

slides.md

File metadata and controls

882 lines (600 loc) · 25.1 KB
theme class highlighter lineNumbers info drawings
shibainu
text-center
prism
true
# ユーザ管理画面開発
persist

ユーザ管理画面開発説明


layout: quote

激しい開発を経て、プロダクト本体はリリースされた。

しかし利用ユーザや企業の登録・更新・削除処理は、未だエンジニアの手作業で行われていた。

導入企業・ユーザ数が伸びるにつれ、その負荷も指数関数的に増大していた。

これは、トイル撲滅のため立ち上がった男達の物語である。


layout: quote


layout: default-5

目次

  • サービス概要
  • 開発の進め方
  • 使用技術(Dockerfile以上)
  • 使用技術(Dockerfile未満)
  • 今後

layout: section-2

サ-ビス概要


サ-ビス概要

機能要件

  • ユーザ・企業・接続元IPリストのCRUDをする
  • ユーザを作る権限を持った企業外ユーザ(販売代理店等)を管理する

機能要件だけ見るとチュートリアルに毛が生えた程度

---

サ-ビス概要

非機能要件

  • 別VPCのRDSを操作する
  • 認証・認可 OIDC
    • 製品アクセスの有無により、ユーザプールを分割する
  • マイクロサービスの開発・運用を効率化する
    • 冪等性の考慮
      • 分散トランザクション管理をなるべくやらない Sagaパターンの実装が不要なアーキテクチャを考える
    • アプリケーションでの処理をビジネスロジックに集中させる ビジネスロジック以外の処理はなるべくインフラで実装する
      • ログ振分け・サーキットブレイカー等
  • セキュリティ最重視
    • WAF
    • AWSアカウント自体の管理

layout: section-2

開発の進め方


Discussions Issues Pull Requestsの流れが最高

  • Discussions : とりあえずの提案・バグか仕様か分からないので質問、等

  • Issues : やることが決定したもの
  • Pull Requests : 実装のレビュー
  • テンプレートを設置し、ボタンでIssuesの複数テンプレートを使い分ける


複数リポジトリのIssuesを一覧化

  • 各マイクロサービスのリポジトリを1つ1つ見に行く必要がない
  • カスタムフィールドでPriorityを追加
  • Priority毎にグループ分けして表示

JIRAのように扱うため、
labelsで機能補完

closed, blocked by等、
チケット間の関係性を表現

sortが奇麗になるよう、
bug, enhance等の接頭辞を付与

色の並びにも気を配った


layout: section-2

使用技術
(Dockerfile以上)


使用技術(Dockerfile以上)

frontend
backend
CI/CD
認証・認可

使用技術(Dockerfile以上)

frontend SSRに対応
backend DDD指向・オニオンアーキテクチャで実装
リソースAPIではトークン検証処理を行う
CI/CD aws謹製のGithub Actionsで実装
認証・認可 OIDCに則って各APIを構築 ・ Organizations機能(予定)

開発秘話(frontend)

  • 元々の構成は
  • ユーザのロースペックなPC環境を考慮してSSR化を検討。に。
    • vite.jsの構成から、next.jsの構成に移すのが大変だった
    • ググってもjsの記事しか出てこない。tsの型指定が辛い。

開発秘話(backend)

rustの型制約が激しい

  • アプリケーションレイヤーでもセキュリティを高める必要があり、採用
    • 異様に強い型制約、メモリ安全が魅力
    • ロギング、トークン検証等をモジュール化し、アスペクト指向プログラミングを実施
  • とはいえ、型制約が激しくてビルドが通らない。。。
    • MySQLのテーブルでbool値を格納するカラムがtinyint(1)で作成されていた
    • が、Rust側でintを指定すると、ビルドエラー
  • Rust公式によると、MySQLのtinyint(1)はRustではbool型と扱う、とのこと
    • MySQLint(11) unsigned とRustu16 であればエラーだが、MySQLint(11) unsigned とRustu32 はエラーにならない

開発秘話(backend)

DDD

  • オニオンアーキテクチャを以下のように実装した
    • ドメインモデル層: このシステムで扱うべき関心事
    • ドメインサービス層: ドメインモデルのビジネスロジックを定義。アプリケーションサービス層から利用される共通ロジックを提供。
    • アプリケーションサービス層: ユーザとの接点(エンドポイント等)を定義
    • インフラストラクチャ層: 外部ライブラリ、DB等の接続
  • 実装したモジュールをどの層に置くか
    • SQL文の記述、AWS SDKはインフラストラクチャ層に。ドメインモデリングを阻害しないようにする。
    • トークン検証はアプリケーションサービス層
      • エンドポイント毎に検証スコープの範囲が違うため、ビジネスロジックにも思える
      • アプリケーション固有の処理だが、ドメインに関する処理ではないので、アプリケーションサービス層に配置した

開発秘話(backend)

やっぱりrustの型制約が激しい

  • 型制約が激しくてDIが辛い。。。
  • オニオンアーキテクチャの各層をモジュール化して、依存関係逆転の法則を実装する
    • 頑張る

開発秘話(backend)

APIドキュメント管理

  • 当初はOpenAPIに則ろうとした
    • ドキュメントもgit管理したいし、ドキュメントとコードの連携も取りたい
    • だが、Swagger Editorを使うとソースコードと設定ファイルが分離するため、いずれ整合性が取れなくなる
  • 要件をまとめると
    • ドキュメントのgit管理
    • コードからドキュメントの生成
    • ドキュメントからコードの生成
    • ドキュメントの設定ファイルがソースコードから独立していない
    • 独自フレームワークを持たない
      • オニオンアーキテクチャに影響を与えない、受けない
    • 簡便なホスティング

開発秘話(backend)

APIドキュメント管理

  • 意外と要件に合致するものはなかった
  • ホスティングはGitHub Pagesでよい
  • Rust Docで、ドキュメントからコードの生成以外の要件は実現可能
    • ドメインモデル層でレスポンスを定義
    • アプリケーションサービス層のソースコードにパラメーターに関するコメントを記載

Rust Doc + GitHub Pages

  • 以下の前提があれば、ソースコード上のコメントをAPIドキュメントとして機能させられる
    • 外部公開しない
    • フロントエンドのメンバーもrustを読める

開発秘話(backend)

APIドキュメント管理


開発秘話(backend)

トークン検証

よくあるpemを使ったdecode処理

use jsonwebtoken::{TokenData, DecodingKey, Validation, decode};
fn decode_jwt(jwt: &str, secret: &str) ->
    Result<TokenData<Claims>, jsonwebtoken::errors::Error> {

    let secret = std::env::var(secret).expect("secret is not set");
    decode::<Claims>(
        jwt, &DecodingKey::from_secret(secret.as_ref()),
        &Validation::default())
}

が、今回必要なトークン検証はpemを使った処理ではなく、
Auth0発行のJWKSからkidに基づいて該当のJWTを探し、n e でデコードする処理


開発秘話(backend)

トークン検証

  1. ヘッダーのkid をもとに複数のJWKSの中から一致するkidを見つけ、JWTを特定
pub fn find_from_kid(jwks: Jwks, kid: &str) -> Result<JwtKey, JwksError> {
    let length = jwks.keys.len();

    let mut index: usize = 0;
    let mut is_exists: bool = false;

    for i in 0..length {
        if jwks.keys[i].kid == kid {
            is_exists = true;
            index = i;
            break;
        }
    }

// 次ページに続く

開発秘話(backend)

トークン検証

// 全ページから続く

    if is_exists == true {
        Ok(JwtKey {
            alg: jwks.keys[index].alg.to_owned(),
            kty: jwks.keys[index].kty.to_owned(),
            r#use: jwks.keys[index].r#use.to_owned(),
            n: jwks.keys[index].n.to_owned(),
            e: jwks.keys[index].e.to_owned(),
            kid: jwks.keys[index].kid.to_owned(),
            x5t: jwks.keys[index].x5t.to_owned(),
            x5c: jwks.keys[index].x5c.to_owned(),
        })
    } else {
        Err(JwksError)
    }
}

開発秘話(backend)

トークン検証

2. JWTから該当のneを使い、トークン検証

let jwt = match kid {
    Some(v) => auth0_token::find_from_kid(self.jwks.clone(), &v),
    None => panic!("something wrong with auth0 token"),
};

let val = match jsonwebtoken::decode::<Claims>(
    result,
    &DecodingKey::from_rsa_components(
        &jwt.as_ref().unwrap().n,
        &jwt.as_ref().unwrap().e
    ),
    &Validation::new(Algorithm::RS256),
) {
    Ok(v) => Some(v),
    Err(err) => match *err.kind() {
        _ => return Box::pin(ready(Err(JwtAuthError::Unauthorized.into()))),
    },
};

layout: section-2

使用技術
(Dockerfile未満)


使用技術(Dockerfile未満)

コンテナー・ネットワーク


使用技術(Dockerfile未満)

コンテナー・ネットワーク

  • AWS ECS on Fargate
    • キャパシティープロバイダー戦略を設定し、FARGATE_SPOTを最大限使用
  • AWS ECR
    • イメージスキャン
    • ライフサイクルポリシー設定
  • AWS Application Load Balancer
  • AWS Firelens
  • AWS Cloud Map
  • AWS Route53
    • DNS SEC署名有効化
  • AWS App Mesh
    • VPC内通信もTLS有効化

src: ./slides/real_resources.md


ログ設計

  • リクエストの流れを追いやすい設計にする
    • 1つのリクエストに対して、全ノードのログを一ヶ所で見たい
    • frontendのロググループを見て、次はbackendのログを見て、というログ設計はやめる
  • awsがマネージドサービス用にカスタマイズしたfluent bit
  • log_routerコンテナーをサイドカー構成でecsタスクに同梱し、任意の場所にログ送信
    • たとえば、envoyのアクセスログはs3へ、アプリケーションログはCloudwatch Logsへ、アクセスログのうち特定のクライアントからのログのみkinesis data firehose経由でAmazon OpenSearchへ等
  • 試されるfluent bit力
    • 全ログをとりあえずcloudwatch logsに出力した
    • Datadogにも出力して、可視性・一覧性を追求する

開発秘話(firelens)

設定ファイルを管理したくない

  • ブログを漁ると、タスク定義とは別にfluent bitの設定ファイルを用意する、という記事ばかりヒットする
    • s3に配置、設定ファイルをコンテナー内で読み込むようDockerfileを編集、等
    • 管理コスト。。。
  • ログ出力先が一ヶ所の場合のみ、タスク定義に記載したオプションを設定値としてfluent bitに渡せる
    • 今回の場合では、設定ファイル無しでfirelensを使える
  • タスク全体でログ出力先を一ヶ所にまとめるのではない
    • コンテナー毎に一ヶ所
    • envoyのアクセスログはdatadog、アプリケーションログはcloudwatch logs、のような振分けが可能

開発秘話(firelens)

DataAlreadyAcceptedExceptionエラー

  • log_routerコンテナー自体のログ(Cloudwatch Logs)にDataAlreadyAcceptedExceptionエラーが出力され続ける
    • The given batch of log events has already been accepted. The next batch can be sent with sequenceTokenのメッセージが、ECSタスクがリクエストを受け付ける毎に記録される
    • Cloudwatch LogsのsequenceTokenは被っていなかった
    • 原因は、log_routerコンテナーのデフォルト値と自分で設定した値の競合だった
      • aws製fluent bitコンテナーは、このような値を無条件設定する
      • fluent bit公式を参考に、タスク定義のlogConfigurationで"Match": "*" を設定した
      • Matchパラメーターが複数設定され、ログの二重送信をCloudWatch Logsが拒否した結果、DataAlreadyAcceptedExceptionエラーが発生していた
    • AWSサポートに問い合わせて、解決まで2か月かかった。。。

src: ./slides/real_resources.md


src: ./slides/virtual_resources.md


サービスメッシュ

  • AWS App Mesh採用
    • 選択肢はApp Mesh, Istio, Linkerdだった
    • Istioほどの機能は不要
    • とにかくメンテしたくない
  • envoyコンテナーをサイドカー構成でecsタスクに同梱した
  • ingressアクセスをサービスメッシュで管理できるように、仮想ゲートウェイを構築した
  • EKS移行後にLinkerdを検討予定

開発秘話(App Mesh)

マネコンで設定すると、誤ったデフォルト値が強制挿入される

  • AWSマネジメントコンソールでタスク定義を作成する際、App Mesh統合の有効化にチェックを入れると、App Meshで用いるenvoyイメージや必要な設定が自動挿入される
    • envoyコンテナーに環境変数APPMESH_VIRTUAL_NODE_NAMEが挿入される
  • 東京リージョンで自動設定されるイメージバージョンはv1.19.1.0-prodだったが、公式によると、1.15.0以上では環境変数APPMESH_RESOURCE_ARNが必要
    • バージョン1.19.1のイメージにAPPMESH_VIRTUAL_NODE_NAMEを追加すると、挙動が不安定になった
      • APPMESH_VIRTUAL_NODE_NAMEAPPMESH_RESOURCE_ARNを両方追加すると、envoyからappへの通信がconnection errorとなった
  • さらに、App Mesh統合の有効化をチェックして、APPMESH_VIRTUAL_NODE_NAMEを削除すると、エラーでタスク定義の保存に失敗する
    • App Mesh統合の有効化のチェックを外したうえで、APPMESH_RESOURCE_ARNのみが追加されるように、タスク定義のJSONを手で書くしかなかった

開発秘話(App Mesh)

朝見てみたら、仮想ゲートウェイの起動失敗タスクが500以上。。。。。

  • 原因は、appのヘルスチェックエンドポイントのステータスコードが200以外だったこと
  • 通信経路は以下
    1. Route53ホストゾーン
    2. ALB
    3. ターゲットグループ
    4. 仮想ゲートウェイのenvoyコンテナー
    5. 仮想サービス
    6. 仮想ルーター
    7. 仮想ノードのenvoyコンテナー
    8. 仮想ノードのappコンテナー
  • mesh内通信でステータスコードは書き換えられない
    ターゲットグループの受け取るステータスコードはappのもの
  • 200以外のステータスコードをターゲットグループが受け取ると、自身のヘルスチェックに失敗するため、仮想ゲートウェイにSIGTERMが送信される
  • タスクにつき1コンテナーの起動だったため、仮想ゲートウェイのenvoyコンテナーが停止してタスク数が0になる
  • ECSサービスで最低タスク数を1と設定したため、新たなタスクが立ち上がる

開発秘話(App Mesh)

朝見てみたら、仮想ゲートウェイの起動失敗タスクが500以上。。。。。


開発秘話(App Mesh)

http2対応できない

  • actix webのAPI群への通信をhttp2にしたかったので、公式にしたがってtls暗号化し、appをhttp2対応させた
  • が、upstream connect error or disconnect/reset before headers. reset reason: connection terminationというenvoyのエラーが出力される
  • awsサポート回答によると、↓とのこと
    • http2は、tls必須ではない
    • envoyへの通信とenvoy app間のプロトコルは一致させなければならない。envoyへはhttp2、envoy app間はhttp1.1というのはできない。
    • envoy app間をtls暗号化すると、app meshのコントロールプレーンが通信を補足できない
    • appはtls暗号化せずhttp2対応しなくてはならない

開発秘話(App Mesh)

http2対応できない

  • actix webの公式を見ても、tls暗号化せずにhttp2化する方法が見つからない。actix-web automatically upgrades connections to HTTP/2 if possible.と書いてはあるが、tls暗号化しないとactix webはhttp2にならなかった。
  • actix webをtls暗号化せずにhttp2対応させる術が見つからず、app meshで仮想ノード間のhttp2対応は諦めるという結論になった
    • その後、クライアントと仮想ゲートウェイ間のhttp2化には成功した

src: ./slides/virtual_resources.md

layout: default-5

ECSタスク

  • 各ECSサービスはタスクに以下のコンテナーを持つ
    • log_router
    • envoy
    • datadog agent
    • app
  • 管理するコンテナーはappだけ
    • 仮想ゲートウェイのタスクはappコンテナー無し
  • ecs execで各コンテナー内にssmできるよう設定済み

layout: section-2

使用技術
(Dockerfile未満)


サ-ビス概要

非機能要件

  • 別VPCのRDSを操作する
  • 認証・認可 OIDC
    • 製品アクセスの有無により、ユーザプールを分割する
  • マイクロサービスの開発・運用を効率化する
    • 冪等性の考慮
      • 分散トランザクション管理をなるべくやらない Sagaパターンの実装が不要なアーキテクチャを考える
    • アプリケーションでの処理をビジネスロジックに集中させる ビジネスロジック以外の処理はなるべくインフラで実装する
      • ログ振分け・サーキットブレイカー等
  • セキュリティ最重視
    • WAF
    • AWSアカウント自体の管理

使用技術(Dockerfile未満)

コンテナー・ネットワーク

  • AWS ECS on Fargate
    • キャパシティープロバイダー戦略を設定し、FARGATE_SPOTを最大限使用
  • AWS ECR
    • イメージスキャン
    • ライフサイクルポリシー設定
  • AWS Application Load Balancer
  • AWS Firelens
  • AWS Cloud Map
  • AWS Route53
    • DNS SEC署名有効化
  • AWS App Mesh
    • VPC内通信もTLS有効化

セキュリティ

  • AWS Config
  • AWS Control Tower
    • AWS Organizations
    • AWS WAF
    • AWS Shield
    • AWS Firewall Manager
  • AWS Guard Duty
  • AWS Macie
  • AWS KMSをきちんと管理
  • AWS IAMをきちんと管理
    • IAMグループに対してポリシー割当
    • パーミッションバウンダリ設定
  • AWS BudgetsをChatbotでSlackに通知

src: ./slides/real_resources.md


layout: default-3

セキュリティ対策も追加


layout: default-3

dev環境のみの構成


モニタリング(トレース)

  • Datadog agentが簡単

モニタリング(ログ)

firelensでどこにでも出せる

AWS Cloudwatch Logs
Datadog

モニタリング(メトリクス)

Datadog
AWS Container Insights
Amazon Managed Service for Prometheus
Amazon Managed Service for Grafana

Datadog頑張る


layout: section-2

今後


今後

システム面

  • frontend管理
    • Storybook
    • Cypress or Autify
  • WAF
    • Prisma Cloud
  • サービスメッシュ
    • Linkerd
  • k8s
    • AWS EKS
  • IaC
    • AWS CDK or Plumi
  • カオスエンジニアリング
    • AWS FIS or Gremlin

サービス面

  • CS連携の整備
    • サービス時間、SLAの策定
    • Intercom他、CSツール検討
  • 利用規約
  • セキュリティチェックシート
    • ホワイトペーパー作成
    • 静的サイト作成

layout: default-7

今後


layout: default-7