1.3K Views
December 07, 24
スライド概要
2024/12/7にVRChatにて開催された第39回ITインフラ集会にて使用した発表資料です。
登壇報告
- https://zenn.dev/ambr_inc/articles/87352ca4ba7436
ambrのAWS CDK事情 ITインフラ集会 20241207
⾃⼰紹介 kairox (回路) ● サーバーサイドエンジニア ○ Unity以外何でも係 ■ ○ ● クラウドインフラや⼀部Corp ITも業務範囲 gogh サーバーサイドリード SNS ○ X: qazx7412 ○ GitHub: limit7412 ○ VRChat: kairox(回路) Copyright (C) 2024 ambr, Inc. All Rights Reserved. 2
ambrについて 社名 :株式会社ambr(アンバー) 設⽴ :2018年8⽉15⽇ 住所 :東京都⽂京区⼩⽯川1-28-1 ⼩⽯川桜ビル 5F 社員数:約30名 調達額:シリーズAにおいて10.2億円 株主 :ANRI, 電通グループ, SBIインベストメント, 東急不動産, インテージホールディング他 事業 :仮想世界‧XRの開発 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 3
事業概要 メタバースアプリ開発 スタジオ事業(XR) Gogh xambr アバター‧ルームクリエイト‧作業集中 イベント向け⾃社プラットフォーム ● TGSDW(旧TGSVR) ● X-NEOKET Roblox ⾃社エクスペリエンス‧受注制作 ● ゆるバース2024 VRChat 受注制作 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 4
事業概要 メタバースアプリ開発 スタジオ事業(XR) Gogh xambr アバター‧ルームクリエイト‧作業集中 イベント向け⾃社プラットフォーム ● TGSDW(旧TGSVR) ● X-NEOKET Roblox ⾃社エクスペリエンス‧受注制作 ● ゆるバース2024 VRChat 受注制作 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 5
前提1: AWSのIaC⽤ツール Copyright (C) 2024 ambr, Inc. All Rights Reserved. 6
超ざっくりなAWSのIaCツール ● AWS専⽤ツール ○ CloudFormation(CFn) ■ ○ ○ ● AWSのIaCの(本来の)基本、yamlで各リソースを定義 AWS CDK ■ 対応した汎⽤プログラミング⾔語でインフラの定義を記述できる ■ 実⾏時「記述した⾔語 => TypeScript => CFn」に変換され実⾏される AWS SAM ■ サーバーレスに特化したデプロイツール ■ yamlで各リソースを定義するが素のCFnより抽象度が⾼く記述できる AWS対応の汎⽤ツール ○ ○ Terraform ■ HCLでインフラの定義を記述する ■ CFnに依存しない(はず) Serverless Framework ■ サーバーレスに特化したデプロイツール ■ SAMよりデプロイが楽だが有料 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 7
前提2: ambrのインフラ事情 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 8
ambrのサーバーサイド ● ● ● xambr ○ APIサーバー ○ アセット配信 ○ アカウント管理サーバー ○ リアルタイムサーバー(ゲームサーバー) ○ 統計送信⽤サブシステム ○ 内部向け管理画⾯ gogh ○ APIサーバー ○ アセット配信&UGC管理配信 ○ 内部向け管理画⾯ Corp IT ○ 勤怠管理slack連携 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 9
ambrのサーバーサイド ● ● ● xambr ○ APIサーバー => AWS CDK ○ アセット配信 => AWS CDK ○ アカウント管理サーバー=> AWS CDK ○ リアルタイムサーバー(ゲームサーバー)=> AWS CDK ○ 統計送信⽤サブシステム => AWS CDK + AWS SAM ○ 内部向け管理画⾯ => AWS CDK gogh ○ APIサーバー => AWS CDK ○ アセット配信&UGC管理配信 => AWS CDK ○ 内部向け管理画⾯ => AWS CDK Corp IT ○ 勤怠管理slack連携 => AWS SAM Copyright (C) 2024 ambr, Inc. All Rights Reserved. 10
xambrのサーバーサイドのリポジトリ単位での管理リソース ● xambr本体 ○ APIサーバー => AWS CDK ■ ○ アセット配信 => AWS CDK ■ ○ アカウント管理サーバー=> AWS CDK ■ AppRunner, DynamoDB, Cognito リアルタイム系 ○ リアルタイムサーバー(ゲームサーバー)=> AWS CDK ■ ● Lambda,(WAF, Aurora, ElastiCache) アカウント管理 ○ ● S3, CloudFront ,(WAF) 内部向け管理画⾯ => AWS CDK ■ ● Fargate, Aurora, ElastiCache, WAF ECS on EC2 統計系 ○ 統計送信⽤サブシステム => AWS CDK + AWS SAM ■ Lambda, Kinesis, Cognito Copyright (C) 2024 ambr, Inc. All Rights Reserved. 11
xambrのバックエンド構成図(ちょっと古いかも…) Copyright (C) 2024 ambr, Inc. All Rights Reserved. 12
goghのサーバーサイドでの管理リソース ● gogh ○ APIサーバー => AWS CDK ■ ○ アセット配信&UGC管理配信 => AWS CDK ■ ○ S3, CloudFront, Lambda, WAF 内部向け管理画⾯ => AWS CDK ■ ● AppRunner, WAF, Cognito, PlanetScale, Momento Lambda,(WAF, PlanetScale, Momento) xambr APIサーバーからのfork Copyright (C) 2024 ambr, Inc. All Rights Reserved. 13
goghのバックエンド構成図 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 14
リソースにフォーカスすると… ● ● API系 ○ Fargate, AppRunner, Aurora, ElastiCache, WAF ○ S3, CloudFront ○ Lambda アカウント管理 ○ ● リアルタイム系 ○ ● AppRunner, DynamoDB, Cognito ECS on EC2 統計系 ○ Lambda, Kinesis, Cognito Copyright (C) 2024 ambr, Inc. All Rights Reserved. 15
ambrのCDK構成 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 16
全サービス共通の話 ● ⾔語にはGoを採⽤ ○ ● リアルタイム系等での⼀部C#を採⽤しているがチームとしてのメイン⾔語はGo インフラのレイヤー単位でデプロイするように作成 ○ ○ 例 ■ cdk/api/ ■ cdk/database/ ■ cdk/network/ デプロイ単位でディレクトリを切りmain.goとcdk.jsonを配置 ■ ○ ● 例: cdk/api/main.go デプロイ単位とディレクトリ内のスタックの切り⽅は各サービスごとに決め共通化はしない AWS Lambdaの関数が開発する中で横に増えていくようなサービスではAWS SAM(以降SAM)を併⽤する ○ AWS SAMは関数のルーティングやバッチとしてのcron等を⾼い抽象度で記述できる ○ 要件によってはLambda Web Adapter等を使⽤して関数を⼀つにまとめらてるのでCDK管理のほうが都合が良い ■ ambrではaws-lambda-go-api-proxyを採⽤ Copyright (C) 2024 ambr, Inc. All Rights Reserved. 17
基本的なディレクトリ構成 ● 構成としては⼤体こんな感じ ○ cdk/ ■ api/ ● application/ ○ ● ■ app_runner.go main.go database/ (略) ○ ■ util/ ■ go.mod ■ cdk.json src/(api/) ■ ○ go.mod docker/ ■ application/ ● Dockerfile Copyright (C) 2024 ambr, Inc. All Rights Reserved. 18
基本的なディレクトリ構成 ● ディレクトリを切る単位 ○ ○ api/ ■ Fargate ■ AppRuner ■ Lambda database/ ■ RDS ■ S3 ● ■ Cognito ● ○ ○ CloudFront Lambda(for CustomAuth) network/ ■ WAF ■ VPC other/ ■ IAM Copyright (C) 2024 ambr, Inc. All Rights Reserved. 19
基本的なディレクトリ構成 ● ● ディレクトリを切る単位 ○ api/ ○ database/ ○ network/ ○ other/ cdk deployコマンド実⾏について ○ 各ディレクトリ直下に⼊りcdk deployコマンドを実⾏する ○ 実⾏順 network -> database -> api -> other ■ 当然依存関係は逆になるようにリソースを参照する Copyright (C) 2024 ambr, Inc. All Rights Reserved. 20
コード設計 // cdk/api/main.go func main() { defer jsii.Close() app := awscdk.NewApp(nil) stackProps := awscdk.StackProps{ Env: env(), CrossRegionReferences: jsii.Bool(true), } application.NewApplication(app, &application.ApplicationStackProps{ StackProps: stackProps, }) dashboard.NewDashboard(app, &dashboard.DashboardStackProps{ StackProps: stackProps, }) app.Synth(nil) } func env() *awscdk.Environment { return &awscdk.Environment{ Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")), Region: jsii.String(os.Getenv("CDK_DEFAULT_REGION")), } } Copyright (C) 2024 ambr, Inc. All Rights Reserved. 21
コード設計
// cdk/api/application/app_runner.go
func NewApplication(scope constructs.Construct, props *ApplicationStackProps) {
newAppRunnerStream(scope, "api-stack", props)
}
func newAppRunnerStream(
scope constructs.Construct,
id string,
props *ApplicationStackProps,
) awscdk.Stack {
var sprops awscdk.StackProps
if props != nil {
sprops = props.StackProps
}
ctx, err := util.TryGetContext(scope)
if err != nil {
panic(err)
}
parameter := environment.GetParameter(environment.Project(ctx.Project), environment.Environment(ctx.Env))
stack := awscdk.NewStack(scope, jsii.String(id+"-"+ctx.GetTarget()), &sprops)
// 各リソースを記述
return stack
}
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
22
コード設計
●
CDK CLIで指定する⾃作オプション等は共通化
○
--context project=hoge --context env=dev
func TryGetContext(scope constructs.Construct) (CDKContext, error) {
res := CDKContext{}
projectKey := "project"
projectValue := scope.Node().TryGetContext(&projectKey)
if v, ok := projectValue.(string); ok {
res.Project = v
} else {
return CDKContext{}, fmt.Errorf("project context is required")
}
envKey := "env"
envValue := scope.Node().TryGetContext(&envKey)
if v, ok := envValue.(string); ok {
res.Env = v
} else {
return CDKContext{}, fmt.Errorf("env context is required")
}
return res, nil
}
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
23
コード設計
●
スタック間の値のやり取りは変数として直接引き渡す
_, acl := newGlobalSSMStream(scope, "acl-stack", &StorageStackProps{
StackProps: awscdk.StackProps{
Env: &awscdk.Environment{
Account: jsii.String(os.Getenv("CDK_DEFAULT_ACCOUNT")),
Region: jsii.String("us-east-1"),
},
CrossRegionReferences: jsii.Bool(true),
},
})
newS3Stream(scope, "s3-stack", acl, props)
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
24
コード設計
●
ディレクトリ間の値のやり取りはSSM パラメータストアとCFnのoutputを利⽤
○
パラメータストア(SSM)
■
○
CLIコマンドを実⾏したローカル環境にキャッシュが残っている事があり扱いに注意が必要
output(CFn)
■
CFn作成(synth)時に値を解決出来ない場合があるので使⽤出来ない場合がある
awsssm.NewStringParameter(stack, jsii.String("app-api-url-ssm"), &awsssm.StringParameterProps{
ParameterName: jsii.String("/hoge/" + ctx.Env + "/URL"),
StringValue: jsii.String("https://" + *service.AttrServiceUrl()),
})
awscdk.NewCfnOutput(stack, jsii.String("capp-api-url-output"), &awscdk.CfnOutputProps{
ExportName: jsii.String("hoge-url-" + ctx.Env),
Value:
jsii.String("https://" + *service.AttrServiceUrl()),
})
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
25
コード設計 ● 参照側 userPoolID := awsssm. StringParameter_ValueFromLookup(stack, jsii.String("/hoge/"+ctx.GetTargetSlash()+"/POOL_ID")) bucket := awss3.Bucket_FromBucketName(stack, jsii.String("s3-bucket"), awscdk.Fn_ImportValue(jsii.String("s3-bucket-name-"+ctx.GetTarget())), ) ● outptだと解決出来なかったのでssmを使⽤した例(のはず…) sg := awsec2.SecurityGroup_FromLookupById(stack, jsii.String("sg-from-output"), awsssm.StringParameter_ValueFromLookup(stack, jsii.String("/hoge/"+ctx.GetTargetSlash()+"/SG_ID"), nil), ) Copyright (C) 2024 ambr, Inc. All Rights Reserved. 26
コード設計
●
Docker関連はawsecrassets.NewDockerImageAssetを使⽤してECRの存在はできるだけ意識しない作り
○
cdk deployコマンドでECRと各サーバー系リソースへの反映までできる
dockerImageAsset := awsecrassets.NewDockerImageAsset(stack,
jsii.String("uploader-ecr-repository"), &awsecrassets.DockerImageAssetProps{
Directory: jsii.String("../../"),
File:
jsii.String("docker/uploader/Dockerfile"),
Platform: awsecrassets.Platform_LINUX_AMD64(),
})
function := awslambda.NewFunction(stack, jsii.String("uploader-lambda"), &awslambda.FunctionProps{
FunctionName: jsii.String("uploader-" + ctx.GetTarget()),
Runtime:
awslambda.Runtime_FROM_IMAGE(),
Handler:
awslambda.Handler_FROM_IMAGE(),
Architecture: awslambda.Architecture_X86_64(),
Code: awslambda.AssetCode_FromEcrImage(dockerImageAsset.Repository(), &awslambda.EcrImageCodeProps{
TagOrDigest: dockerImageAsset.ImageTag(),
}),
})
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
27
リアルタイム系の話 ● 基本的にはAPI系と⼤体同じ思想で作成 ● Dockerに関してデプロイ時間短縮のため2つのデプロイ⽅法を⽤意 ○ awsecrassets.NewDockerImageAssetを使⽤ ○ CDK上では他リソースに紐づかないECRリポジトリを作成しGitHub Actions上でここpush後(ECSの)Taskを更新 ■ ● ECS on EC2で稼働する複数のサーバー群を管理するのでどうしても複雑になる ○ ● ユーザーから⾒て接続点となるMagicOnionサーバーのみを更新するための仕組み takashiii0くん当時丸投げしたのにきっちりやってくれて本当に感謝 (参考)リアルタイム系に関してのブログ記事 ○ VRメタバースのリアルタイム通信サーバーの技術にMagicOnionとNATSを選んだ話 ■ https://ambr-inc.hatenablog.com/entry/20230512/1683882000 Copyright (C) 2024 ambr, Inc. All Rights Reserved. 28
統計系の話
●
統計送信⽤サブシステム
○
●
Kinesisやそこへ接続するためのCognito、各種処理をするためのLambdaやバックアップ⽤のS3がある
○
●
前述の通りSAMを併⽤
インフラの要素数が少ないのでディレクトリを1段下げて運⽤
○
●
LambdaはKinesisからのデータを受け取るAPIと⼿動実⾏する複数のツール郡で構成
Lambdaが複数存在し、またオペレーションの変更によって横に増える公算があった
○
●
アプリから統計⽤SaaSの間に⼊りS3へバックアップを送りつつSaaSのAPIへデータを流すシステム
cdk/
■
id_pool/
■
pipeline/
■
main.go
ssm等を介してCDKとSAMを連携することが可能
○
ACCESS_KEY: !Sub "{{resolve:ssm:/log-pipeline/API_ACCESS_KEY}}"
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
29
(参考)統計系のSAMの例
Resources:
SendSaaSAPIFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "send-api-lambda"
PackageType: Zip
Runtime: provided.al2023
Architectures:
- x86_64
Handler: dummy
CodeUri: ./
MemorySize: 1024
Timeout: 10
Events:
EventMethod1:
Type: Api
Properties:
Path: "/send"
Method: post
Environment:
Variables:
TZ: "Asia/Tokyo"
ACCESS_KEY: !Sub "{{resolve:ssm:/log-pipeline/API_ACCESS_KEY}}"
Metadata:
BuildMethod: makefile
OpsFunction:
Type: AWS::Serverless::Function
Properties:
FunctionName: !Sub "ops-lambda"
PackageType: Zip
Runtime: provided.al2023
Architectures:
- x86_64
Handler: dummy
CodeUri: ./
MemorySize: 1024
Timeout: 10
Metadata:
BuildMethod: makefile
# Makefile
build-SendSaaSAPIFunction:
GOOS=linux GOARCH=amd64 go build -o \
$(ARTIFACTS_DIR)/bootstrap ./src/lambda/send_saas/main.go
build-OpsFunction:
GOOS=linux GOARCH=amd64 go build -o \
$(ARTIFACTS_DIR)/bootstrap ./src/lambda/ops/main.go
Copyright (C) 2024 ambr, Inc. All Rights Reserved.
30
CDKを使ってみての振り返り ● デプロイ作業が導⼊前に⽐べて格段に簡略化された ○ 導⼊前はCFnの⼤群があり新規に環境を作成しようとするとデプロイで1⽇死闘をする必要があった ○ ついでに別プロダクト(gogh)へのfork作業も簡単になった ■ ● ディレクトリとスタックの分割の粒度はこれで正しかったのかは議論の余地がありそう ○ ● ● ここで速度優先でほぼそのままforkした結果反省点もそのまま引き継がれてしまった感はある S3等流⽤できるコードを共通化出来た反⾯コードが複雑化してしまった気がする Goを使⽤したのは失敗だった ○ TypeScript版への追従が遅く使⽤したい関数が⽤意されていないことがあった ○ 多くの参考記事はTypeScriptを使⽤しておりGoを採⽤したものが少なくそのまま参考には出来ない Lambda等のリソースはSAMのような特化型のツールに⽐べ低レイヤーの隠蔽をしてくれない分内容に対して複雑化してしまう傾 向があった Copyright (C) 2024 ambr, Inc. All Rights Reserved. 31
32
ambrのAWS CDK事情 ITインフラ集会 20241207