theme | class | highlighter | lineNumbers | info | drawings | |
---|---|---|---|---|---|---|
shibainu |
text-center |
prism |
true |
# ユーザ管理画面開発
|
|
激しい開発を経て、プロダクト本体はリリースされた。
しかし利用ユーザや企業の登録・更新・削除処理は、未だエンジニアの手作業で行われていた。
導入企業・ユーザ数が伸びるにつれ、その負荷も指数関数的に増大していた。
これは、トイル撲滅のため立ち上がった男達の物語である。
- サービス概要
- 開発の進め方
- 使用技術(Dockerfile以上)
- 使用技術(Dockerfile未満)
- 今後
機能要件
- ユーザ・企業・接続元IPリストのCRUDをする
- ユーザを作る権限を持った企業外ユーザ(販売代理店等)を管理する
機能要件だけ見るとチュートリアルに毛が生えた程度
---非機能要件
- 別VPCのRDSを操作する
- 認証・認可 OIDC
- 製品アクセスの有無により、ユーザプールを分割する
- マイクロサービスの開発・運用を効率化する
- 冪等性の考慮
- 分散トランザクション管理をなるべくやらない Sagaパターンの実装が不要なアーキテクチャを考える
- アプリケーションでの処理をビジネスロジックに集中させる ビジネスロジック以外の処理はなるべくインフラで実装する
- ログ振分け・サーキットブレイカー等
- 冪等性の考慮
- セキュリティ最重視
- WAF
- AWSアカウント自体の管理
Discussions Issues Pull Requestsの流れが最高
- Discussions : とりあえずの提案・バグか仕様か分からないので質問、等
- Issues : やることが決定したもの
- Pull Requests : 実装のレビュー
- テンプレートを設置し、ボタンでIssuesの複数テンプレートを使い分ける
複数リポジトリのIssuesを一覧化
- 各マイクロサービスのリポジトリを1つ1つ見に行く必要がない
- カスタムフィールドでPriorityを追加
- Priority毎にグループ分けして表示
JIRAのように扱うため、
labelsで機能補完
closed, blocked by等、
チケット間の関係性を表現
sortが奇麗になるよう、
bug, enhance等の接頭辞を付与
色の並びにも気を配った
![](/GVATECH-CTOROOM/managed_explanation/raw/main/img/github_labels.png)
frontend | |
backend | ![]() |
CI/CD | |
認証・認可 |
frontend | SSRに対応 |
backend | DDD指向・オニオンアーキテクチャで実装 リソースAPIではトークン検証処理を行う |
CI/CD | aws謹製のGithub Actionsで実装 |
認証・認可 | OIDCに則って各APIを構築 ・ Organizations機能(予定) |
- 元々の構成は
- ユーザのロースペックなPC環境を考慮してSSR化を検討。
に。
- vite.jsの構成から、next.jsの構成に移すのが大変だった
- ググってもjsの記事しか出てこない。tsの型指定が辛い。
rustの型制約が激しい
- とはいえ、型制約が激しくてビルドが通らない。。。
- MySQLのテーブルでbool値を格納するカラムがtinyint(1)で作成されていた
- が、Rust側でintを指定すると、ビルドエラー
- Rust公式によると、MySQLのtinyint(1)はRustではbool型と扱う、とのこと
- MySQL
int(11) unsigned
とRustu16
であればエラーだが、MySQLint(11) unsigned
とRustu32
はエラーにならない
- MySQL
DDD
- オニオンアーキテクチャを以下のように実装した
- ドメインモデル層: このシステムで扱うべき関心事
- ドメインサービス層: ドメインモデルのビジネスロジックを定義。アプリケーションサービス層から利用される共通ロジックを提供。
- アプリケーションサービス層: ユーザとの接点(エンドポイント等)を定義
- インフラストラクチャ層: 外部ライブラリ、DB等の接続
- 実装したモジュールをどの層に置くか
- SQL文の記述、AWS SDKはインフラストラクチャ層に。ドメインモデリングを阻害しないようにする。
- トークン検証はアプリケーションサービス層
- エンドポイント毎に検証スコープの範囲が違うため、ビジネスロジックにも思える
- アプリケーション固有の処理だが、ドメインに関する処理ではないので、アプリケーションサービス層に配置した
やっぱりrustの型制約が激しい
- 型制約が激しくてDIが辛い。。。
- goでinterface型を使うような逃げ道がない。
- 依存関係を逆転しきれないことも
- オニオンアーキテクチャの各層をモジュール化して、依存関係逆転の法則を実装する
- 頑張る
APIドキュメント管理
- 当初はOpenAPIに則ろうとした
- ドキュメントもgit管理したいし、ドキュメントとコードの連携も取りたい
- だが、Swagger Editorを使うとソースコードと設定ファイルが分離するため、いずれ整合性が取れなくなる
- 要件をまとめると
- ドキュメントのgit管理
- コードからドキュメントの生成
- ドキュメントからコードの生成
- ドキュメントの設定ファイルがソースコードから独立していない
- 独自フレームワークを持たない
- オニオンアーキテクチャに影響を与えない、受けない
- 簡便なホスティング
APIドキュメント管理
- 意外と要件に合致するものはなかった
- ホスティングはGitHub Pagesでよい
- Rust Docで、ドキュメントからコードの生成以外の要件は実現可能
- ドメインモデル層でレスポンスを定義
- アプリケーションサービス層のソースコードにパラメーターに関するコメントを記載
Rust Doc + GitHub Pages
- 以下の前提があれば、ソースコード上のコメントをAPIドキュメントとして機能させられる
- 外部公開しない
- フロントエンドのメンバーもrustを読める
APIドキュメント管理
トークン検証
よくある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
でデコードする処理
トークン検証
- ヘッダーの
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;
}
}
// 次ページに続く
トークン検証
// 全ページから続く
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)
}
}
- JWKのパラメーターは、JWTハンドブックの6,7章が詳しい
- 今回はRSA公開鍵を用いるので、その際の必須パラメーターの
n
とe
も追加した
トークン検証
2. JWTから該当のn
とe
を使い、トークン検証
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()))),
},
};
- 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有効化
- リクエストの流れを追いやすい設計にする
- 1つのリクエストに対して、全ノードのログを一ヶ所で見たい
- frontendのロググループを見て、次はbackendのログを見て、というログ設計はやめる
- awsがマネージドサービス用にカスタマイズしたfluent bit
- log_routerコンテナーをサイドカー構成でecsタスクに同梱し、任意の場所にログ送信
- たとえば、envoyのアクセスログはs3へ、アプリケーションログはCloudwatch Logsへ、アクセスログのうち特定のクライアントからのログのみkinesis data firehose経由でAmazon OpenSearchへ等
- 試されるfluent bit力
- 全ログをとりあえずcloudwatch logsに出力した
- Datadogにも出力して、可視性・一覧性を追求する
設定ファイルを管理したくない
- ブログを漁ると、タスク定義とは別にfluent bitの設定ファイルを用意する、という記事ばかりヒットする
- s3に配置、設定ファイルをコンテナー内で読み込むようDockerfileを編集、等
- 管理コスト。。。
- ログ出力先が一ヶ所の場合のみ、タスク定義に記載したオプションを設定値としてfluent bitに渡せる
- 今回の場合では、設定ファイル無しでfirelensを使える
- タスク全体でログ出力先を一ヶ所にまとめるのではない
- コンテナー毎に一ヶ所
- envoyのアクセスログはdatadog、アプリケーションログはcloudwatch logs、のような振分けが可能
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か月かかった。。。
- AWS App Mesh採用
- 選択肢はApp Mesh, Istio, Linkerdだった
- Istioほどの機能は不要
- とにかくメンテしたくない
- envoyコンテナーをサイドカー構成でecsタスクに同梱した
- ingressアクセスをサービスメッシュで管理できるように、仮想ゲートウェイを構築した
- EKS移行後にLinkerdを検討予定
- k8sが前提のツールなため
- envoyじゃないメリデメを考える
マネコンで設定すると、誤ったデフォルト値が強制挿入される
- AWSマネジメントコンソールでタスク定義を作成する際、App Mesh統合の有効化にチェックを入れると、App Meshで用いるenvoyイメージや必要な設定が自動挿入される
- envoyコンテナーに環境変数
APPMESH_VIRTUAL_NODE_NAME
が挿入される
- envoyコンテナーに環境変数
- 東京リージョンで自動設定されるイメージバージョンは
v1.19.1.0-prod
だったが、公式によると、1.15.0以上では環境変数APPMESH_RESOURCE_ARN
が必要- バージョン1.19.1のイメージに
APPMESH_VIRTUAL_NODE_NAME
を追加すると、挙動が不安定になったAPPMESH_VIRTUAL_NODE_NAME
とAPPMESH_RESOURCE_ARN
を両方追加すると、envoyからappへの通信がconnection errorとなった
- バージョン1.19.1のイメージに
- さらに、App Mesh統合の有効化をチェックして、
APPMESH_VIRTUAL_NODE_NAME
を削除すると、エラーでタスク定義の保存に失敗する- App Mesh統合の有効化のチェックを外したうえで、
APPMESH_RESOURCE_ARN
のみが追加されるように、タスク定義のJSONを手で書くしかなかった
- App Mesh統合の有効化のチェックを外したうえで、
朝見てみたら、仮想ゲートウェイの起動失敗タスクが500以上。。。。。
- 原因は、appのヘルスチェックエンドポイントのステータスコードが200以外だったこと
- 通信経路は以下
- Route53ホストゾーン
- ALB
- ターゲットグループ
- 仮想ゲートウェイのenvoyコンテナー
- 仮想サービス
- 仮想ルーター
- 仮想ノードのenvoyコンテナー
- 仮想ノードのappコンテナー
- mesh内通信でステータスコードは書き換えられない
ターゲットグループの受け取るステータスコードはappのもの
- 200以外のステータスコードをターゲットグループが受け取ると、自身のヘルスチェックに失敗するため、仮想ゲートウェイにSIGTERMが送信される
- タスクにつき1コンテナーの起動だったため、仮想ゲートウェイのenvoyコンテナーが停止してタスク数が0になる
- ECSサービスで最低タスク数を1と設定したため、新たなタスクが立ち上がる
朝見てみたら、仮想ゲートウェイの起動失敗タスクが500以上。。。。。
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対応しなくてはならない
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化には成功した
- 各ECSサービスはタスクに以下のコンテナーを持つ
- log_router
- envoy
- datadog agent
- app
- 管理するコンテナーはappだけ
- 仮想ゲートウェイのタスクはappコンテナー無し
- ecs execで各コンテナー内にssmできるよう設定済み
非機能要件
- 別VPCのRDSを操作する
- 認証・認可 OIDC
- 製品アクセスの有無により、ユーザプールを分割する
- マイクロサービスの開発・運用を効率化する
- 冪等性の考慮
- 分散トランザクション管理をなるべくやらない Sagaパターンの実装が不要なアーキテクチャを考える
- アプリケーションでの処理をビジネスロジックに集中させる ビジネスロジック以外の処理はなるべくインフラで実装する
- ログ振分け・サーキットブレイカー等
- 冪等性の考慮
- セキュリティ最重視
- WAF
- AWSアカウント自体の管理
- 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に通知
- Datadog agentが簡単
- AWS XRayもやりたい
firelensでどこにでも出せる
AWS Cloudwatch Logs
Datadog
Datadog
AWS Container Insights
Amazon Managed Service for Prometheus
Amazon Managed Service for Grafana
- frontend管理
- Storybook
- Cypress or Autify
- WAF
- Prisma Cloud
- サービスメッシュ
- Linkerd
- k8s
- AWS EKS
- IaC
- AWS CDK or Plumi
- カオスエンジニアリング
- AWS FIS or Gremlin