theme | class | highlighter | lineNumbers | info | drawings | |
---|---|---|---|---|---|---|
shibainu |
text-center |
shiki |
true |
# ユーザ管理画面開発
|
|
激しい開発を経て、プロダクト本体はリリースされた。
しかし利用ユーザや企業の登録・更新・削除処理は、未だエンジニアの手作業で行われていた。
導入企業・ユーザ数が伸びるにつれ、その負荷も指数関数的に増大していた。
これは、トイル撲滅のため立ち上がった男達の物語である。
- サービス概要
- 開発の進め方
- 使用技術(Dockerfile以上)
- 使用技術(Dockerfile未満)
- 今後
機能要件
- ユーザ・企業・接続元IPリストのCRUDをする
- ユーザを作る権限を持った企業外ユーザ(販売代理店等)を管理する
機能要件だけ見るとチュートリアルに毛が生えた程度
--- src: ./slides/service_requirements.md --- --- 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等の接頭辞を付与
色の並びにも気を配った

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()))),
},
};
- リクエストの流れを追いやすい設計にする
- 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公式を参考に、タスク定義の
"Match": "*"
をlogConfigurationに設定した - 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コンテナーに環境変数
- 公式によると、バージョン1.15.0以上では必要な環境変数は
APPMESH_RESOURCE_ARN
に変更されていた- だがマネコンで、東京リージョンで自動設定されるイメージバージョンは
v1.19.1.0-prod
だった APPMESH_VIRTUAL_NODE_NAME
は不要なのに、マネコンが強制挿入してくる
- だがマネコンで、東京リージョンで自動設定されるイメージバージョンは
マネコンで設定すると、誤ったデフォルト環境変数が強制挿入される
- バージョン1.19.1のイメージに
APPMESH_VIRTUAL_NODE_NAME
(不要な方)を追加すると、挙動が不安定になったAPPMESH_VIRTUAL_NODE_NAME
とAPPMESH_RESOURCE_ARN
を両方追加すると、envoyからappへの通信がconnection errorとなったAPPMESH_VIRTUAL_NODE_NAME
のみを挿入する必要がある
- さらに、App Mesh統合の有効化をチェックして、
APPMESH_VIRTUAL_NODE_NAME
を削除すると、エラーでタスク定義の保存に失敗するAPPMESH_RESOURCE_ARN
のみを追加するためには、App Mesh統合の有効化のチェックを外したうえで、タスク定義のJSONを手で書くしかなかった
朝見てみたら、仮想ゲートウェイの起動失敗タスクが500以上。。。。。
- 原因は、appのヘルスチェックエンドポイントのステータスコードが200ではなかったこと
- 通信経路は以下
- Route53ホストゾーン
- ALB
- ターゲットグループ
- 仮想ゲートウェイのenvoyコンテナー
- 仮想サービス
- 仮想ルーター
- 仮想ノードのenvoyコンテナー
- 仮想ノードのappコンテナー
- mesh内通信でステータスコードは書き換えられない、つまりターゲットグループの受け取るステータスコードはappのもの
- 200以外のステータスコードをターゲットグループが受け取ると、自身のヘルスチェックに失敗するため、仮想ゲートウェイにSIGTERMが送信される
- タスク毎1コンテナーの起動のため、仮想ゲートウェイのenvoyコンテナーが停止しタスク数は0になる
- 仮想ゲートウェイの新タスク起動
朝見てみたら、仮想ゲートウェイの起動失敗タスクが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
- xray_daemon
- app
- 管理するコンテナーはappだけ
- 仮想ゲートウェイのタスクはappコンテナー無し
- ecs execで各コンテナー内にssmできるよう設定済み
- AWS Shield
- AWS WAF
- AWS NetworkFirewall
- AWS DNSFirewall
- AWS Config
- AWS Guard Duty
- AWS Macie
- AWS Security Hub
- AWS KMSをきちんと管理
- AWS IAMをきちんと管理
- IAMグループに対してポリシー割当
- パーミッションバウンダリ設定
- AWS BudgetsをChatbotでSlackに通知
- AWS ControlTower
- AWS Config
- AWS CloudTrail
- AWS Single Sign-On
- セキュリティ系サービス
- Amazon Detective
- AWS Guard Duty
- AWS Health
- AWS Macie
- AWS Security Hub
- AWS Firewall Manager
- AWS WAF
- AWS Network Firewall
- AWS DNS Firewall
- 管理系サービス
- AWS Audit Manager
- AWS Compute Optimizer
- AWS Resource Access Manager
- Amazon VPC IP Address Manager
- S3 Storage Lens
- Datadog agentが簡単
- AWS XRayもやりたい
firelensでどこにでも出せる
AWS Cloudwatch Logs
Datadog
Datadog
AWS Container Insights
Amazon Managed Service for Prometheus
Amazon Managed Service for Grafana
- 開発環境のEC2からアクセス可能にする
- Transit Gatewayを設置
- マルチAZ構成にする
- AWS Well-Architected Toolもまじめにやる
- frontend管理
- Storybook
- Cypress or Autify
- WAF
- Prisma Cloud
- サービスメッシュ
- Linkerd
- k8s
- AWS EKS
- IaC
- AWS CDK or Plumi
- カオスエンジニアリング
- AWS FIS or Gremlin