TECH PLAY

KINTOテクノロジーズ

KINTOテクノロジーズ の技術ブログ

969

AWSサーバレスアーキテクチャをMonorepoツール - Nxとterraformで構築してみた! こんにちは。DevOpsで開発者を幸せにしたい。KINTO テクノロジーズのCCoEチーム所属の栗原です。 AWS Summit Tokyo 2023:クルマのサブスク「KINTO」のアジリティとガバナンスを両立する DBRE の取り組み でも発表しましたが、弊社DBREチームではSlackからのリクエストをトリガーに、一時的な踏み台サーバーを払い出すプラットフォーム(以降DBREプラットフォーム)を全社に展開しています。 DBREプラットフォームはAWSのサーバレスサービスを組み合わせて実装されています。同サーバレス部分をチームの使い慣れたterraformでIaCしつつ、 Nx というMonorepoツールで開発者体験の向上に成功した話をこの記事で紹介します。サーバレスに限らず、Monorepoでの開発フローに興味がある方にも参考になるかと思います。 背景、課題 DBREプラットフォームのアーキテクチャは以下のようになっています。 上記に加え運用関連も含めるとGolangのLambdaが20個ほどあり、それらをオーケストレーションするStep Functions、DynamoDB、スケジュールトリガーをするEventBridge等の組み合わせで構築しています。 開発を進めるにあたり以下のような課題、要望がありました。 terraformのplan、applyのワークフローと統合し安全なデプロイを実現したい Formatter、Linter等の静的コード解析を適切に組み込みたい サーバレスアーキテクチャを開発するのであれば、 SAM や serverless framework が検討されるかと思いますが、terraformでIaCしたいという要望をクリアできませんし、GolangでのLambdaはケアしてくれない等の理由から別の開発アーキテクチャを検討することになりました。 terraformのlambdaモジュール を眺めてみます。とかくterraformに参照させるLambdaコードの Zip を事前に作れていれば、terrafromでIaCしたい問題はクリアできるだろうと考えました。 resource "aws_lambda_function" "test_lambda" { # If the file is not in the current working directory you will need to include a # path.module in the filename. filename = "lambda_function_payload.zip" function_name = "lambda_function_name" role = aws_iam_role.iam_for_lambda.arn handler = "index.test" source_code_hash = data.archive_file.lambda.output_base64sha256 runtime = "nodejs16.x" environment { variables = { foo = "bar" } } } 更に後者の静的コード解析を適切に組み込みたい要望を考えてみます。サーバレス開発は言うならば 小さいコードベースを組み合わせたもの です。つまりコードベース群の境界をはっきりさせることで開発ツールとの統合を容易にし、ビルドスクリプトもシンプルに保てるのではという考えからMonorepoツールの導入を検討しました。 Monorepoツールとは 結論から言うと、 Nx というTypeScript製のMonorepoツールを導入しました。 導入理由は monorepo.tools というMonorepoツール比較ページでの機能の網羅性が高かったのと、将来の横展開を考えた際Javascript製というのも魅力を感じたためです。(フロントエンド界隈への導入障壁が低いという仮定。) 実例は次の章で説明しますが、前提としてMonorepoとはなんなのか?またNxは何をしてくれるのか?を簡単に説明します。 用語定義 Nxの用語に合わせ、本資料で使う用語を定義します。 プロジェクト : monorepo内の1つのリポジトリのような塊(例: 単一のLambdaコード、共通モジュール) タスク : テスト、ビルド、デプロイなどアプリケーションを構築するうえで必要な処理の総称 Monorepoとは 関連するプロジェクトが isolated(独立性を保ちつつ) でかつ well-defined relationships(依存関係は適切に定義されている) な状態で格納されている単一リポジトリと説明されています。対してWeb界隈では良くみられるマルチリポジトリの構成を polyrepo と呼ぶそうです。 出典: monorepo.tools monorepo.tools を要約すると、以下の様なメリットがあります。 コミットがシステム単位でアトミックになる 共通モジュールの展開が容易(共通モジュールを更新した際、取り込みなど不要ですぐに利用可能) (マインドセット的に)縦割りにならず、システム全体に意識が向きやすい 新しいリポジトリを立ち上げる時の工数が減る Monorepoツールではないですが、AWSの cdk でもIaCとアプリケーションコードの単一リポジトリ管理を推奨していることもあり、Monorepo化への潮流は一つの流れだと考えられます。 We discovered that failures are often related to "out-of-band" changes to an application that aren't fully tested, such as configuration changes. Therefore, we developed the AWS CDK around a model in which your entire application is defined in code, not only business logic but also infrastructure and configuration. …and fully rolled back if something goes wrong. - https://docs.aws.amazon.com/cdk/v2/guide/best-practices.html Nxがしてくれること 大雑把にいうと、 プロジェクト毎 に 自身のタスク と 依存関係 を定義していけば、Nxが いい具合にタスクをオーケスレーションしてくれる ということになります。 以下はterraformプロジェクトのタスク、依存関係の定義の例です。このように定義すると、 plan-development タスクを実行すると、定義した依存関係のLambdaコードのビルド(コンパイル、zip化)が先に実行され、その後terraformのplanを走らせてくれます。 fmt や test などもterraform専用のタスクとしてシンプルに定義することができます。 このように各コードベースの責務を明確化していくことで、コード全体の見通しをよくしていくことができます。 プロジェクト単位で、それぞれの開発言語にあった開発ツールを組み込むことが可能ですし、ビルド職人に頼らなくとも適切な開発フローを構築していくことが可能になります。 KINTO テクノロジーズでの実用例 前述のDBREプラットフォームの一部を抜粋し、簡素化したうえで実用例を説明していきます。 GolangのLambdaコードが二つあり、双方同じ共通モジュールを利用しています。terraformからデプロイできるように、Lambdaコードのプロジェクトは自身のコードをコンパイルし、Zipファイルを作成するまでが責務となります。 ディレクトリ構成はこのようになります。 プロジェクト定義 上記4プロジェクトの各プロジェクト定義をそれぞれ記載します。 ①: 共通モジュール Golangでは共通モジュールは利用側で参照できれば良いので、ビルドは不要で静的解析、UTのみをタスクとして定義します projects/dbre-toolkit/lambda-code/shared-modules/package.json { "name": "shared-modules", "scripts": { "fmt-fix": "gofmt -w -d", "fmt": "gofmt -d .", "test": "go test -v" } } ②、③: Lambdaコード 共通モジュールを依存プロジェクトとして登録することで、 共通モジュールのコードが変更されるた場合、タスクの実行が必要 といった定義になります build タスクはgo buildを実行して生成されたバイナリーをZip化するまでが責務になり、後にterraformプロジェクトで利用されます projects//dbre-toolkit/lambda-code/lambda-code-01/package.json { "name": "lambda-code-01", "scripts": { "fmt-fix": "gofmt -w -d .", "fmt": "gofmt -d .", "test": "go test -v", "build": "cd ../ && GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o lambda-code-01/dist/main lambda-code-01/main.go && cd lambda-code-01/dist && zip lambda-code.zip main" }, "nx": { "implicitDependencies": [ "shared-modules" ] } } ④: IaC plan-${env} 、 apply-${env} を実行すると、依存関係に指定されているLambdaコードの build が先に実行されます(plan、apply実行時には必要なzipが生成されている状態になる) projects//dbre-toolkit/iac/package.json { "name": "iac", "scripts": { "fmt": "terraform fmt -check -diff -recursive $INIT_CWD", "fmt-fix": "terraform fmt -recursive -recursive $INIT_CWD", "test": "terraform validate", "plan-development": "cd development && terraform init && terraform plan", "apply-development": "cd development && terraform init && terraform apply -auto-approve" }, "nx": { "implicitDependencies": [ "lambda-code-01", "lambda-code-02" ], "targets": { "plan-development": { "dependsOn": [ "^build" ] }, "apply-development": { "dependsOn": [ "^build" ] } } } } terraformのモジュールからは、以下の様に前段で生成されているZipファイルを参照させます。 local { lambda_code_01_zip_path = "${path.module}/../../../lambda-code/lambda-code-01/dist/lambda-code.zip" } # 中略 resource "aws_lambda_function" "lambda-code-01" { function_name = "lambda-code-01" architectures = ["x86_64"] runtime = "go1.x" package_type = "Zip" filename = local.lambda_code_01_zip_path handler = "main" source_code_hash = filebase64sha256(local.lambda_code_01_zip_path) } タスク実行 各プロジェクトの分割、タスク定義ができたので、タスクの実行について見ていきます。 Nxでは run-many サブコマンドで特定プロジェクトの特定タスク、全プロジェクトの特定タスクの実行が可能です。依存関係に基づき、可能な場合はパラレルに実行されるので速度も高速です。 nx run-many --target=<定義したタスク名> --projects=<プロジェクト名のカンマ区切り> nx run-many --target=<定義したタスク名> --all iac プロジェクトの plan-development を実行する例。依存関係があるタスクは、定義した依存関係に基づいてタスクを実行してくれる まさにやりたかったポイントです。依存プロジェクトのタスクを先行して実行してくれるため、terraform実行時には適切にLambdaのコードがZip化されている状態を担保することができます。 $ nx run-many --target=plan-development --projects=iac --verbose > NX Running target plan-development for 1 project(s) and 2 task(s) they depend on: - iac —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > nx run lambda-code-01:build updating: main (deflated 56%) > nx run lambda-code-02:build updating: main (deflated 57%) > nx run iac:plan-development Initializing modules... Initializing the backend... Initializing provider plugins... - Reusing previous version of hashicorp/aws from the dependency lock file - Using previously-installed hashicorp/aws v4.39.0 terraform has been successfully initialized! 〜〜中略 } Plan: 0 to add, 2 to change, 0 to destroy. 全プロジェクトの test タスクを実行する例。タスクに依存関係がないので全てパラレルに実行される UTなど依存関係がないタスクはパラレルに実行できます。 CIでの実行はもちろん、『GitHubにpushする前に必ずUTを実行する』といった開発ルールがあった場合も一つのコマンドで解決することができます。 $ nx run-many --target=test --all --verbose > NX Running target test for 4 project(s): - lambda-code-01 - lambda-code-02 - shared-modules - iac —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > nx run shared-modules:test ? github.com/kinto-dev/dbre-platform/dbre-toolkit/shared-modules [no test files] > nx run lambda-code-01:test === RUN Test01 --- PASS: Test01 (0.00s) PASS ok github.com/kinto-dev/dbre-platform/dbre-toolkit/lambda-code-01 0.255s > nx run iac:test Success! The configuration is valid. > nx run lambda-code-02:test === RUN Test01 --- PASS: Test01 (0.00s) PASS ok github.com/kinto-dev/dbre-platform/dbre-toolkit/lambda-code-02 0.443s —————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > NX Successfully ran target test for 4 projects > nx run lambda-code-02:test Nx、Monorepoツールの強力な機能 プロジェクトを適切に定義していくとことで、タスクをオーケストレーションできる様子が伝わったかと思います。 ただし、これだけでは普通のタスクランナーと変わらないので、Nx、Monorepoツールを使う大きなメリットをいくつか紹介します。 変更プロジェクトのみタスク実行 最も早いタスク実行とは そもそもタスクを実行しないこと です。 affected コマンドという変更プロジェクトのみのタスクを実行してくれる機構が用意されており、CIを高速に完了することが可能です。 以下がコマンドのシンタックスです。2つのGitポインターを渡してあげることで、同ポインター間で変更があったプロジェクトのタスクのみ実行してくれます。 nx affected --target=<タスク名> --base=<two dots diffのbase> --head=<two dots diffのhead> # lambda-code-01にだけ変更がある状態 $ git diff main..feature/111 --name-only projects/dbre-toolkit/lambda-code/lambda-code-01/main.go $ nx affected --target=build --base=main --head=feature/111 --verbose > NX Running target build for 1 project(s): - lambda-code-01 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > nx run lambda-code-01:build updating: main (deflated 57%) ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > NX Successfully ran target build for 1 projects 依存先のプロジェクトに変更があれば、依存関係に基づきタスクを実行してくれます。 # shared-moduleにだけ変更がある状態 $ git diff main..feature/222 --name-only projects/dbre-toolkit/lambda-code/shared-modules/utility.go # shared-moduleに依存しているプロジェクトのタスクが実行される $ nx affected --target=build --base=main --head=feature/222 --verbose > NX Running target build for 2 project(s): - lambda-code-01 - lambda-code-02 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > nx run lambda-code-01:build updating: main (deflated 56%) > nx run lambda-code-02:build updating: main (deflated 57%) ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— > NX Successfully ran target build for 2 projects CI/CDパイプラインの簡素化 タスク名が変わらなければ、プロジェクトを追加していってもCI/CDパイプラインの変更が不要なため、メンテナンスコストが下がります。 加えて、前述の affected コマンドにより、CI/CDの処理を高速化することができます(変更したプロジェクトのタスクのみ実行してくれるため)。 以下はGitHub ActionsのCIの例です。 name: Continuous Integration on: pull_request: branches: - main - develop types: [opened, reopened, synchronize] jobs: ci: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 with: fetch-depth: 0 # --immutable オプションは、yarn.lockに記載の固定バージョンのdependenciesをインストールさせるオプション - name: install npm dependencies run: yarn install --immutable shell: bash - uses: actions/setup-go@v3 with: go-version: '^1.13.1' - uses: hashicorp/setup-terraform@v2 with: terraform_version: 1.3.5 - name: configure AWS credentials uses: aws-actions/configure-aws-credentials@v1-node16 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} aws-region: 'ap-northeast-1' # タスク実行箇所はこの記述量で完了する - name: format check run: nx affected --verbose --target fmt --base=remotes/origin/${{ github.base_ref }} --head=remotes/origin/${{ github.head_ref }} - name: test run: nx affected --verbose --target test --base=remotes/origin/${{ github.base_ref }} --head=remotes/origin/${{ github.head_ref }} - name: build run: nx affected --verbose --target build --base=remotes/origin/${{ github.base_ref }} --head=remotes/origin/${{ github.head_ref }} - name: terraform plan to development run: nx affected --verbose --target plan-development --base=remotes/origin/${{ github.base_ref }} --head=remotes/origin/${{ github.head_ref }} Git Hookと組み合わせてさらに生産性の向上 『Gitでpushする前に静的解析とUnit Testくらいはローカルでやってほしい。Gitの履歴も汚れるし』といった開発ルールも簡単に解決することができます。 affected コマンドの --files 、 --uncommitted オプションと、Git Hookを組み合わせることで、変更ファイルが属するプロジェクトのみを対象にでき、開発者のストレスを最小限に(実行に時間をかけずに)実現が可能です。 例えば以下の様な affected コマンドを pre-commit フックに仕込んでおけば、コミット履歴も汚れず、レビューのノイズも軽減することができます。 nx affected --target lint --files $(git diff --cached --name-only) : nx affected --target unit-test --files $(git diff --cached --name-only) nx affected --target fmt-fix --files $(git diff --cached --name-only) その他のメリット タスクの実行結果はプロジェクトのコードに変更がなければキャッシュされる タスクの実行結果は、生成されたファイル、標準出力/エラーともキャッシュされます。(詳細は こちら ) $ tree .nx-cache/ ├── ce36b7825abacc0613a8b2c606c65db6def0e5ca9c158d5c2389d0098bf646a1 │   ├── code │   ├── outputs │   │   └── projects │   │   └── dbre-toolkit │   │   └── lambda-code │   │   └── lambda-code-01 │   │   └── dist │   │   ├── lambda-code.zip │   │   └── main │   └── terminalOutput ├── ce36b7825abacc0613a8b2c606c65db6def0e5ca9c158d5c2389d0098bf646a1.commit ├── nxdeps.json ├── run.json └── terminalOutputs ├── 1c9b46c773287538b1590619bfa5c9abf0ff558060917a184ea7291c6f1b988c ├── 6f2fbb5f2dd138ec5e7e261995be0d7cddd78e7a81da2df9a9fe97ee3c8411c5 ├── 88c7015641fa6e52e0d220f0fdf83a31ece942b698c68c4455fa5dac0a6fd168 ├── 9dc8ebe6cdd70d8b5d1b583fbc6b659131cda53ae2025f85037a3ca0476d35b8 ├── c4267c4148dc583682e4907a7692c2beb310ebd2bf9f722293090992f7e0e793 ├── ce36b7825abacc0613a8b2c606c65db6def0e5ca9c158d5c2389d0098bf646a1 ├── db7e612621795ef228c40df56401ddca2eda1db3d53348e25fe9d3fe90e3e9a1 ├── dc112e352c958115cb37eb86a4b8b9400b64606b05278fe7e823bc20e82b4610 └── eb94fd3a7329ab28692a2ae54a868dccae1b4730e4c15858e9deb0e2232b02f3 これをCI/CDでもキャッシュしておけば、例えばコードレビューで一部分だけ修正が必要になった場合、修正後のPushに対するCI処理のタスクの大部分はキャッシュが使われるため、更に高速に開発を回すことが可能です。 - name: set nx cache dir to environment variables id: set-nx-version run: | echo "NX_CACHE_DIRECTORY=$(pwd)/.nx-cache" >> $GITHUB_ENV shell: bash # nxのキャッシュをGitHubのキャッシュに登録 - name: nx cache action uses: actions/cache@v3 id: nx-cache with: path: ${{ env.NX_CACHE_DIRECTORY }} key: nx-cache-${{ runner.os }}-${{ github.sha }} restore-keys: | nx-cache-${{ runner.os }}- graph コマンドにより、プロジェクトの依存関係をビジュアライズできる コードベースの境界が明確になったとはいえ、依存関係を網羅的に確認したい時はあります。 graph サブコマンドが整備されており、プロジェクト間の依存関係を可視化することができます。そのあたりもケアしてくれている点もNxの魅力の一つです。 DBREプラットーフォームの現状 DBREプラットフォームでは現状28プロジェクトを有するMonorepoとなっています。前述の例だとプロジェクト数が少なかったため、メリットがわかりづらかったかもしれませんが、これくらいの規模になってくると affected コマンド等の恩恵を十二分に享受できています。 $ yarn workspaces list --json {"location":".","name":"dbre-platform"} {"location":"dbre-utils","name":"dbre-utils"} {"location":"projects/DBREInit/iac","name":"dbre-init-iac"} {"location":"projects/DBREInit/lambda-code/common","name":"dbre-init-lambda-code-common"} {"location":"projects/DBREInit/lambda-code/common-v2","name":"dbre-init-lambda-code-common-v2"} {"location":"projects/DBREInit/lambda-code/push-output","name":"dbre-init-lambda-code-push-output"} {"location":"projects/DBREInit/lambda-code/s3-put","name":"dbre-init-lambda-code-s3-put"} {"location":"projects/DBREInit/lambda-code/sf-check","name":"dbre-init-lambda-code-sf-check"} {"location":"projects/DBREInit/lambda-code/sf-collect","name":"dbre-init-lambda-code-sf-collect"} {"location":"projects/DBREInit/lambda-code/sf-notify","name":"dbre-init-lambda-code-sf-notify"} {"location":"projects/DBREInit/lambda-code/sf-setup","name":"dbre-init-lambda-code-sf-setup"} {"location":"projects/DBREInit/lambda-code/sf-terminate","name":"dbre-init-lambda-code-sf-terminate"} {"location":"projects/PowerPole/iac","name":"powerpole-iac"} {"location":"projects/PowerPole/lambda-code/pp","name":"powerpole-lambda-code-pp"} {"location":"projects/PowerPole/lambda-code/pp-approve","name":"powerpole-lambda-code-pp-approve"} {"location":"projects/PowerPole/lambda-code/pp-request","name":"powerpole-lambda-code-pp-request"} {"location":"projects/PowerPole/lambda-code/sf-deploy","name":"powerpole-lambda-code-sf-deploy"} {"location":"projects/PowerPole/lambda-code/sf-notify","name":"powerpole-lambda-code-sf-notify"} {"location":"projects/PowerPole/lambda-code/sf-setup","name":"powerpole-lambda-code-sf-setup"} {"location":"projects/PowerPole/lambda-code/sf-terminate","name":"powerpole-lambda-code-sf-terminate"} {"location":"projects/PowerPoleChecker/iac","name":"powerpolechecker-iac"} {"location":"projects/PowerPoleChecker/lambda-code/left-instances","name":"powerpolechecker-lambda-code-left-instances"} {"location":"projects/PowerPoleChecker/lambda-code/sli-notifier","name":"powerpolechecker-lambda-code-sli-notifier"} {"location":"projects/dbre-toolkit/docker-image/shenron-wrapper","name":"dbre-toolkit-docker-image-shenron-wrapper"} {"location":"projects/dbre-toolkit/iac","name":"dbre-toolkit-iac"} {"location":"projects/dbre-toolkit/lambda-code/dt-list-dbcluster","name":"dbre-toolkit-lambda-code-dt-list-dbcluster"} {"location":"projects/dbre-toolkit/lambda-code/dt-make-markdown","name":"dbre-toolkit-lambda-code-dt-make-markdown"} {"location":"projects/dbre-toolkit/lambda-code/utility","name":"dbre-toolkit-lambda-code-utility"} terraformでのIaCもコンポーネント単位で4つのプロジェクトに分割しています。このように気軽にプロジェクトを分割できることで、単一リポジトリでも各コードベースのサイズをスリムに保つことが可能です。 affected コマンドによりCI/CDも高速に完了するため、開発体験を下げることなく生産性を上げることができています。 $ yarn list-projects | grep iac {"location":"projects/DBREInit/iac","name":"dbre-init-iac"} {"location":"projects/PowerPole/iac","name":"powerpole-iac"} {"location":"projects/PowerPoleChecker/iac","name":"powerpolechecker-iac"} {"location":"projects/dbre-toolkit/iac","name":"dbre-toolkit-iac"} 課題 この開発アーキテクチャが完成するまでに課題になった点と、どう解決したかも紹介させていただきます。 導入で触れた様にLambdaコードのZip化が重要ポイントだったのですが、実行環境やZipのメタデータ(更新日時等)を完全に同じにしないと、コードに変更がなくてもterraformで差分が発生してしまうという課題がありました。解決策として、コードのビルド、Zip化はコンテナ内で実行し、タスク定義からはそれを呼ぶことで解決しました。 Dockerfile FROM golang:1.20-alpine RUN apk update && \ apk fetch zip && \ apk --no-cache add --allow-untrusted zip-3.0-r*.apk bash COPY ./docker-files/go-single-module-build.sh /opt/app/go-single-module-build.sh ./docker-files/go-single-module-build.sh #!/bin/bash set -eu -o pipefail while getopts "d:m:b:h" OPT; do case $OPT in d) SOURCE_ROOT_RELATIVE_PATH="$OPTARG" ;; m) MAIN_GO="$OPTARG" ;; b) BINARY_NAME="$OPTARG" ;; h) help ;; *) exit ;; esac done shift $((OPTIND - 1)) cd "/opt/mounted/$SOURCE_ROOT_RELATIVE_PATH" || exit 1 rm -f ./dist/* CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o "./dist/$BINARY_NAME" "$MAIN_GO" cd ./dist || exit 1 # for sha256 diff chown "$HOST_USER_ID":"$HOST_GROUP_ID" "$BINARY_NAME" touch --no-create -t 01010000 "$BINARY_NAME" ./*.tmpl zip "$BINARY_NAME.zip" "$BINARY_NAME" ./*.tmpl chown -R "$HOST_USER_ID":"$HOST_GROUP_ID" ../dist 他にもローカル実行が現状できていないなどの課題もあります。今後terraformに限らず、 SAM や cdk のMonorepo化もチャレンジしていきたいと思っています。 まとめ 本記事ではMonorepoツールでAWSのサーバレスの管理方法の紹介をベースに、Nxの強力な機能について紹介させていただきました。コードベースの境界が明確になったことで、プロジェクト毎に適切な開発ツールとの統合、terraformでのIaCを実現することができました。僕も、私もMonorepoしたい!と思っていただけた方は是非 プラットフォームG で一緒に働きませんか?ご拝読ありがとうございました。
アバター
はじめに こんにちは。プラットフォームGのOperationToolManagerチームでPlatformEngineeringとかツール周りの開発・運用の役割の島村です。 同じくプラットフォームGのOperationToolManagerチームで内製ツールの開発を行っている山田です。 KINTOテクノロジーズではAmazon ECS+Fargateをアプリケーション実行基盤として使用しています。また、CICDについてはGitHubActionsを使用しています。 AWSのECSにおけるBlueGreenDeploymentの仕組みは、DeploymentControllerとして「CODE_DEPLOY」が主として使用されており、「EXTERNAL(サードパーティーでの制御)」を使用している実例は少ないと思います。 CI/CD Conference 2023 by CloudNative Daysでも、BlueGreenDeploymentをするために、ECSからKuberenetesへ移動された事例もお伺いしました。 小さく始める Blue/Green Deployment とはいえ、ECSでもCodeDeployの条件に制限されないBlueGreenDeploymentができるのでは?ということと、アプリケーション開発部門にはデプロイ方式を複数提供するべきと考え、準備を開始しました。 やはり通常はCODE_DEPLOYを設定するほうが多く、EXTERNALの設定のドキュメントなども少ない状態でしたが、アプリケーション側への仕組みの提供を行うことができました。 外部のパイプラインツール+ECS(Fargate)を対象としたBlueGreenDeploymentの実装事例としてご紹介いたします。 背景 課題 ECSのローリングアップデートだけだと今後のリリースの際に要件とマッチしない可能性がある 複数のデプロイ手法を選べるようにして、アプリケーションの特性に合ったデプロイを行うべきである 解決方法 ということで、まずは第一歩として、ECSでのBlueGreenDeploymentを提供することにしました。カナリアリリースなどは今後の課題としますが、最終的にはこの形で実装できたので、流入量などをCLIで設定する形で実装できると想定しています。 設計 CODE_DEPLOYでの確認 「ECS BlueGreenDeployment」で調べれば、色々と出てきます。 が、それだけで済ますのもよろしくはないかなというので、概要などをまとめたいと思います。 このような構成です。CodeDeployに各種の設定をして、新しくTask定義に紐づいたTaskを作成、Deployment設定に則って流入を変更させていきます。一括で切り替えたり、一部だけを確認して徐々に増やすなどです。 満たせないなと思った仕様 CodeDeployでの環境と動作を確認したところ、こういった点が気になりました。設定次第かもですので、ご存じなら突っ込んでいただけると。 テスト系をある程度期間立ち上げて動作確認したい(カスタマーの確認など) 1日程度は維持できるが、その設定を経過して切り替えボタンを押さない場合デプロイに失敗する 切り替え後に任意のタイミングで古いアプリケーションを落としたい CodeDeployだと、時間制限は設定できるが任意ではなさそう 切り戻しをConsoleからだと煩雑になりそう 権限設計で、SwitchRoleをしないとConsoleから触れないので、操作がややこしくなる EXTERNALでの全体構成 コンポーネント(要素) 名称 概要 Terraform AWSなど色々なサービスをコード化する製品。IaC。社内のデザインパターンとModuleはTerraformで作成されています。 GitHubActions GitHubに包含されているCICDツール。KINTOテクノロジーズではGitHubActionsを使用してアプリケーションのビルド・リリースなどを実行しています。このGitHubActionsからパイプラインを使用して新旧アプリケーションのデプロイや切り替えを行っています。 ECS(ElasticContainerService) アプリケーション実行環境としてECSを使っています。設定上、DeploymentControllerはECS/CODE_DEPLOY/EXTERNALの設定ができますが、本件はEXTERNALでの実装例です。 DeploymentController ECSのコントロールプレーンのようなもの(と個人的には思っている)。 TaskSet ECSのサービスに紐づくTaskのまとまり。CLIからは作成できますがConsoleから作成できないようです。これを使うことで、1つのサービスに複数のタスク定義バージョンを並行で作成できます。 CLIリファレンス 。作成の際にALBやTargetGroupなどが必要で、設定する項目が多い ALB ListenerRule ALB上でTargetGroupに振り分けるルール。BlueGreenDeploymentでは、この紐づけを変更することで新旧アプリケーションの導線を切り替えます。 制限事項 ECSのDeploymentControllerは作成時のみ設定できるので、既存Serviceの変更は不可 EXTERNALの場合、プラットフォームバージョンはServiceで固定されない(TaskSetの際に指定) サービスの起動タイプがEC2に固定される。ただ、TaskSetを作成する際にFargateを指定すればタスクはFargateで起動する 実装 Terraform KINTOテクノロジーズではIaCとしてTerraformを使用しています。Module化もしていますが、そのModuleを修正した際に出た注意点などを整理します。 ListnerRule GitHubActionsでListnerRuleを書き換えてTargetGroupを変更するので、ignore_changeを設定。 ECS Service NetworkConfiguration LoadBalancer ServiceRegisteries の3つはEXTERNALの場合は設定できないため、Dynamicなどで設定をしている場合は作成しないようにする必要があります。その場合、CloudMapに登録されないので、AppMeshなどで組み合わせる場合は、考慮が必要です。BlueGreenDeploymentを設定したServiceからほかのECS ServiceへAppMeshを使った通信は問題ありません。 そもそも、BlueGreenで並行稼働していることから、CloudMapに登録されて通信可能とすると…間違えたアクセスが発生すると思うので、挙動的には正しいかなと思います。 CICD用RoleのIAMPolicy 色々とECS系以外にも権限が必要となります。サンプルとしては以下の通りです。 resource "aws_iam_policy" "cicd-bg-policy" { name = "cicd-bg_policy" path = "/" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Action = [ "iam:PassRole" ] Effect = "Allow" Resource = "arn:aws:iam::{ACCOUNT}:role/{ROLE名}" }, { Action = [ "ecs:DescribeServices" ] Effect = "Allow" Resource = "arn:aws:ecs:{REGION}:{ACCOUNT}:service/{ECS_CLUSTER_NAME}/{ECS_SERVICE_NAME}" }, { Action = [ "ecs:CreateTaskSet", "ecs:DeleteTaskSet" ] Effect = "Allow" Resource = "*" conditions = [ { test : "StringLike" variable = "ecs:service" values = [ "arn:aws:ecs:{REGION}:{ACCOUNT}:service/{ECS_CLUSTER_NAME}/{ECS_SERVICE_NAME}" ] } ] }, { Action = [ "ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition" ] Effect = "Allow" resources = ["*"] }, { Action = [ "elasticloadbalancing:ModifyRule" ] Effect = "Allow" Resource = "arn:aws:elasticloadbalancing:{REGION}:{ACCOUNT}:listener-rule/app/{ALB_NAME}/*" }, { Action = [ "elasticloadbalancing:DescribeLoadBalancers", "elasticloadbalancing:DescribeListeners", "elasticloadbalancing:DescribeRules", "elasticloadbalancing:DescribeTargetGroups" ] Effect = "Allow" Resource = "*" }, { Action = [ "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups" ] Effect = "Allow" resources = ["*"] }, ] }) } ECSのクラスタや、ECSサービス名、ALB名はCICDのRoleの対象範囲などを考慮して置換・読み替えてください。 Create/DeleteTaskSetの権限は、Resourceでは絞れず、Conditionで起動するServiceを固定しています。DescribeLoadBalancers系の権限とec2:DescribeSubnet、DescribeSecurityGroupsの権限は、ワークフローの中で、状態判定を入れていますので、そのためのものです。 "elasticloadbalancing:ModifyRule"は言わずもがな、ListenerRuleを書き換えてリリースのために必要です。ListenerRuleはARNがランダム値が付与されるので、ALB名までで絞っています。 GitHubActions KINTOテクノロジーズではCICDツールとしてGitHubActionsを利用しています。 運用方法としてはプラットフォームGでCICD標準ワークフローを作成して、それをアプリ開発チームに提供して利用していただいています。 ワークフロー概要 今回のワークフローでは以下のステップに沿った形でBlueGreenDeploymentの仕組みを作成しました。今回ご紹介するのはDeployのワークフローのみとなります。 注意した点など これらのワークフローをアプリ開発チームに提供する側として以下の点に注意しました。 誤操作を発生させないように実行時のパラメータ指定は最小限にする実装 ワークフローは手動実行のため実行時に誤ったパラメータを指定しないよう、CLIで取得できるパラメータは全てワークフロー内で取得する ワークフロー設定の簡略化 シークレットを極力使わないような実装 環境変数でAWSリソース名を設定しているが、システム固有の値以外は固定値にすることでほとんど設定が不要 シークレットに利用するAWSリソースのARNを全て登録しておけば、ワークフロー内でリソース名からARNを取得する処理が不要になりコード量が減ります。 しかし、初期設定の負担をなるべく減らすためにほとんど設定不要のリソース名からCLIでARNを取得して利用する処理で実装しました。 ワークフローの実装 ここでは各ワークフローをサンプルコードを用いながら主となる処理の説明をしたいと思います。 どのワークフローも基本的には、 AWS Credentialsの取得 → CLIで必要なパラメータを取得 → バリデーションチェック → 実行 のような流れになっています。 タスクセット作成 ワークフロー実行時のパラメータは、ECRにあるイメージタグと環境です。 タスクセット作成前にテスト用としてターゲットグループが利用できるか、実行時のパラメータのイメージタグがECRに存在するか、などのバリデーションチェックを入れます。 その後、イメージタグからタスク定義を作成します。 タスク定義の作成後はタスクセット作成時に必要なパラメータ(サブネット、セキュリティグループ、タスク定義)を取得して、タスクセットを作成するCLIを実行する流れです。 jobs: ... ## 利用するターゲットグループの確認 check-available-targetGroup: ... ## ECRのイメージからタスク定義の作成 deploy-task-definition: ... ## タスクセットの作成 create-taskset: runs-on: ubuntu-latest needs: deploy-task-definition steps: # AWS Credentialsの取得 - Set AWS Credentials  ... - targetGroupの取得 ... # タスクセットの作成 - name: Create TaskSet run: | # タスク定義のARNを取得 taskDefinition=`aws ecs describe-task-definition\ --task-definition ${{ env.TASK_DEFINITION }}\ | jq -r '.taskDefinition.taskDefinitionArn'` echo $taskDefinition # サブネットの取得 subnetList=(`aws ec2 describe-subnets | jq -r '.Subnets[] | select(.Tags[]?.Value | startswith("${{ env.SUBNET_PREFIX }}")) | .SubnetId'`) if [ "$subnetList" == "" ]; then echo ※サブネットが取得できないため、処理を中断します。 exit 1 fi # セキュリティグループの取得 securityGroupArn1=`aws ec2 describe-security-groups | jq -r '.SecurityGroups[] | select(.Tags[]?.Value == "${{ env.SECURITY_GROUP_1 }}") | .GroupId'` if [ "$securityGroupArn1" == "" ]; then echo ※セキュリティグループが取得できないため、処理を中止します。 exit 1 fi securityGroupArn2=`aws ec2 describe-security-groups | jq -r '.SecurityGroups[] | select(.Tags[]?.Value == "${{ env.SECURITY_GROUP_2 }}") | .GroupId'` if [ "$securityGroupArn2" == "" ]; then echo ※セキュリティグループが取得できないため、処理を中止します。 exit 1 fi echo --------------------------------------------- echo タスクセットの作成 aws ecs create-task-set\ --cluster ${{ env.CLUSTER_NAME }}\ --service ${{ env.SERVICE_NAME }}\ --task-definition ${taskDefinition}\ --launch-type FARGATE\ --network-configuration "awsvpcConfiguration={subnets=["${subnetList[0]}","${subnetList[1]}"],securityGroups=["${securityGroupArn1}","${securityGroupArn2}"]}"\ --scale value=100,unit=PERCENT\ --load-balancers targetGroupArn="${createTaskTarget}",containerName=application,containerPort=${ env.PORT } リスナールール切り替え リスナールール切り替えのワークフローでは、まず起動中のタスクセット数を取得して確認します。 本番環境のタスクセットだけが起動している場合に(タスク数は1のとき)、本番環境とテスト環境に関連するリスナールールを切り替えてしまうと、本番環境に紐づくタスクセットがなくなってしまいます。 そのケースを避けるために起動中のタスクセット数を取得して、1つ以下のときはリスナールールを切り替えずに処理を落とす実装をしています。 その後は本番用とテスト用リスナールールの切り替えです。2つのリスナールールを切り替えるCLIがないため、切り替えと言っていますが正確にはリスナールールを変更するCLI(modify-rule)を実行しています。それぞれのリスナールール変更の処理を同時並行で実行しているため、多少の処理時間の差があった場合でも2つのリスナールールがともにテスト環境に紐づかないよう、sleepコマンドで処理タイミングを調整しています。 env: RULE_PATTERN: host-header ## http-header / host-header / path-pattern / source-ipなど PROD_PARAM: domain.com TEST_PARAM: test.domain.com ... jobs: ## 起動しているタスクセットが1つ以下の場合はホストヘッダーを変更できないようする check-taskSet-counts: runs-on: ubuntu-latest steps: ## AWS Credentialsの取得 - name: Set AWS Credentials ... # バリデーション - name: Check TaskSet Counts run: | taskSetCounts=(`aws ecs describe-services --cluster ${{ env.CLUSTER_NAME }}\ --service ${{ env.SERVICE_NAME }}\ --region ${{ env.AWS_REGION }}\ | jq -r '.services[].taskSets | length'`) if [ "$taskSetCounts" == "" ]; then echo※ 起動中のタスクセット数が取得できないため、処理を中断します。 exit 1 fi echo 起動中のタスクセット数: $taskSetCounts if [ $taskSetCounts -le 1 ]; then echo ※起動中のタスクセット数が1つ以下のため、処理を中断します。 exit 1 fi ## ALBリスナールール(本番用、テスト用)の切り替え change-listener-rule-1: runs-on: ubuntu-latest needs: check-taskSet-counts steps: ## AWS Credentialsの取得 - name: Set AWS Credentials ... - name: Change Listener Rules run: | # alb名からALBのARNを取得 albArn=`aws elbv2 describe-load-balancers --names ${{ env.ALB_NAME }} | jq -r .LoadBalancers[].LoadBalancerArn` # ALBのARNからリスナーのARNを取得 listenerArn=`aws elbv2 describe-listeners --load-balancer-arn ${albArn} | jq -r .Listeners[].ListenerArn` # リスナーのARNからリスナールールのARNを取得 listenerRuleArnList=(`aws elbv2 describe-rules --listener-arn ${listenerArn} | jq -r '.Rules[] | select(.Priority != "default") | .RuleArn'`) pattern=`aws elbv2 describe-rules --listener-arn ${listenerArn}\ | jq -r --arg listener_rule ${listenerRuleArnList[0]} '.Rules[] | select(.RuleArn == $listener_rule) | .Conditions[].Values[]'` if [ "$pattern" == "" ]; then echo ※リスナールールが取得できないため、処理を中止します。 exit 1 fi echo --------------------------------------------- echo 現在のルールパターン: $pattern echo --------------------------------------------- if [ $pattern == "${{ env.TEST_PARAM }}" ]; then aws elbv2 modify-rule --rule-arn ${listenerRuleArnList[0]} --conditions Field="${{ env.RULE_PATTERN }}",Values="${{ env.PROD_PARAM }}" else sleep 5s aws elbv2 modify-rule --rule-arn ${listenerRuleArnList[0]} --conditions Field="${{ env.RULE_PATTERN }}",Values="${{ env.TEST_PARAM }}" fi echo --------------------------------------------- echo 変更後のルールパターン aws elbv2 describe-rules --listener-arn ${listenerArn}\ | jq -r --arg listener_rule ${listenerRuleArnList[0]} '.Rules[] | select(.RuleArn == $listener_rule) | .Conditions[].Values[]' ## ALBリスナールール(本番用、テスト用)の切り替え change-listener-rule-2: ... change-listener-rule-1と同様の処理で、listenerRuleArnListの要素の指定のみ異なる ... タスクセット削除 タスクセット削除のワークフローでは、実行時のパラメータは環境のみにしています。 もし削除するタスクセットIDをパラメータに指定するような実装にすれば、ワークフローではそのタスクセットIDを削除するCLIを実行するだけなので一行で済みます(AWS Credentialsの取得などはありますが)。 しかし、もし誤って本番稼働中のタスクセットIDを指定してしまった場合、本番環境のタスクセットが消えてテスト環境のみ残る危険があります。 そのため、実行時のパラメータは環境のみにして、ワークフローの実装でテスト環境のタスクセットを取得して削除するような実装をしました。 env: TEST_PARAM: test.domain.com # テスト用のホストヘッダー ... jobs: ## タスクセットの削除 delete-taskset: runs-on: ubuntu-latest steps: ## AWS Credentialsの取得 - name: Set AWS Credentials ... # テスト用ホストヘッダーに紐づくターゲットグループを取得 - name: Get TargetGroup run: | # ALB名からALBのARNを取得 albArn=`aws elbv2 describe-load-balancers --names ${{ env.ALB_NAME }} | jq -r .LoadBalancers[].LoadBalancerArn` # ALBのARNからリスナーのARNを取得 listenerArn=`aws elbv2 describe-listeners --load-balancer-arn ${albArn} | jq -r .Listeners[].ListenerArn` # リスナーのARNとテスト用のホストヘッダーから、テスト用ルールに紐づくターゲットグループを取得 testTargetGroup=`aws elbv2 describe-rules --listener-arn ${listenerArn}\ | jq -r '.Rules[] | select(.Conditions[].Values[] == "${{ env.TEST_PARAM }}") | .Actions[].TargetGroupArn'` echo "testTargetGroup=${testTargetGroup}" >> $GITHUB_ENV # リスナールールがテスト用ホストヘッダーのターゲットグループに紐づくタスクセットIDを取得 - name: Get TaskSetId run: | taskId=`aws ecs describe-services\ --cluster ${{ env.CLUSTER_NAME }}\ --service ${{ env.SERVICE_NAME }}\ --region ${{ env.AWS_REGION }}\ | jq -r '.services[].taskSets[] | select(.loadBalancers[].targetGroupArn == "${{ env.testTargetGroup }}") | .id'` if [ "$taskId" == "" ]; then echo ※テスト用ホストヘッダーのターゲットグループに紐づくタスクセットが見つからないため、処理を中断します。 exit 1 fi echo 削除予定のタスクセットID echo $taskId echo "taskId=${taskId}" >> $GITHUB_ENV # 取得したタスクセットIDからタスクセットを削除 - name: Delete TaskSet run: | aws ecs delete-task-set --cluster ${{ env.CLUSTER_NAME }} --service ${{ env.SERVICE_NAME }} --task-set ${{ env.taskId }} 次のステップ ALBのListenerRuleの部分をブラッシュアップ、検討をしてカナリアリリースについても可能としたいが、まずは使用してもらって、フィードバックをいただくのが先ということで、アプリケーション側へ展開中です。 GithubActionsのワークフローに関しては、シークレットは極力使わない実装ができましたが、まだ環境変数の設定が多いため今後は減らしていきたいと思います。例えば、システム固有の値だけを環境変数で設定するなど。 また、リスナールールの切り替えは安全かつ瞬時にルールの切り替えはできないかなと模索中です。 所感 最初にもありましたが、ECS + EXTERNAL(GithubActions)でのBlueGreenDeploymentの実例はおそらく少なく、参考にできるドキュメントがない状態からここまで仕組みを作成しました。 振り返るとGithubActionsのワークフローでの実装自体は難しくはないと思いますが、簡単(設定が少ない)かつ安全に利用できるワークフローを目指すために工夫できた点がたくさんありました。 今後はこの仕組みを実際に利用していただき、フィードバックから改善してより良い仕組みを目指していきたいと思います。 まとめ OperationToolManagerチームは、社内向けの横断ツールを統制して必要なものを開発しています。 Platformグループの他チームが作ったものを受け入れたり、必要なものを新規作成や既存のものをマイグレーションしたりしています。 こういった活動に少しでも興味を持ったり話を聞いてみたいと思った方は、お気軽にご連絡いただければと思います。 @ card
アバター
こんにちは、KINTOテクノロジーズ プラットフォームグループ CCoE(Cloud Center of Excellence)チームの多田です。 CCoEチームは、「クラウドサービスを利用したシステム開発の積極的な推進とガバナンスによる統制を通じて、モビリティプロダクトの開発を、よりアジャイルかつセキュアにする」ことをミッションに活動しています。 KINTOテクノロジーズにおけるCCoE CCoEの活動内容 CCoEチームは、上述のミッションの元、クラウドの「活用」と「統制」を幅広く支援する組織として、多くの施策に取組んでいます。代表的なものをいくつかご紹介しておきます。 クラウドの「活用」 ナレッジ共有や人材育成を通じて、効率的な開発が継続的に支援を行う をあるべき姿とし、次のことに取組んでいます。 クラウド人材育成 KINTOテクノロジーズ独自のAWSスキルマップを利用した勉強会や教育コンテンツを整備し、エンジニアスキルをレベルアップする クラウドの「統制」 グループ会社セキュリティポリシーに準拠したクラウド環境を提供し、常にセキュアな状態を維持するための支援を行う をあるべき姿として、次のことに取組んでいます。 クラウドセキュリティガイドライン セキュリティポリシー準拠のためのIaC開発とセキュリティツールを整備する セキュリティプリセットクラウド環境 上記セキュリティガイドラインに則った設定済みのクラウド環境を提供する 我々の活動内容を簡単な絵にすると次のような形になります。 クラウドセキュリティガイドラインを中心として、その内容を事前設定したクラウド環境を提供し、開発グループに利活用してもらう。このクラウド環境をセキュアに維持するために、SIEM(Security Information and Event Management)やCSPM(Cloud Security Posture Management)でモニタリングを行っています。また、セキュリティガイドラインを人材育成や情報共有サイト等にも利活用する。といった活動を行っています。 セキュリティについては、日々情報がアップデートされますし、クラウドサービスも常に進化していきます。そのため、それらに追随するために、ガイドラインを中心とした持続的な活動を目指しています。 ここからは、現在、クラウドの「統制」で進めている、「セキュリティプリセットクラウド環境(Google Cloud)」について紹介していきます。 セキュリティプリセットクラウド環境(Google Cloud)の取組み セキュリティプリセットクラウド環境(Google Cloud)とは セキュリティプリセットクラウド環境は、アジリティとセキュリティの両輪を実現し、アジャイルな開発を促進することを目的として開発しています。 開発グループがクラウド環境を自由に利活用できる 企業として、最低限望ましくない利用を制限 では、具体的にどのようなセキュリティを設定しているか、ということですが、ベースとなるセキュリティ基準は以下の2つとなります。 CIS Benchmark グループ会社のセキュリティルール さらに、これらをどこに設定しているのか、ということですが、こちらは、以下の3箇所で設定しています。 組織ポリシー プロジェクト(作成時に設定) CSPM(Cloud Security Posture Management) 以降で、それぞれの箇所にどうようなセキュリティ設定を実装しているか具体的に説明していきます。 組織ポリシーで設定したセキュリティは何か 組織ポリシー は、最低限望ましくない操作を制限するために利用します。これは、予防的ガードレールと呼ばれる考えかたと同様です。 予防的ガードレールは、システムやプロセスにおいて、予防的な制約や制限を設けることで、望ましくない行動やリスクを事前に防止する仕組みやルールの総称で、人為的なミスや意図しない行動によるセキュリティ上の問題やコンプライアンス違反を最小限に抑えることができます。 セキュリティプリセットクラウド環境では、あまり厳しい制限にならないように、本当に最低限実施してほしくない操作に絞って制限するようにしています。最低限の基準として、上述の2つのセキュリティ基準をベースにしています。実際に設定しているいくつかの、組織ポリシーを紹介します。 組織ポリシー 説明 constraints/gcp.resourceLocations リソースを作成できるロケーションを制限。asiaなどのマルチリージョンを設定 constraints/sql.restrictPublicIp Cloud SQLインスタンスへのパブリックアクセスを制限 constraints/storage.publicAccessPrevention Cloud Storageのデータのパブリック公開を制限 constraints/compute.skipDefaultNetworkCreation リソースの作成時にデフォルトネットワークと関連リソースの作成を制限 constraints/iam.disableAuditLoggingExemption 監査ログからプリンシパル除外設定を制限 これらの組織ポリシーを組織全体に設定し、フォルダ配下やプロジェクトにも同様のポリシーを継承しています。万一、プロジェクト側で、これらのポリシーが許容できない場合は、プロジェクト固有でポリシーをオーバーライドする運用となっています。 プロジェクト作成時に設定したセキュリティは何か 組織ポリシーで設定できないが、予防的ガードレールとして設定したいルールについては、プロジェクト作成時に設定しています。 例えば、 CIS Benchmark には、「2. Logging and Monitoring」「2.13 Ensure Cloud Asset Inventory Is Enabled」というルールがあり、Cloud Asset Inventoryの有効化をプロジェクト作成時に自動化して実施しています。 その他、プロジェクト作成時に設定している CIS Benchmark のルールを紹介します。 項目 タイトル 説明 2.4 Ensure Log Metric Filter and Alerts Exist for Project Ownership Assignments/Changes プロジェクトオーナーの割当を監視しアラートを通知する 2.5 Ensure That the Log Metric Filter and Alerts Exist for Audit Configuration Changes 監査設定の変更を監視しアラート通知する 2.6 Ensure That the Log Metric Filter and Alerts Exist for Custom Role Changes プロジェクトオーナー、組織ロール管理者、IAMロール管理者はカスタムロールを作成できる。カスタムロールの作成は過剰な特権を持つ可能性があるため監視しアラート通知する 2.7 Ensure That the Log Metric Filter and Alerts Exist for VPC Network Firewall Rule Changes ファイアウォールルールの作成または更新を監視しアラート通知する 2.8 Ensure That the Log Metric Filter and Alerts Exist for VPC Network Route Changes VPCネットワークのルート変更を監視しアラート通知する 2.9 Ensure That the Log Metric Filter and Alerts Exist for VPC Network Changes VPCネットワークの変更を監視しアラート通知する 2.10 Ensure That the Log Metric Filter and Alerts Exist for Cloud Storage IAM Permission Changes Cloud StorageのIAM権限変更を監視しアラート通知する 2.11 Ensure That the Log Metric Filter and Alerts Exist for SQL Instance Configuration Changes SQLインスタンスの設定変更を監視しアラート通知する また、プロジェクト作成時ではないですが、以下の CIS Benchmark のルールを組織全体に設定としています。 項目 タイトル 説明 2.1 Ensure That Cloud Audit Logging Is Configured Properly すべてのアクティビティ、ユーザデータへの読み取り、書き込みアクセスを追跡するために監査ログを設定する CSPMで設定したセキュリティは何か CSPM(Cloud Security Posture Management)で実現したセキュリティを説明します。CSPMでは 組織ポリシー でも、プロジェクト作成時にも設定できない、リスクのある操作についての検知・通知を実現するために利用しています。 これは、発見的ガードレールという考え方に一致します。 発見的ガードレールは、システムやプロセスにおいて異常な活動やセキュリティ上のリスクを検出し、早期に発見するための仕組みやルールであり、インシデントや異常な活動の発見・分析に重点を置きます。予防的ガードレールと発見的ガードレールは相補的な役割を果たすものです。 セキュリティプリセットクラウド環境では、発見的ガードレールの考え方のもと、CSPM製品を使ったリスクの検知・通知を実現しているのですが、CSPM製品の選定に少し紆余曲折がありました。 採用したCSPM製品は何か CSPM(Cloud Security Posture Management)は、クラウド環境における、クラウドリソースのセキュリティ設定や構成を監視し、ベストプラクティスに基づいたポリシーやガイドラインに準拠しているかどうかを評価・管理するためのツールです。 Google Cloudでは、 Security Command Center と呼ばれるサービスがあり、StandardとPremiumの2つのティアが存在します。 Standard Premium Security Health Analytics (重大な構成ミスの特定を含む) ○ ○ Security Health Analytics (PCI,CIS,コンプライアンスレポート等) X ○ Web Security Scanner X ○ Event Threat Detection X ○ Container Threat Detection X ○ Virtual Machine Threat Detection X ○ Rapid Vulnerability Detection X ○ 費用 無料 サブスクリプション 我々としては、 CIS Benchmark 等を基準に「発見的ガードレール」を実現したいと考えていたため、要件的には、Premiumの利用となるのですが、費用的には高価で、現在のGoogle Cloudの利用規模でいうと割にあいません。 では、CSPM製品をどうしたかというと、以下の観点でいくつの製品をピックアップし、机上調査とPoCを実施しました。 CIS Benchmark 等のコンプライアンス基準が利用できる 独自ルールも実装できる 安価である(Security Command Center Premiumの費用と比べて) 主な製品と評価コメントは次のとおりです。 Forseti Security(Open-source security tools for GCP) Google Cloudリソースのインベントリ情報を収集して、定期的に監査ルールをチェックする。監査ルールは、「IAMポリシー」、「バケットACL」、「BigQueryデータセットのACL」、「Cloud SQLネットワーク」等であり、CIS等のコンプライアンス基準が利用できない Cloud Custodian(OSS) CIS等のコンプライアンス基準のデフォルトルールセットがないため、各ルールを0から実装する必要がある サードパーティ製品(S社) CIS等のコンプライアンス基準がデフォルトで準備されており、独自ルールも Rego で実装することができる。SaaS製品であるが価格も非常に安価でマーケットプレイスから購入もできる これらの調査結果をもとに、サードパーティ製品(S社)を採用することに決めました。Cloud Custodianと迷いましたが、製品のインテグレーションやルール実装のスピード感を優先し決定しています。この製品を具体的にどのように利用しているかは、また別の機会にブログにしようと思いますが、CSPM製品を導入し、 CIS Benchmark 等を基準にリスクのある操作を検知・通知し、改善を行うことでセキュアな状態を維持することに努めています。 :::message 実は、今年の2月ごろに、Security Command Center Premiumは組織単位ではなく、プロジェクト単位でも有効化できるようになり、費用的にも使いやすくなりました。Premiumでは、CSPM以外にも多くの機能があるので、今後は、サードパーティ製品とうまく使い分けることを検討していきたいと思います。 ::: プロジェクトのIAM権限はどうしているか もう一つ重要なことがあります。それは、開発グループに払い出すプロジェクトのIAM権限をどうするかという問題です。結論からいうと、利用者には「編集者」権限を付与しています。 もちろん、「編集者」権限を付与することは、バッドプラクティスであることは理解しているのですが、開発グループ側の利便性とセキュリティをトレードオフし、まずは、この権限でスタートしています。その代わり、 Policy Intelligenceツール の運用を徹底し、最小権限の原則に近づけるような運用をしていくこととしています。このあたりも別の機会にブログにしたいと思います。 まとめ セキュリティプリセットクラウド環境についてまとめると、以下となります。 開発グループが自由に利活用でき、かつ、最低限の望ましくない操作が制限されているクラウド環境を実現 実現には、「予防的ガードレール」と「発見的ガードレール」の考え方を採用 予防的ガードレールは 組織ポリシー 、発見的ガードレールは、CSPM製品を利用することで実現 IAM権限は、最小権限の原則に近づける運用を Policy Intelligenceツール で徹底 今後のCCoE活動 最後に今後のCCoE活動についてふれておきます。 今回は、Google Cloudのセキュリティプリセット環境を紹介しましたが、AWSについても同様のコンセプトでセキュリティプリセット環境を準備しています。さらにセキュリティプリセット環境をセキュアに維持するため、セキュリティのモニタリングサービス/ツールを充実させようと考えています。今回は、CSPMについて少し紹介しましたが、CSPM以外のCNAPP(Cloud Native Applicaiton Protection Platform)とよばれる領域のツールについても整備を進めようとしています。また、最近、少しずつ耳にするようになった、X.1060フレームワークを使って、セキュリティ対応組織力の継続的な向上にも着手していく予定です。機会があれば、この辺りも紹介できればと思います。 ここまで読んでいいただきありがとうございました。
アバター
AstroでSvelte使ってみた こんにちは(こんばんは)、Svelte不定期連載その4です。 過去の記事はこちら SvelteKit + Svelte を1年間くらい使ってみた知見など※SvelteKit メジャーリリース対応済み Svelteと他JSフレームワークとの比較 - Svelte不定期連載-01 Svelteでユニットテスト - Svelte不定期連載-02 SvelteでStorybookを使ってみる - Svelte不定期連載-03 今回はAstroでSvelte使ってみたの回です。 今回はSvelte連載ではあるものの、少し毛色を変えてみます。 巷を騒がせているAstroというフレームワークをご存知でしょうか。 デフォルトでクライアントサイドのJavaScriptを一切使用せずにウェブサイトを構築する、といったフレームワークです。デフォルトではJavaScriptを読み込ませずに、コンポーネントに明示的な指定をすることでJavaScriptが読み込まれます。Astroではアイランドというネーミングで親しまれています。また公式にもある通り、色々なフレームワークをAstroにマウントして使用することが出来ます(!) https://astro.build/ Astroの特徴を掻い摘んでみます。 ゼロJavaScript マルチページアプリケーション(MPA) 様々なUIフレームワークをAstro上にインテグレーションできる 今回はAstro上でSvelteを動かしてみましょう。propsやバインディングなど色々試してみようと思います。 環境用意 AstroにSvelteコンポーネントをimport AstroとSvelteでpropsしてみる AstroとSvleteバインディングで行う 環境用意 AstroとSvelteをインストールしてみる yarn create astro astro-svelte Astroのcliを用いて astro-svelteディレクトリにAstroをインストールします。 これでAstroを動かす用意は出来ましたが、これだけではSvelteを使えません。 次にAstro上でSvelteを動かせるようにSvelteとAstro用のSvelteモジュールをインストールしましょう。 yarn add @astrojs/svelte svelte Astro + Svelteを動かすモジュールが揃いましたので、AstroのConfigファイルであるastro.config.mjsにSvelteを使う旨を書きます。これでAstro上でSvelteを動かす準備ができました。 CLIのおかげでとても手順が少なくてEASYです。 import { defineConfig } from 'astro/config'; // ここを追加 import svelte from '@astrojs/svelte'; // https://astro.build/config export default defineConfig({  // ここを追加 integrations: [svelte()], }); 用意出来たので実際にAstro上でSvelteを動かしてみましょう。 AstroにSvelteコンポーネントをimport <script> let text = 'Svelte' </script> <p>{text}</p> まずは子となるSvelteコンポーネントを作りました。 タグの中にSvelteという文字列が挿入されるコンポーネントです。 では次にAstroの親コンポーネントにSvelteコンポーネントをimportします。 --- import Sample from '../components/Sample.svelte' --- <Sample /> 表示されましたね、とても簡単。というかすごいですね…! AstroはMPAなのでルーティングだけAstroに任せてコンポーネントはSvelteみたいな使い方も出来そうです。 AstroとSvelteでpropsしてみる 先程のSvelteコンポーネントの値をexportします。 <script> export let text = '' </script> <p>{text}</p> Astro側で文字列を挿入します。 --- import Sample from '../components/Sample.svelte' --- <Sample text="Svelte" /> 同じようにSvelteという文字列が表示されました。 では、逆にSvelteを親としてPropsはできるのでしょうか。試してみます。 Astroの子コンポーネントを定義して… --- export interface Props { astrotext: '' } const {astrotext} = Astro.props --- <p>{astrotext}</p> Svelteのコンポーネントで読み込む! src/components/Sample.svelte <script> import Child from './Child.astro' export let text = '' </script> <p>{text}</p> <Child astrotext="Svlete" /> NGでした。 どうやら親はAstroである必要ありそうです。 では親も子もSvelteの場合はどうでしょう。 まずSvelteの子コンポーネントを作ります。 <script> export let svelteChild = '' </script> <p>{svelteChild}</p> それをSvelteの親コンポーネントで定義…! <script> import SvelteChild from "./SvelteChild.svelte"; export let text = '' </script> <p>{text}</p> <SvelteChild svelteChild="SvelteChild" /> 無事に表示されました! 当たり前といっちゃあたりまえなのですがSvelte to Svelteはいけるようです。 またpage配下のファイルも*.astroファイルである必要がありそうです。 NGケース src/pages/+page.svelte src/pages/index.svelte 異なるUIフレームワーク拡張子のファイルをimportするには、*.astroが親である必要があることが判明しました。 SvleteのバインディングをAstroで行う 最後にバインディングを行ってみます。 Svelteでバインディングします。 <script> export let text = '' let name = ''; </script> <input bind:value={name}> <p>{name}</p> <p>{text}</p> nameという文字列部分がバインディングされていく想定です。 src/page/index.astroは変わらないので画面を見てみます。 入力しても反映されません・・・。 Astroでは、一部のクライアントサイド特有の機能(例えばこのようなインプットフィールドへのユーザー入力)はデフォルトでは機能しません。 これらの機能を利用したい場合は、import しているComponentに対してAstroのclient:loadディレクティブを使用することでバインディングが可能になります。 --- import Sample from '../components/Sample.svelte' --- <Sample text="Svelte" client:load /> 無事に動きました。 clientディレクティブも:loadだけではないので色々試してみると面白いかもです。 https://docs.astro.build/ja/reference/directives-reference/#client-directives まとめ 本当に動くの・・・?みたいな半信半疑で始めましたがAstro x UIフレームワーク プロダクトで使えそうなくらいの実用性がありますね。 コーポレートサイトなどは特にAstro使いやすそうです、ここでは触れてませんがsitemap周りの機能も強力です。 以上、AstroでSvelte使ってみたでした。 次は連載最後、Svelteの実用的なTipsです(経験則)。
アバター
はじめに こんにちは、KINTOテクノロジーズで、決済プラットフォームの開発チームに所属している西田です。 今回は、以前 こちら でも紹介した社内向けの決済に関する業務システムのバックエンドを AWS SAM で構築したお話をしたいと思います。 AWS SAM とは まずはじめに AWS SAM(Serverless Application Model)とは、Lambda や API Gateway などのサーバーレスなサービスを簡単に構築・デプロイができるツールです。AWS SAM を利用することで、我々開発者がインフラに対しての知識を深く持つ必要がなくなり、サーバーレスアーキテクチャを利用したアプリケーション開発部分に集中することができます。 AWS SAM を採用したきっかけ KINTOテクノロジーズへ入社してすぐに決済に関する業務システムの開発に参画、開発期間は2~3ヶ月程度であったこともあり、バックエンド側の技術選定では素早く開発できる技術の選定が求められました。 社内向けのシステムであったため、アクセス数が限定である点と前職では AWS SAM を利用して環境構築していた経験を活かして AWS SAM を利用して開発を進めることにしました。 AWS SAM の使い方 AWS SAM を利用して、REST API を APIGateway と Lambda を使ったサーバーレス構成で構築してみたいと思います。 ディレクトリ構成はこちらです。 . ├── hello_world │ ├── __init__.py │ └── app.py └── template.yaml はじめに AWS SAM を公式の ドキュメント からインストールします AWS SAM では template というファイルを利用して、AWS のリソースの管理を行います。 AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sam-app Sample SAM Template for sam-app Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: FunctionName: HelloWorldFunction CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.9 Events: HelloWorld: Type: Api Properties: Path: /hello Method: get import json def lambda_handler(event, context): body = { "message": "hello world", } response = { "statusCode": 200, "body": json.dumps(body) } return response SAM コマンドを使ってデプロイをします。 今回は --guided を使って対話形式でデプロイしてみます。 sam deploy --guided スタック名やリージョンなどを入力していきます。 Stack Name [sam-app]: # デプロイするスタック名を入力 AWS Region [ap-northeast-1]: # デプロイするリージョンを入力 #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: # 変更内容を確認するかを入力 #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: # SAM CLI が IAM ロールを作成するかを入力 #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: # ロールバックを無効にするかを入力 HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: # Lambda に対する認可を設定するかを入力 Save arguments to configuration file [Y/n]: # 設定を保存するかを入力 SAM configuration file [samconfig.toml]: # 設定ファイルの名前を入力 SAM configuration environment [default]: # 環境名を入力 デプロイが完了したら、Lambda のコンソールから HelloWorldFunction が作成されていることを確認します。 Lambda のトリガーになっている API Gateway を選択することでエンドポイントが確認できます。 curl でリクエストを送ってみます curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello リクエストが成功すると以下のようなレスポンスが返ってきます {"message": "hello world"} 導入してみて 過去に AWS SAM を利用していたこともあり、基盤的な構築自体は当日である程度形にできるくらいにスムーズに行え、目標としていた計画通りに開発を進めることができました。 慣れてくるとサーバーレス構成で API を簡単に作れるところが良いところだと改めて感じました。 また、APIGateway や Lambda だけでなくバッチ処理といった定期処理で利用される EventBridge や SQS も AWS SAM で構築していたりします。 公式のドキュメントも整備されてきているので、利用する上でのハードルも下がっているように思います。 まとめ 今回は、決済に関する業務システムのバックエンドを AWS SAM でゼロから素早く構築したお話をしました。 AWS が提供しているツールでもあるので親和性が高く、環境の構築負荷を下げ、より実装部分に集中できる良いツールだと思いますので、ご興味ある方はぜひ触ってみてください。
アバター
はじめに KINTOテクノロジーズでプロトタイピングエンジニアをしている木下です。 本ブログにて、アジャイルについての連載記事を発信しようという動きがあり、その前振りとしてScrum Inc.認定スクラムマスター資格の更新についてまとめます。 認定スクラムマスター資格の取得やセミナー内容については、 前回の記事 でまとめてありますので、ご興味あればご一読ください。 また前回の記事時点では、ライセンス名が「LSM:Licensed Scrum Master」という名称だったのですが、 2022年7月29日に「RSM:Registered Scrum Master」に名称が変更 されました。 取得したライセンスは自動で名称を更新していたみたいで、自分のライセンスもRSM(Registered Scrum Master)になっていましたので、併せて過去記事に名称の変更を入れました。 更新について 前回ライセンスを取得して1年を過ぎて、保持していた有効期限が近づくと更新しないと無効になるというメールが届きます。 ただ自分はメールに気づかず、前回のセミナーを一緒に受けた同僚に教えてもらうまで更新期限が来ていることを知りませんでした。 更新するかどうかを判断する期間として、60日間の猶予があります。 更新する場合は、 会員向けのサイト に行き更新費用を支払って、更新資格試験のロックを外す必要があります。 更新は1年毎で50ドルが必要という理解でいたのですが、どうやら5年プランと生涯有効プランがありました。 今回は割引が効いているようで、 5年プランが250ドルから199ドル へ、 生涯プランは500ドルから399ドル になる表示がされていました。 KINTOテクノロジーズにはセミナー補助はありますが資格補助は無いため、今回の更新費用は自費となります。 割引が効いているとはいえ円安で「1ドル:138円(支払時)」のため、それぞれの換算は 「199ドル:約27,500円」 と 「399ドル:約55,000円」 になります。 ドルだと金銭感覚があまり分からなかったのですが、円換算したらお財布と心への痛みを大変深く感じました。 なぜ更新したのか 学んだスクラム知識を活かす場面も少なかったことや自費でもあるため、正直に更新する気は全然ありませんでした。 ステークホルダーにアジャイルの理解や抵抗を払拭させることの難しさや、チーム・グループが大きいことに加え、アジャイルやスクラムをやりたい人ばかりではないこともあり、そもそもが難しいなと感じていました。 そのため、周りを巻き込んだり、少しづつでも空気を作っていくための仲間を見つけて行く活動が大切で、様々な面でライセンスの有無よりも何よりも「やるぞ!!」っという思い・マインドが大事だとを強く思いました。 意識が変わったのは、今回の記事を書いたのもそうですが、社内での繋がりが広がったり、接したことない人とスクラムマスターについて何度か話す機会を持ちました。 その中の一つに、アジャイルへ大変な熱量を持つ きんちゃん が入社して早々に行動してくれたおかげで、開くことができた座談会で他のチームと話すことがありました。 話を聞いてみて、自分の中で閉じていたのではと反省したり、似た悩みにどう解決できるのかと気持ちが少し戻ってきていました。 今後も持っていることで輪が広がること、輪が広がれば少なかった活かす場面も増やすことができるのではとの思いから更新することを少し意識しました。 くしくも近いタイミングで、前回の記事を読んで声を掛けてきた社内の同僚が、更新費用を知り背中を強く押してきたことは大変大きな要因になりました。 どうせ受けるなら思考と背中を押されたことから生涯有効プランを選択しました。 本意と不本意が合わさり複雑な心境であったのと試験中もお財布の痛さに頭がいっぱいでしたが、無事に試験に合格できました。 この心とお財布に負った傷を少しでも癒すべく、会社に置いてあった誰かのお土産を1つもらい、5万5千円のお菓子だと思って味わって食べました。 前回のセミナーを一緒に受けた同僚も更新したのですが、年間更新を受けたようで試験を受けてみたら、けっこう忘れてることも多く、振り返る意味でも年に1回の更新試験は受けて良かったと言っていました。 更新試験について 更新試験の挑戦の回数は1回だけではなく、前回のセミナー受講後の試験と同様に2回まで受けるチャンスがありました。 問題を説いた後は、点数と回答の正誤が表示されるので、どこを間違えたのかは確認できます。 出題される問題の内容・難しさに関しては、昨年のセミナーの受講後に受けた問題と同じレベルに感じました。 合格するとメールが届くのと証明書の右下あたりにある公式マークの下に有効期限が記載されているのですが、そこが「Valid Until Lifetime」に変わります。 これで生涯認定スクラムマスターになり、今後は更新試験を受ける必要は無くなりました。 有効期限が切れるごとに更新するかどうか気にしなくて良くなりました。 終わりに・宣伝 前回のセミナーを受けてから1年が過ぎ、ライセンスの更新を問われる時期になりました。 この1年でライセンスを保持するほど活きることがあったのかと言われると個人的にはあまり無く感じていましたが、人との繋がるきっかけという当初思っていたこととは違う形で活きることに価値を感じて更新をしました。 KINTOテクノロジーズでは、座談会をしたチーム以外にも多くのチームやプロジェクト・プロダクトで、アジャイルやスクラムを行ったり、取り組もうと挑戦しています。 これらについては、 アジャイルへ大変な熱量を持つきんちゃん が冒頭にお話したアジャイルの連載記事で発信します。 きんちゃんは開発の枠にとらわれず広い視野でアジャイルを俯瞰しており、実際に社内でも熱い行動を何度も起こしています。 個人的に様々な見方での内容を書かれてるのではないかと期待しています。 それでは連載記事の公開を楽しみお待ちください。
アバター
こんにちは(こんばんは)、Svelte不定期連載その3です。 過去の記事はこちら SvelteKit + Svelte を1年間くらい使ってみた知見など※SvelteKit メジャーリリース対応済み Svelteと他JSフレームワークとの比較 - Svelte不定期連載-01 Svelteでユニットテスト - Svelte不定期連載-02 今回はSvelteとStorybookを使ってみようの回です。 Storybookとは UIであるコンポーネントを管理・運用しやすくするためのツール、だと認識していますが、様々な機能を内包していたりします。 https://storybook.js.org/ 今回やること 今回は以下の3つを行ってみます。 実際のプロジェクトに導入 Storybookにコンポーネントを登録 Storybook上のテストを実行 ではやっていきましょう。 実際のプロジェクトに導入 今回は0からではなく実際のプロジェクトにStorybookを入れてみようと思います。 導入するプロジェクト URL https://noruwaaaaaaaay.kinto-jp.com/ SvelteKit + microCMS + [S3 + Cloudfront] の構成で作られているプロジェクトです。 面白いコンテンツが揃っているので是非みてみてください! おすすめ記事 https://noruwaaaaaaaay.kinto-jp.com/post/93m02vm8chf3/ https://noruwaaaaaaaay.kinto-jp.com/post/fe35u405761/ 導入手順 npx storybook@latest init こちらのコマンドをプロジェクトがあるディレクトリで実行します。 これだけで、プロジェクト内にStorybookの初期構築が完成します。 .storybook というディレクトリとsrc配下に stories というディレクトリが作られました。 これで初期構築は終わりです。 Storybookにコンポーネントを登録 Storybookを動かしてみる Storybookを立ち上げてみます。 yarn storybook を実行します。 このような画面が立ち上がりますね。 src/stories/ 配下にあるコンポーネントや **.stories.ts などはプロジェクト内では使ってないファイルのため、 一度stories配下にあるファイルを全て消して、 Button.stroies.ts を改めて配置し、実際にのるウェイで使っているコンポーネントをStorybookに登録してみます。 Storybookにコンポーネントを登録してみる 実際にプロジェクト内でコンポーネント化されているボタンのビジュアルとコードがこちらです。 <script lang="ts"> export let button: { to: string; text: string }; </script> <div class="button-item"> <a href={button.to} class="link-block" > <span class="link-block-text">{button.text}</span> </a> </div> 実際にStorybookへ上記のボタンコンポーネントを登録してみましょう。 import type { Meta, StoryObj } from '@storybook/svelte'; // ボタンコンポーネントを登録 import Button from '$lib/components/Button.svelte' const meta: Meta<Button> = { title: 'Example/Button', component: Button, tags: ['autodocs'], }; export default meta; type Story = StoryObj<Button>; export const Primary: Story = { // ボタンコンポーネントのexport letになっているオブジェクトを登録 args: { button: { to: '', text: '' } }, }; このような画面に更新されました。 では実際にstorybookの画面上でテキストの差し替えなどしてみます。 実際に変わることが確認出来ました。 本当に最小限ではありますが、これだけでボタンコンポーネントの配置が終わりました。 Storybookでテストをしてみる storiesを追加したコンポーネントに対して実際のstoriesファイルが破損していないかのテストを簡単にしてみます。 導入手順 まずテストに必要なモジュールをインストールします。 yarn add --dev @storybook/test-runner storybook上のテストを実行 実際にテストをしてみます。 yarn test-storybook を実行すると、 テストがパスされるとこのような出力に。 どの部分で失敗したかにもよりますがテストに失敗すると以下のように こちらでストーリーブックが破損していないかのテストが行えました。 たくさんのオプションも用意されているのでもっと詳しく知りたい方は下記をご覧ください。 https://storybook.js.org/docs/svelte/writing-tests/test-runner おわり インストールから、コンポーネントに対してのストーリーの追加、そしてStorybookが破損していないかのテストまでが、簡単に行うことが出来ました。 余談ではありますが、HTMLオンリーのプロジェクトにStorybookを追加しようとしてとても大変だったことがあるので、今回やってみて非常に簡単にできたので良い時代になったな、と感じました。 以上、SvelteでStorybookを使ってみようの会でした。 次回は少し毛色を変えて、 Astro上でSvelteを動かしてみよう です。 次回もお楽しみに!
アバター
1. はじめに こんにちは。共通サービス開発グループ[^1][^2][^3]で複数のサービスが利用する決済プラットフォームの開発チームに所属している鳥居です。 前回の記事 ^4 では、Visual Studio Code(以下 VS Code)の Dev Container を活用し、快適な開発環境を構築する方法を紹介しました。Dev Container は非常に便利である一方、ローカルマシンのリソースを利用するため、パフォーマンスがマシンスペックに依存するという課題がありました。特に Mac での使用では、ファイルシステム間の相互作用が原因で遅延が生じる場合がありました。 今回の記事では、私たちのチームでも実際に活用している Dev Container による開発環境構築をさらに進化させた GitHub Codespaces について解説します。GitHub Codespaces を利用することで、クラウドベースの開発環境を手軽かつ効率的に構築する方法をご紹介します。 2. GitHub Codespaces の概要 GitHub Codespaces は、クラウド上で完全な開発環境を提供するサービスで、VS Code、VS Code Web、IntelliJ、JupyterLab などの主要な開発ツールをサポートしています。これらのツールは Windows、Mac、そして Linux のすべての主要なプラットフォームで使用することができます。これにより、開発者は自分の好みに応じた環境で開発作業を行うことができます。さらに、VS Code Web を使用する場合は、ブラウザがある場所ならどこでも開発環境にアクセスし、ローカルマシンとクラウド間での作業をシームレスに行うことが可能です。 3. GitHub Codespaces の具体的な活用シーン GitHub Codespaces は、以下のようなシーンで活用することができます。 3.1 プロジェクトのクロスプラットフォーム開発 クラウドベースの開発環境であるため、GitHub Codespaces は開発者のデバイスや OS に依存しないのが特長です。これにより、各種環境における開発環境の構築や設定を一から行う手間が省かれます。開発者は自分の好きな OS を利用しつつ、他の全ての人と同等の開発環境を享受できます。 3.2 教育やワークショップでの使用 開発環境のセットアップが容易であるため、教育やワークショップなどの場では GitHub Codespaces が特に有用です。参加者は環境設定に時間を取られることなく、学習や実践に専念することが可能となります。 3.3 プルリクエストのレビュー GitHub Codespaces は、GitHub と深く連携しているため、プルリクエストを直接開くことが可能です。これにより、現在作業しているブランチから切り替えることなく、プルリクエストのレビューをスムーズかつ迅速に行うことができます。 4. GitHub Codespaces のセットアップ手順 GitHub Codespaces を構築するには、Dev Container と同様に、 .devcontainer ディレクトリ内に設定ファイルを作成する必要があります。以下では、GitHub Codespaces のセットアップ手順を紹介します。 4.1 前提環境 GitHub アカウント 対象のリポジトリ 4.2 Dev Container の構築手順 対象のリポジトリに .devcontainer ディレクトリを作成します。 .devcontainer ディレクトリ内に Dockerfile 、 docker-compose.yml 、および .devcontainer.json を作成し、それぞれの設定ファイルに適切な内容を記述します(次章の 設定ファイルのサンプル を参考にしてください)。 設定ファイルをリポジトリにコミットし、プッシュします。 GitHub リポジトリにアクセスし、リポジトリページの右上にある緑色の "Code" ボタンをクリックします。 ドロップダウンメニューから "Codespaces"タブ を選択します。 新しい Codespace を作成するか、既存の Codespace を選択して開くことができます。 今回は初回なので、"Create new codespace on main"を選択します。 オプションとして、リモートマシンのスペックやリージョンを指定できます。 三点リーダーから "New with options..." を選択します。 Codespaces の起動画面に遷移し、準備が整うまで数分かかります。準備が整ったら、ブラウザ上で Visual Studio Code のインターフェイスが表示されます。 これで、Codespaces が起動し、リポジトリのコードを編集できるようになりました。また、ターミナルも利用可能で、開発環境にインストールされているツールを使用できます。 5. 設定ファイルのサンプル 5.1 .devcontainer.json のサンプル .devcontainer.json は、Dev Container や Codespaces の設定を記述するためのファイルです。このファイルでは、開発環境の構築、使用する拡張機能、設定などを定義します。 docker-from-docker は開発環境から Docker を利用するための設定項目です。この設定を追加することで、Dev Container 内からホストマシンの Docker を利用することが可能になります。この設定を追加しない場合、Dev Container 内から Docker を利用することはできません。 ghcr.io/devcontainers/features/sshd [^5]は、JetBrains Gateway Codespaces 用の設定項目です。JetBrains Gateway Codespaces は、JetBrains IDE で GitHub Codespaces を利用するための機能です。この設定を追加することで、JetBrains IDE から GitHub Codespaces へのアクセスが可能になります。 { "name": "sample-app", "build": { "dockerfile": "Dockerfile" }, "service": "devcontainer", "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", "postCreateCommand": "sh .devcontainer/post-create.sh", "features": { "ghcr.io/devcontainers/features/go:1": { "version": "latest" }, // ホストマシンのDockerを利用するための設定 "docker-from-docker": { "version": "latest" }, // Jetbrains Gateway Codespaces用 "ghcr.io/devcontainers/features/sshd:1": { "version": "latest" } }, "settings": { "editor.guides.bracketPairs": true, "editor.stickyScroll.enabled": true, "editor.stickyScroll.maxLineCount": 5, "workbench.colorCustomizations": { "editorStickyScroll.background": "#00708D", "editorStickyScrollHover.background": "#59A2B5" }, "editor.formatOnSave": true, "[go]": { "editor.formatOnSave": true, "editor.defaultFormatter": "golang.go" }, "go.formatTool": "gofmt" }, "extensions": [ "golang.go", "GitHub.vscode-pull-request-github", "GitHub.copilot" ] } 5.2 Dockerfile のサンプル devcontainer や Codespaces の Docker コンテナを構築する際に使用する Dockerfile です。 ARG VARIANT="jammy" FROM mcr.microsoft.com/vscode/devcontainers/base:1-${VARIANT} 5.3 .devcontainer/docker-compose.yml のサンプル devcontainer や Codespaces のコンテナを構築・実行するための docker-compose ファイルです。このファイルでは、開発環境に必要なサービスや環境変数、ボリュームの設定を行っています。 コンテナ内から MySQL や LocalStack にアクセスする場合、 localhost ではなく、 mysql や localstack というホスト名でアクセスする必要があります。そのため、 MYSQL_HOST や LOCALSTACK_HOST のように、ホスト名を環境変数として設定しています。 version: "3" services: devcontainer: build: context: . dockerfile: .devcontainer/Dockerfile environment: TZ: Asia/Tokyo MYSQL_USER: developer MYSQL_PASSWORD: password MYSQL_HOST: mysql:3306 # localstack LOCALSTACK_HOST: localstack:4566 DEFAULT_REGION: ap-northeast-1 AWS_ACCOUNT_ID: "000000000000" AWS_ACCESS_KEY_ID: dummy-access-key AWS_SECRET_ACCESS_KEY: dummy-secret-key volumes: - ..:/workspaces:cached command: /bin/sh -c "while sleep 1000; do :; done" 5.4 docker-compose.yml のサンプル こちらは一般的なアプリケーション開発で利用されている docker-compose.yml のサンプルです。 このファイルでは、 MySQL [^6]や localstack ^7 などのアプリケーションに必要なサービスや環境変数、ボリュームの設定を行っています。これにより、アプリケーションを構成するコンテナを一括で構築・実行することができます。 version: "3" services: app: build: context: . dockerfile: Dockerfile volumes: - .:/workspace ports: - "3000:3000" mysql: container_name: mysql build: ./docker/mysql environment: MYSQL_DATABASE: sample MYSQL_USER: developer MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: password volumes: - ./docker/mysql/sql:/docker-entrypoint-initdb.d ports: - 3320:3306 localstack: image: localstack/localstack:latest environment: - HOSTNAME=localstack - SERVICES=s3 - DEFAULT_REGION=ap-northeast-1 - DATA_DIR=/tmp/localstack/data volumes: - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack" - "/var/run/docker.sock:/var/run/docker.sock" ports: - 4777:4566 6. VSCode、JetBrains IDE から利用 Codespaces は、Web ブラウザから利用できますが、VSCode、 JetBrains IDE からも利用できます。ここでは、それぞれの利用方法を紹介します。 6.1 VSCode で Codespaces を起動する 「VSCode GitHub Codespaces」拡張機能をインストールする VSCode で Codespaces を利用するためには、「VSCode GitHub Codespaces」拡張機能をインストールする必要があります。 拡張機能パネルを開くために、左側のアクティビティバーから拡張機能アイコンをクリックし、検索ボックスに「GitHub CodeSpaces」と入力します。検索結果に表示される「GitHub CodeSpaces」拡張機能をインストールします。 または、 こちら からインストールすることもできます。 GitHub にログインする VSCode で「GitHub CodeSpaces」拡張機能を起動し、GitHub アカウントでサインインします。 6.2 JetBrains IDE で Codespaces を起動する JetBrains Gateway をインストールする JetBrains Gateway は、JetBrains 製の IDE(IntelliJ IDEA、WebStorm、PyCharm など)で GitHub CodeSpaces を利用するためのツールです。[^8] JetBrains Gateway で CodeSpaces を起動する手順は以下のとおりです。 なお、先程の例に示したように、JetBrains Gateway で CodeSpaces を利用するためには、 .devcontainer.json に以下の設定を追加する必要があります。 { "features": { "ghcr.io/devcontainers/features/sshd:1": { "version": "latest" } } } JetBrains Gateway のインストールページ にアクセスし、インストーラをダウンロードしてインストールします。 または、 JetBrains Toolbox からインストールすることもできます。 GitHub CLI をインストールする JetBrains Gateway は、GitHub CLI を利用して GitHub にログインします。 GitHub CLI をインストールするには、 こちら の手順に従ってください。 Windows を利用している場合は、 こちら のインストーラを利用することもできます。 GitHub にログインする JetBrains Gateway を起動し、 GitHub Codespace をインストールします。 次に、メニューの GitHub Codespaces > Sign in to GitHub をクリックし、 GitHub にログインします。 ワンタイムコードと認証ページへのリンクが表示されます。リンクをクリックし、GitHub にログインします。 ワンタイムコードを入力し、[Continue] ボタンをクリックします。 次に、 [Authorize github] ボタンをクリックします。 CodeSpaces を起動する Your Recent Codespaces から起動したい Codespace を選択し、[Open] ボタンをクリックします。 もし、 Codespaces を作成していない場合は Click here から Codespaces の作成画面を開くことができます。 7. 導入してみて感じたメリット・デメリット 7.1 メリット 開発環境がどこからでもアクセス可能:物理的な場所に縛られずに開発作業が可能 チーム内での環境構築が容易:環境設定を共有することで、新たなメンバーのセットアップが迅速かつ簡単 devcontainer と設定を共有できる:開発環境の一貫性が保証される 新しいデバイスでのセットアップが迅速:ハードウェアの変更がプロジェクトの進行を妨げない 複数の開発ツールで利用可能:Visual Studio Code, Visual Studio Code for the Web, JetBrains IDE, JupyterLab(Dev Container では VS Code に限定されていた) 7.2 デメリット 費用がかかる場合がある:利用時間やリソースに応じて、費用が発生する可能性がある[^9][^10] JetBrains Gateway はまだ Beta で不安定な場合がある:この機能がまだ開発段階にあるため、一部の機能が期待通りに動作しない可能性がある インターネット接続が必須:オフラインでの作業ができない クラウド上で実行されるため、パフォーマンスやセキュリティへの懸念がある場合もある:ネットワーク遅延やデータ保護などの問題が生じる可能性がある リポジトリ管理されていないファイルや MySQL などに投入したデータは Codespaces が削除されると消えてしまう:永続的なデータストレージが必要な場合は、適切なバックアップ戦略が必要 8. さいごに いかがでしたでしょうか。参考になれば幸いです。 設定ファイルのサンプルは、実際に開発で利用しているものから抜粋したもので、各メンバーがそれぞれローカル環境、Dev Container、Codespaces でシームレスに開発をおこなっています。 さらに、VS Code の Code Tour 拡張機能と組み合わせて、新規メンバーのオンボーディングや、ワークショップ形式の勉強会にも活用しています。 ワークショップでは、環境構築手順の手間を省いてすぐに本題の作業に取り組むことが出来るので、Codespaces の利便性を大いに感じています。 また、Google Cloud からも Cloud Workstations [^11] がリリースされました。興味がありましたらぜひ触ってみてください。 GitHub Codespaces は、クラウドベースの開発環境を手軽に構築するための強力なツールです。Dev Container を利用することで、チーム全体での環境構築を効率化することができます。GitHub Codespaces を利用することで、さらに手軽に開発環境の構築を行うことができ、チームの生産性の向上につながります。ぜひ、GitHub Codespaces を活用してみてください。 [^1]: 共通サービス開発グループメンバーによる投稿 1 [ グローバル展開も視野に入れた決済プラットフォームにドメイン駆動設計(DDD)を取り入れた ] [^2]: 共通サービス開発グループメンバーによる投稿 2 [ 入社 1 年未満メンバーだけのチームによる新システム開発をリモートモブプログラミングで成功させた話 ] [^3]: 共通サービス開発グループメンバーによる投稿 3 [ JIRA と GitHub Actions を活用した複数環境へのデプロイトレーサビリティ向上の取り組み ] [ [VSCode Dev Containerを使った開発環境構築](https://blog.kinto-technologies.com/posts/2022-12-10-VSCodeDevContainer/) ] [^5]: devcontainers/features sshd について [ devcontainers/features sshd ] [^6]: docker-compose での MySQL 設定方法 [ MySQL と Docker Compose を使ってマルチコンテナー アプリを作成する ] [ [GitHub localstack](https://github.com/localstack/localstack) ] [^8]: JetBrains IDE でのリモート開 [ JetBrains IDE でのリモート開発 ] [^9]: GitHub Codespaces の請求について [ GitHub Codespaces の請求について ] [^10]: Default idle timeout の設定を短くすることで、無駄なコストを抑えることができます。 [ Codespaces の設定 ] [^11]: Google Cloud - Cloud Workstations [ Cloud Workstations ]
アバター
はじめに こんにちは。 モバイルアプリ開発グループ 沖田です。 とあるサービスのプロジェクトにて、 スマホアプリ開発に携わっています。 東京のメンバーと一緒に開発をしていますが、 日頃はOsaka Tech Labに生息しております。 というわけで、 私からは「Osaka Tech Lab 紹介」をお届けします! Osaka Tech Lab ヒストリー 2022年4月 誕生。 IT エンジニア会社として実力を上げたい、 エンジニアの採用を含めて極力幅広く門戸を開きたい、 という想いから生まれた Osaka Tech Lab。 なんと、、ひとりからスタートしました! フロアを広々と独り占め状態です。 名古屋オフィス 室町オフィス 神保町オフィス Osaka Tech Lab 4つの拠点から成る弊社。 「どうして大阪だけOsaka Tech Labなのですか?」とよくご質問をいただきます。 どうしてかと言いますと。。 「大阪分室」や「大阪オフィス」よりも格好よく「Osaka Tech Lab」とすることでいろいろな人に興味を持っていただけると考えたからです! 2022年7月 初めてのOsaka採用。4名が集う。 そこから徐々に仲間が集まり、、 2023年5月現在 16名になりました! Osaka Tech Lab って どこにあるの!? 最寄りは心斎橋駅です! Osaka Tech Lab 地下鉄御堂筋線・長堀鶴見緑地線「心斎橋駅」3番出口 徒歩1分 ※敷地内禁煙(屋内喫煙可能場所あり) Osaka Tech Lab ってどんな雰囲気!? 写真を添えてご紹介します。 エントランスです 本棚です 棚の上には、KINTOのロゴ入りミニカーが並んでいます。 サボテンたちもいます。 フリースペースです ソファー席とテーブル席があります。 ここでわぃわぃご飯食べたり、ちょっと休憩したり、集中コーナーとして使ったり。 お菓子エリアです 日頃は異なる業務をしているメンバーの集まりです。 「ちょっと一息」で雑談するきっかけになれば、、 という願いを込めて創設されました! 大活躍ですw 時計です 雑談がきっかけで購入に至った、みんなで選んだ時計です。 こんな感じです。 「ふと見たときに時間がわかるとうれしいよねー」 「あ、おんなじこと思ってた!」 「設置しても良いか、ちょっと聞いてみよう」 「おしゃんなやつがいいねー」 「地震対策もしておこう!」 こんな感じで、ふと思い立ったことを口頭やslackでわぃわぃやっています。 季節感もあるよ クリスマスシーズンはクリスマスツリーも飾っちゃいます。 Osaka Tech Lab ってどんなメンバーがいるの!? 総勢16名ではありますが、 いろんなグループのメンバーが集まっています。 プロジェクト推進G オウンドメディア&インキュベート開発G プラットフォームG 共通サービス開発G 分析G コーポレートITG 人事採用G モバイルアプリ開発G どっしり構え、時には引っ張り、時には見守ってくださるベテラン勢。 いろんなアイディアを積極的に発信してくれるジュニア勢。 年齢層のバランスもとても良いように思います。 Osaka Tech Lab の魅力ってなに!? 「ヒト」「未知なる未来」でしょうか。 「ヒト」 居心地、良きです。 いわゆる、アットホームな雰囲気というやつです。 出張で来られた方からも、 「はじめて来たけど、Osaka Tech Labって良い雰囲気なんだね」ってお褒めの言葉をたくさんいただいております(//∇//) なぜ居心地が良いのか、考えてみました。 役割が違っても、互いにリスペクトしあって仕事に取り組むメンバーが 集まっているからだと思います。 みんな、自身の意見は持っています。 でも、ヒトの意見も聞くことができて、良くするためのディスカッションをすることができます。 これって、当たり前のようで、当たり前ではなかったりしませんか!? これがOsaka Tech Labの強みだと思います! 「未知なる未来」 オフィス立ち上げに携われるってなかなかない!そんな経験をしてみたい! と思って入社を決めたわたくし。 「やってみたーい」って手を上げると、「どーぞどーぞ」ってなりますw もちろん、なんでもありというわけではありませんが、 目的を掲げて道筋を描いたことであれば、どんどん挑戦させてもらえます。 1つ事例を紹介します! Osaka Tech Labメンバーで 情報共有会 を立ち上げました。 目的はこんな感じです。 Osaka Tech Labメンバーのコミュニケーション活性化 Osaka Tech Labの仲間がどんな業務をしているのかを知り、横のつながりを強化 チームや個人の経験をシェアし、新しい取り組みにつなげること で、こんなことをやっています! グループ紹介 LT Osaka Tech Lab をより良くするためのディスカッション 2023年1月 メンバーの仲が深まってきたころ。 普段は他拠点にいる各グループのマネージャーも招いて、第3回を開催。 語って語って語り尽くす​ 〜 Osaka Tech Lab の 未来を作る第1歩 〜​ マネージャー陣がOsaka Tech Lab に期待することを聞いたり、 各メンバーがココロの中で思い浮かべていることを伝えあったり。 いま思えば、これが大きな第一歩だったように思います。 最初は、10人でスタートしたこちらの会。 人が人を呼び、他拠点にも広がりました。 いまでは、Osaka Tech Lab のメンバーの人数よりも、 他拠点からの参加人数の方が多いです。 所属グループの他拠点のメンバーとは、出張で会うことができますが、 情報共有会を通じて、日頃、関わりのない方とのつながりもできるようになりました。 Osaka Tech Lab 野望 いまは、それぞれが東京主体のプロジェクトにjoinしており、 スピード感あふれる、刺激多き日々を過ごしています。 いつか。。いつの日か。。 Osaka Tech Lab で 1サービスの立ち上げ・開発・運用をしたい。。 そんな想いを秘めているメンバーは、実は多いようですw まだ見ぬ未来が楽しみです♪ さいごに いかがでしたでしょうか。 未知なる未来に向かって、 一緒にOsaka Tech Labを創りませんか!? ご応募、お待ちしています! KINTOテクノロジーズ株式会社採用TOP wantedly
アバター
はじめに こんにちは。プラットフォームGでDevOpsエンジニアだったものからOperationToolManagerチームでPlatformEngineeringとかツール周りの開発・運用の役割になった 島村 です。 KINTOテクノロジーズのプラットフォームGでは、Terraformを使用したIaCを推進しています。社内でよく使われるデザインパターンを定義し、リファレンスアーキテクチャとして提供しており、そのパターンをベースとして各環境を構築しています。開発~本番環境の各環境では、統制のために、チケットベースの依頼にて構築を実施しています。 開発環境を構築する前に、アプリケーション部門の検証のためにサンドボックス環境(AWSアカウント)を準備していますが、手動で構築されることも多く、プラットフォームGの構築する環境とは差分が多くあります。 デザインパターンがあるなら、開発者のリクエストにより環境が自動で構築されれば、環境構築の依頼から作成されるまでの待ち時間もなくなり、開発効率は向上します。 DevOpsではこのようなリクエストベースでの自動構築は要件として一般的な話かなと思いますが、やはり、アプリケーション実行基盤はKubernetesが多いようです。 KINTOテクノロジーズではAmazon ECS+Fargateをアプリケーション実行基盤として使用していますので、ECSを対象とした自動環境構築の(多分)珍しい事例としてご紹介いたします。 背景 課題 アプリケーション開発担当者が必要なタイミング(検証・走り出し)でシステムが存在しない DevOps活動の一環として、AutoProvisioning(自動環境構築)については調査をして、一般的だなと感じたが社内には存在しない 比較的自由度の高いサンドボックス環境で構築した環境とプラットフォームGによって提供されるデザインパターンに則った環境での差分が大きい IAM権限などセキュリティまわり VPC/Subnet/NAT Gatewayなどの共通系コンポーネントの存在 などなど それに伴い、構築依頼の際の双方のコミュニケーションコストが高くなる 解決方法 自動構築の仕組みを作ればいいじゃない デザインパターンなので不足するAWSサービスもありますが、そこは許容して手動追加を前提としています。 最初の第一歩として、1時間程度でAWS上の環境を自動構築して、アプリケーション動作確認やCICDの準備ができるという状態にすることは価値がある。 作ってみる ありがたいことに、Terraformの1ファイル(locals.tf)を書くだけで色々なパターンで環境構築できるようにModule化が進んでいるので、こちらをベースに考えます。 社内作成のModule群を使うこと(Must) 社内のデザインパターンをベースに構築すること(Must) DNSは自動で設定されて、HTTPSで通信ができること locals.tfを自動生成できること GolangのHCLWriteで構造化して生成できるかアプリケーションを試作 構造化が難しいことが試作して判明したため、最終的には自動生成を諦めた Templateファイルから一部パラメータを置換することで対応 置換での処理となったので、各コンポーネントの細かい設定は不可 できたもの CMDB上のGUIから、 プロダクト デザインパターン を選択して新規作成を押すと、プロダクトに紐づいた部署のサンドボックス環境に、指定された構成が10分~40分(構成次第)で構築される。 全体構成 個別説明 Terraformコードを作成する部分と実際にサンドボックス環境に構築する部分は分けており、個別にテストできるようにしました。 Terraformコード生成パーツ ProvisioningSourceRepo Issue管理 GitHubActions実行 作成したサンドボックス環境のTerraformコード サンドボックス環境ごとのCIDRリスト ProvisioningAppRepo デザインパターンのTemplate CodeBuildのYaml(buildspec.yml) CodeBuild上で動かす各種ShellScript InfraRepo TerraformModule AWS環境構築部分 S3 CodePipelineのSourceとArtifact EventBridge CodePipelineのTrigger CodePipeline/CodeBuild 実構築環境 Route53(Dev) 本番のDNSから権限移譲をしてDev環境のRoute53を使用 Terratest(Apply) Terratestのサンプルはこのような形。Init、Plan、Applyのどこかで失敗したらテストを終わらせるという条件の関係で入れ子に。Apply途中で失敗した場合は、途中までのものをDestroyする。Golangの知識があればもっと綺麗にかけると思う。 package test import ( "github.com/gruntwork-io/terratest/modules/terraform" "testing" ) func TestTerraformInitPlanApply(t *testing.T) { t.Parallel() awsRegion := "ap-northeast-1" terraformOptions := &terraform.Options{ TerraformDir: "TerraformファイルがあるPATH" + data.uuid, EnvVars: map[string]string{ "AWS_DEFAULT_REGION": awsRegion, }, } // InitでErrorがなければPlan、PlanでErrorがなければApplyと // IFで入れ子構造の対応を実施(並列だとInitで失敗してもテストとしてすべて走る) if _, err := terraform.InitE(t, terraformOptions); err != nil { t.Error("Terraform Init Error.") } else { if _, err := terraform.PlanE(t, terraformOptions); err != nil { t.Error("Terraform Plan Error.") } else { if _, err := terraform.ApplyE(t, terraformOptions); err != nil { t.Error("Terraform Apply Error.") terraform.Destroy(t, terraformOptions) } else { // 正常終了 } } } } 要素 名称 概要 CMDB(内製) Configuration Management Database。構成管理のデータベースのこと。リッチな機能までは不要でしたので、KINTOテクノロジーズではCMDBを内製しています。その上に自動構築のリクエストフォームを構築しています。また、構築後にFQDNなどをCMDBに自動で登録しています。 Terraform AWSなど色々なサービスをコード化する製品。IaC。社内のデザインパターンとModuleはTerraformで作成されています。 GitHub ソースコードを保存するバージョン管理システム。構築リクエストはIssueを起票する形でログに残るようにしています。また、削除する際などにTerraformのコードが必要なので、サンドボックス環境の各コードも保存しています。 GitHubActions GitHubに包含されているCICDツール。KINTOテクノロジーズではGitHubActionsを使用してアプリケーションのビルド・リリースなどを実行しています。今回はIssue起票をトリガーに、Create/Deleteを判断して必要なコード群を選択し圧縮してAWSに連携するために使用しています。 CodePipeline/CodeBuild AWSが提供しているCICD関連のツール。Terraformコードを実行するために使用しています。GitHubActions上でTerraform/Terratestを走らせてもよいのですが、GitHubActionsはアプリケーションビルドなどで日々使用しているので、Usage limits などによる各プロダクトチームへの影響を避けるためこちらを使用しました。 Terratest インフラコードなどをテストするためのGoライブラリ。モジュールのテストもできますが、今回はTerraformのApply途中で失敗した際にリカバリすることを目的に使用しています。 公式はこちら 制限されること 各開発チームに紐づく複数のサンドボックス環境(AWSアカウント)を対象にしますが、作成できるのは同時1つまでにしています(排他) DNSの関係でCodePipeline/CodeBuildを1つの環境で動かしているため アプリケーションが動く部分以外も作成します 無駄も多い気もしますが、構築デザインパターンの関係上こうなりました FQDNからDBまで一気通貫のラインとして構築されます VPCなどを事前にModuleに設定する必要があります 使用する前にVPCなどの共通コンポーネント構築一式が必要 Module群がない場合はどうするのか KINTOテクノロジーズでは以前からデザインパターン化を進めていましたので、CloudFrontからRDSまでをすべて含めた構築が容易にTerraformでできる利点があります。 そこまで進んでいないけどAutoProvisioningをECSで実現したい場合はどうする方法があるでしょうか。 考えてみた ECSのClusterまでは事前に作成しておき、 ECS Service ECR Repository ALB TargetGroup ALB ListenerRule IAM Role Route53 を書いたTerraformファイルを準備して構築するのが楽なのかなと。TaskDefinitionは権限あれば作れるので、使用する側でよしなに。 構成案 GitHubActionsの代わりにCodePipeline/CodeBuildでもいいと思ってますが、CodeCommitとかGUIの準備を考えるとGitHubにまとめたほうが簡単なんじゃないか?ということで、この構成に。AWS Protonはまだ触れてないので、未検討です。 locals.tfなどのParameter部分を分離しておいて、sedコマンドや、GolangのHCLライブラリをつかって作成できれば行けるかなと思います。Terratestなどで構築が確認出来たら、任意のFQDNをALBのエイリアスに追加して、ListenerRuleと合わせる形。 次のステップ もともとはプレ提供でフィードバックをもらおうと思っていましたが、現状はあまり利用されていません。そのためにGUIを提供したので、今後さまざまな人にまずは使って貰ってそのフィードバックを受領するところから始めようと考えてます。 とはいえ、対応できるデザインパターンを増やすことや、付随したCICDの設定の簡素化などできることは多いかなと考えています。…本当はKubernetes導入をして、事例の多いAutoProvisioningに進めたいところ。 |・ω・`) だめですか? 所感 本当はGolangでテンプレートを自動生成する方向で頑張ったんですが、社内のデザインパターンのHCL構造が解析、再構成しづらい形だったのであきらめたという経緯もあります。 Consoleの再発明じゃないかという話も内部ではありましたけど、そこまで行けると、サンドボックス環境だけでなくSTG環境くらいまでなら自動化できたかなとも思っています。プラットフォームGもGUI上からポチポチちょっと入力と選択するだけで環境ができる。わあ、楽。 そこまで達したかったのが本音ですが、まずは第一歩でも進めることができたことは良かったかなと。 Kubernetesでは、Helmチャートをテンプレートで準備すれば、似た感じで作れるのではと思っています。別方法も検討してみて、いろいろとやってみたいですね。 まとめ OperationToolManagerチームは、社内向けの横断ツールを統制して必要なものを開発しています。 前に執筆しました O11yの記事 もですが、仕組みを整理してアプリケーション開発者側に提示してセルフサービスで使用できるようにする、そういった開発者が価値を創造するための下支えの立場で活動しています。 少し前にPlatformEngineeringのMeetUpが開催されましたが、進む方向性として合致してると安心できますね。 OperationToolManagerチームでは内製ツールの構築部門もいますので、開発者が迅速にかつ集中してアプリケーションの価値を作れるようにしていきます。 またこの活動に少しでも興味を持ったり話を聞いてみたい、と思った方はお気軽にご連絡いただければと思います。 @ card
アバター
はじめに KINTOテクノロジーズにおけるローカライゼーションに関して、後編となる今回の記事では、これまでチームが行ってきたことをご紹介します。 課題 前回の記事 では、ソフトウェア翻訳が翻訳キーと対応するバリュー(文言)のペアで保存されることが多い、と説明しましたが、今回はこのデータをどう管理しているかについて説明します。キーと文言のペアがある前提として、ローカライゼーションのプロジェクトマネージャーはベース言語(KINTOの場合は英語)をどのように扱い、翻訳業者(Language Service Provider =LSP)に翻訳依頼すべきでしょう? 私自身、フリーランスで翻訳を行っていた経験がありますが、その中でもエンジニアがExcelで翻訳キーとソース言語の文言を2列で作成し、それをターゲット言語の数だけ複製して翻訳に回すというのはよくあることでした。しかし、この管理方法には、大きく3つの課題があります。 文脈や言葉の前後関係の欠落 一元管理できず、将来的な不整合が起こりうる 人為的ミスが起こりやすい 1つ目の課題として、Excelシートは翻訳者にとっては何の脈絡もない単純なテキストの羅列です。指定のテキストがボタンなのかどうか?デザイン上どれくらいのスペースがあるか?プレゼンのようなストーリーテリングの場合、どのような場面で誰が誰に向かって話しているか?適切に翻訳するにはこのような情報が必要です。 2つ目の課題は、翻訳管理システム(TMS)などに登録しない限り、翻訳した、文言はどこにも記録されないということです。記録がないため、以前どのように翻訳したかを参照することが難しく、以前は「reservation」としていたものを今回は「booking」としてしまうなど、のちのちサービスのクオリティやカスタマーエクスペリエンスに矛盾が生じる可能性があります。ソース言語(英語)だけでもこの課題は発生するので、これが翻訳対象言語の数だけ不整合が起こりうることを想像してみてください。Excelでは追いきれません。 3つ目の課題は開発側の人為的ミスが起こるリスクが高いことです。翻訳文言がExcelファイルに保存されている場合、最終的には手作業で1つずつソースコードにコピペする必要があります。(前回記事の例では「localizable.strings」ファイル)そのため、コピペミスや誤字脱字、さらには最終チェックを行うQAが適切に行われないと、バグが発生する可能性があります。 どのように解決したか これらの課題を解決するために、Excelでの翻訳管理を行わず、 Lokalise という専用のツールを導入することにしました。 _Lokalise_は翻訳タスクを管理できる翻訳管理システムで、すべての文言を1か所で集中管理できます。また、翻訳メモリ(過去にどう翻訳したかを記録するデータベース)の整備や、全てのプロダクトに適用できる用語集の作成ができるのも、このツールのメリットです。また、ソースファイルが保存されているGitHubに接続する機能もあり、開発者が開発した新しい画面や新しい機能のキーを「プル」し、その後、ターゲット言語の翻訳キーと文言のペアをGitHubにプルリクエストを作成して「プッシュ」可能です。 このようにプロセスを自動化することで、生産性が向上するだけでなく、開発者とローカライズチーム双方の作業時間を短縮することができます。さらに、一貫したクオリティを保ちながら、条件の整合性を確保し、正確にエンジニアに引き渡すことも可能です。_Lokalise_を使用するもう一つの大きなメリットは、Adobe XDなどのデザインツールから_Lokalise_に各用語のスクリーンショットをインポートできることです。これによって、翻訳者はツールで翻訳する際に視覚的に対象文言がどこにあるかわかるので、文脈などを考慮して翻訳することができます。情報量の少ないExcelシートにやみくもに入力するようなことはありません。 以下は、我々が開発しているGlobal KINTO App (GKA)の_Lokalise_導入前と導入後のフローチャートです。iOSとAndroidの両方で利用できるようにするため、ファイル変換にも対応する必要がありました。また、エクスポートする際のフォーマット変換も_Lokalise_が行ってくれるので、工数が節約できます。 Next Step 我々の次のステップとして、スケーラビリティを検討したいと考えています。ソースファイルの更新は_Lokalise_で自動化できましたが、各ファイルへのプルリクエストは1つずつ行うため、GitHubのリポジトリや製品の数が時間とともに増えていくと、すぐに重い作業になってしまいます。_Lokalise_とGitHubの中間的な存在として、社内で小規模な管理ツールを作成し、ソースファイルに必要な更新を伴うプルリクエストを一元管理できるようにすることを、解決策のひとつとして考えています。 また、QAプロセス強化の一環として、既存のブランドガイドラインと同様に、一連のスタイルガイドを作成する計画もあります。スタイルガイドによって、翻訳業者(LSP)に依頼した翻訳でも、翻訳者間、あるいは言語間のギャップなく、ブランドのトーン&マナーを維持したまま、わかりやすく翻訳することができるようになります。 また、世界各国のKINTOサービスに我々のローカライゼーションプロジェクトを_横展開_する可能性もあり、まだまだ今後のアイデアは尽きません。これらについては、後日別の記事などでお話するかもしれませんが、まずは、私のローカライゼーションの記事に興味を持っていただいてありがとうございます!翻訳や言語ローカライゼーション、他言語化というトピックに関して、少しでも心をくすぐれていれば何よりです。次にアプリをダウンロードしたり、動画配信サービスを利用したり、ゲームをする際には、多言語コンテンツがいかに技術的に複雑なことを行っているか、ぜひ注目してみてください。
アバター
KotlinエンジニアがFlutterに入門して1ヶ月でWebアプリケーションを作った話 こんにちは。Woven Payment Solution 開発グループの大杉です。 私たちのチームは、 Woven by Toyota において Toyota Woven City で使われる決済システムの開発を行っていて、普段はKotlin/Ktorによるサーバーサイドの開発をしています。 私たちは、Woven Cityを一緒に作っていく協力企業や社内のビジネスチームと協力してPoC (Proof of Concept, 概念実証)を繰り返し、 決済システムの機能を拡充させていっています。そして先日、第一弾として実店舗での小売販売を想定した決済システムのPoCを実施しました。 この記事では、私たちがPoCの中でクライアントアプリの開発にFlutterを採用するに至った経緯を紹介したいと思います。 はじめに PoCで小売販売をするために、決済システムの他に次のような店舗運営向けの機能開発も行いました。 店舗で販売する商品の管理 POSレジ 商品のスキャン ショッピングカート機能 店舗への売上報告と支払いの管理 棚卸し 特に、数万件に及ぶ商品情報の定期更新や月末の締日に合わせた売上報告、棚卸しを実施するためには、決済APIだけではなく、技術者ではない店舗担当者が作業できるGUIアプリケーションが必要でした。これが、普段はサーバーサイドの開発を行っている私たちが急遽クライアントアプリを開発に着手するに至ったきっかけです。 言語・フレームワークの選定 クライアントアプリを開発するに当たり、WebだけでなくiOS/Androidでもアプリ開発ができるクロスプラットフォームのフレームワークに候補を絞りました。 s 言語 / フレームワーク 選定理由 Dart / Flutter , Flutter on the web - 最近注目されているトレンドな技術である - 社内のモバイルアプリ開発チームでも採用されているため、チーム間の親和性が高い TypeScript / Expo (React Native) , Expo for web - Web開発に関しては最も成熟した技術の一つであるReactで開発できる - Reactの開発経験のあるチームメンバーが多く、キャッチアップに時間がかからない Kotlin / Compose Multiplatform , Compose for web - 採用事例がまだ少ないため、チャレンジングな開発ができる - 開発経験のあるチームメンバーはいないが、Kotlinを書ける人にとっては実装しやすそう 技術検証 言語・フレームワークを選定するために、クライアントアプリを開発する上で大事な要素である状態管理と画面遷移を組み合わせたWebアプリを作成して技術検証を行いました。 作成したアプリは、 + - ボタンを押下すると数値がカウントアップして、左の画面(Home Page)の next ボタンを押下すると右の画面(Detail Page)に遷移して数値を表示するというすごく単純なものです。 それぞれの言語・フレームワークの組み合わせについて、UIコンポーネントの実装方法、パフォーマンス、ライブラリ・ドキュメント・コミュニティサポートの点で開発体験の違いを見てみました。 UIコンポーネントの実装方法 まずは、上図右のDetail Pageのコードを例にFlutter on the web, Expo for web, Compose for webを比較してみました。 Dart / Flutter on the web DOMではなくオブジェクト指向なコンポーネントでUIを実装できるので、とても直感的だと感じています モバイルとWebでほとんど同じコードを利用できます スタイリングにはMaterial Designがデフォルトで適用されるので、一長一短ありますが、エンジニアがデザインもする必要がある状況ではとても助かります Canvaskitでレンダリングする場合、ほとんど同じ見た目のUIを描画することができます class DetailPage extends StatelessWidget { const DetailPage({super.key}); @override Widget build(BuildContext context) { final args = ModalRoute.of(context)!.settings.arguments as DetailPageArguments; return Scaffold( appBar: AppBar( title: const Text("Flutter Demo at Detail Page"), ), body: Center( child: ConstrainedBox( constraints: const BoxConstraints(minWidth: 120), child: Center( child: Text( args.value.toString(), style: const TextStyle(fontSize: 72), ), ), ), ), ); } } TypeScript / Expo Flutter同様に、DOMではなくオブジェクト指向なコンポーネントでUIを実装できるので、とても直感的だと感じています ただし、フレームワークが用意してくれるコンポーネントは最小限であるため、実装が必要です モバイルとWebでほとんど同じコードを利用できます スタイリングは、StyleSheetというCSSに似た記法でスタイリングしますが、適用されるスコープが限定されるためCSSほど辛くないです このサンプルの画面遷移の実装には react-navigation を使用しています const DetailPage: React.FC = () => { // from react-navigation const route = useRoute<RouteProp<RootStackParamList, 'Detail'>>(); return ( <View> <Header title={'Expo Demo at Detail Page'} /> <CenterLayout> <Counter value={route.params.value}/> </CenterLayout> </View> ); } const Header : React.FC<{title: String}> = (props) => { const {title} = props; return ( <View style={styles.header}> <Text style={styles.title}> {title} </Text> </View> ) } const CenterLayout: React.FC<{children: React.ReactNode}> = (props) => { const {children} = props; return ( <View style={styles.layout}> {children} </View> ) } const Counter: React.FC<{value: number}> = (props) => { const {value} = props; return ( <View style={styles.counterLayout}> <Text style={styles.counterLabel}>{value}</Text> </View> ) } const styles = StyleSheet.create({ header: { position: "absolute", top: 0, left: 0, width: '100%', backgroundColor: '#20232A', padding: '24px 0', }, title: { color: '#61dafb', textAlign: 'center', }, layout: { display: 'flex', flexDirection: 'row', justifyContent: 'center', alignItems: 'center', height: '100vh', }, counterLayout: { minWidth: 120, textAlign: 'center' }, counterLabel: { fontSize: 72, } }); Kotlin / Compose for web モバイルやデスクトップで扱うCompose UIではなく、HTMLのDOMをラッパーしたようなWeb専用のコンポーネントでUIを実装します モバイルとWebでコードの流用はできません スタイリングは、CSSで実装する必要があります コンポーネントに対してCSSのようなプロパティをコンポーネントに直接定義するか、StyleSheetオブジェクトとして切り出して実装します このサンプルの画面遷移の実装にはCompose multiplatformのWebとデスクトップ向けの routing-compose というライブラリを使用しています @Composable fun DetailPage(router: Router, params: Map<String, List<String>>?) { Div { components.Header(title = "Compose for web Demo at Detail Page") CenterLayout { params?.get("value")?.get(0)?.let { Counter(it.toInt()) } } } } @Composable fun Header(title: String) { H1(attrs = { style { position(Position.Fixed) top(0.px) left(0.px) paddingTop(24.px) paddingBottom(24.px) backgroundColor(Color("#7F52FF")) color(Color("#E8F0FE")) textAlign("center") width(100.percent) } }) { Text(title) } } @Composable fun CenterLayout(content: @Composable () -> Unit) { Div(attrs = { style { display(DisplayStyle.Flex) flexDirection(FlexDirection.Row) justifyContent(JustifyContent.Center) alignItems(AlignItems.Center) height(100.vh) } }) { content() } } @Composable fun Counter(value: Int) { Span(attrs = { style { minWidth(120.px) textAlign("center") fontSize(24.px) } }) { Text(value.toString()) } } パフォーマンス 次に、それぞれの言語・フレームワークで作ったサンプルアプリに対してビルド時間とバンドルサイズの比較をしました。 ビルド時の最適化オプションはデフォルトのものを使用しています。 検証環境は、MacBook Pro 2021 (CPU: M1 Pro, Memory: 32GB)です。 言語 / フレームワーク ビルド条件 ビルド時間 バンドルサイズ Dart / Flutter on the web - Flutter v3.7.7 - Dart v2.19.2 14s 1.7MB (CanvasKit) 1.3MB (Html) TypeScript / Expo for web - TypeScript v4.9.4 - Expo v48.0.11 10s 500KB Kotlin / Compose for web - Kotlin v1.8.10 9s 350KB Flutterで同機能を提供するために必要なバンドルサイズはReactのおよそ10倍であり、初回レンダリングにはかなり時間がかかってしまう可能性が高いことがわかります。 Flutterで生成されるJSコードについては、ビルドオプションに --dump-info を追加することで詳細を確認することができ、 主に、dartとFlutterのフレームワーク部分のコードが含まれていました。 ライブラリ・ドキュメント・コミュニティサポート 最後に、それぞれの言語・フレームワークについてライブラリ・ドキュメント・コミュニティサポートなどの情報をまとめました。 言語 / フレームワーク ライブラリ ドキュメント・コミュニティサポート Dart / Flutter on the web Flutter packages でFlutterで利用可能なライブラリを検索できる。 その中でも Flutter Favorite が付いているものは公式が人気があり使いやすいライブラリであることを提示してくれている。 公式ドキュメント や 動画 が充実しており、また、公式が状態管理などで推奨するライブラリや設計指針を提示してくれている。 TypeScript / Expo for web 基本的なライブラリはかなり充実しており、デファクトスタンダードなものも検索すると見つけやすい。各ライブラリのメンテナンスはコミュニティに依存している部分が大きいため、よく考えて選定する必要がある。 基本的な実装については Reactの公式ドキュメント や Expoの公式ドキュメント が充実している。ライブラリを含めた有効な設計指針については、 ネット上のReactの議論を参考にすれば大丈夫そう。 Kotlin / Compose for web JVMのライブラリ自体はかなり多い。ただし、AndroidやCompose UI関連のライブラリはCompose for webでは利用できない場合が多い。 ドキュメントはあまりないため、 GitHubリポジトリ を探すか、コミュニティの Slackチャンネル で情報を探す必要がある。 そして、Flutterを採用へ 上述した技術検証を元に、私たちはFlutterをPoCにおけるクライアントアプリ開発の技術スタックとして採用しました。 理由は以下の3点です。 クライアントアプリ開発に不慣れなメンバーであっても、ドキュメントや参考情報が充実しており、本業のサーバーサイド開発の工数を圧迫しにくいと予想 フレームワークの開発が活発でありながらもメンテナンス体制が整っているため、バージョンアップやライブラリの導入が容易であること PoCという特性上、通信環境が安定した環境で実行されるアプリであるため、パフォーマンスの欠点はあまり問題にならないこと また、後付けの理由になってしまいますが、Flutterだけでは解決できない課題に遭遇した際にDart上でJSを実行できることもとても心強かったです。 私たちのシステムではKeycloakを認証基盤に使用しており、Keycloakの公式からFlutter用のKeycloakのクライアント向けライブラリは提供されていないため、JS用ライブラリをDartで動作させて認証を行っています。 おわりに この記事では、PoCで使用するクライアントアプリの開発でFlutterを採用した経緯について紹介させていただきました。 現在、私たちはサーバーサイド開発と並行してクライアントアプリの開発も行っています。 今後さらに技術的な知見を深められたらこのブログで情報をアップデートしていきたいと思います。
アバター
こんにちは。 KINTO テクノロジーズの DBRE チーム所属の p2sk です。 DBRE(Database Reliability Engineering)チームでは、横断組織としてデータベースに関する課題解決や、組織のアジリティとガバナンスのバランスを取るためのプラットフォーム開発などを行なっております。DBRE は比較的新しい概念で、DBRE という組織がある会社も少なく、あったとしても取り組んでいる内容や考え方が異なるような、発展途上の非常に面白い領域です。 弊社における DBRE の取り組み例としては、あわっち( @_awache )による DBRE ガードレール構想の実現に向けた取り組みについて というテックブログや、 今年の AWS Summit の登壇内容 を是非ご覧ください。 今回の記事は、データベースに関する課題解決の事例として「Aurora MySQL でレコードが存在するのに SELECT すると Empty set が返ってくる」という不思議な事象を調査した話をご紹介します。 発生した事象 プロダクトの開発者から、「踏み台サーバーからデータベースに対して特定のクエリを実行すると挙動がおかしくなる」という問い合わせを受けました。データベースは Aurora MySQL 2.07.2 (MySQL 5.7.12)を使っており、 MySQL クライアントのバージョンは 5.7.38 for Linux (x86_64) でした。その時に共有してもらった挙動のイメージは以下の画像の通りです。 画像の中にある通り、レコードが存在しているテーブルに対して select * from t1; という全レコードを取得するクエリを実行したところ、 Empty set が返ってきます。また、その直後にクエリを実行すると ERROR 2013 (HY000): Lost connection to MySQL server during query  というエラーが返ってきます。さらにその後は ERROR 2006 (HY000): MySQL server has gone away No connection. Trying to reconnect... というエラーが返ってきました。それ以降は、以下の画像のように Empty set / ERROR 2013 / ERROR 2006 のループになります。 一方で、 select * from t1 limit 1; というクエリの場合は、期待通り 1 レコードが返ってきました。 この時点では原因について全く思い当たる節が無く、また別の環境で再現する方法も分からない状態でした。幸い、事象が再現するテーブルが複数あったので、様々な条件で再現の有無や事象解消の有無について調査を実施しました。 調査の実施 再現の有無を調査 以下のクエリ達は、事象が発生するクエリと取得対象のデータは同じ(全レコード、全カラム)ですが、全て問題なく結果が返ってきました。 select c1, c2 from t1; -- 全カラム指定 SELECT * FROM t1; -- 予約語を大文字にして実行 Select * from t1; -- 最初の1文字だけ大文字にして実行 他にも、以下のような確認を実施しました。 ライターインスタンスでは再現するが、リーダーインスタンスでは再現しない ライターインスタンスでも、同一データベース内で再現するテーブルと再現しないテーブルがある MySQL クライアントを 8.0 系に変えると再現しない テーブルを構成するカラムに特殊なものはなく、入っているデータもおかしい点は無さそう 事象解消の有無を調査 続いて、データやメタデータを変更することで事象が解消するかを調査しました。結果は以下の通りです。 再現するテーブルを対象に analyze table を実行しても、解消しなかった テーブルを新規作成し、ダンプファイルから同じデータを投入した場合、解消した 再現するテーブルのダンプファイルを作成後、 DROP & CREATE で同名のテーブルを再作成し、ダンプファイルからデータを投入した場合、解消した 再現するテーブルのレコードを全件 DELETE 後にダンプファイルから同じデータを投入した場合、解消した Aurora のアーキテクチャを踏まえた切り分けの実施 ここまでの調査ですと、テーブル再作成で解消するためデータに問題があるようにも見えますし、MySQL クライアントを 8.0 系に変えて解消するためデータには問題がないようにも見えます。そこで、Aurora のアーキテクチャを改めて確認しました。 こちらの AWS 公式資料 によると、以下のことが確認できます。 Aurora のコンピュートレイヤとストレージレイヤは完全に分離されている ライターインスタンスもリーダーインスタンスも同一のクラスターボリュームを参照する 最もわかりやすい図を下図に引用しました。 出典: Amazon Aurora アーキテクチャ概要 このアーキテクチャを踏まえて、コンピュートレイヤとストレージレイヤのどちらが関係していそうかを切り分けるために、 Aurora クローン を作成して再現の有無を確認しました。 クローンを作成しても、データはコピーされずにクローン元と同一のストレージを参照し続けます。 下図のように、どちらかのクラスタで新たなデータ更新が行われた時だけ新しいデータページが作成されますが、更新が行われない限り、ストレージレイヤに変更はありません。 出典: Aurora クローン作成の仕組み 作成したクローンに接続して同様のクエリを実行したところ、事象が再現しました。したがって、ストレージレイヤは今回の問題とは無関係な可能性が高いと判断しました。ライターインスタンスでは再現するが、リーダーインスタンスでは再現しないという結果もこの判断を補強してくれそうです。 ということで、Aurora のコンピュートレイヤが今回の事象に関連していると推定しました。コンピュートレイヤが保持している何らかのデータに関連があると考え、改めてアーキテクチャ図を確認したところキャッシュ機構の関連性を疑いました。 現在の設定がどうなっているかを以下のクエリで確認したところ、クエリキャッシュは有効化されていました。 select @@session.query_cache_type; そこで、以下のようにクエリキャッシュをセッションレベルで無効化したときに事象が再現するか確認しました。 set session query_cache_type = 1; -- クエリキャッシュON select @@session.query_cache_type; -- 確認 SELECT * FROM t1; -- 再現しなかった select * from t1; -- 再現した set session query_cache_type = 0; -- クエリキャッシュOFF select @@session.query_cache_type; -- 確認 SELECT * FROM t1; -- 再現しなかった select * from t1; -- 再現しなかった(!) ということで、クエリキャッシュを無効化すると事象が再現しなくなることが確認できました。MySQL 8.0 ではクエリキャッシュは廃止されているため、8.0 系のクライアントを使うと事象が再現しなかったという結果も納得できます。 また、クエリキャッシュを RESET すると、クエリキャッシュを ON にしていても再現しなくなりました。ちなみに FLUSH QUERY CACHE だと引き続き再現しました。 RESET でキャッシュを削除してあげる必要があるようです。 set session query_cache_type = 1; -- クエリキャッシュON select @@session.query_cache_type; -- 確認 RESET QUERY CACHE; -- クエリキャッシュのリセット SELECT * FROM t1; -- 再現しなかった select * from t1; -- 再現しなかった これまでの結果から、今回の事象はクエリキャッシュに関連していることが分かりました。 似た事例の調査 原因の切り分けが進んだところで、似た事例が報告されていないかを調査しました。その結果、 こちら のバグレポートに辿り着きました。内容としてはバグレポートのタイトル「Query caching with two different clients causes errors」にある通り、2種類のバージョンの MySQL クライアントを使うと、片方のバージョンでキャッシュされた内容にもう片方からもアクセスしようとしてエラーになる、というものです。 こちらのレポートをもとに事象を再現できるか試したところ、バージョン 5.6.35 と 5.7.38 を使った場合に再現することができました。再現手順を記事末尾の appendix に記載しておりますので、ご興味のある方はお試しください。(appendix では 5.7.41 を使っていますが、再現します。) 異なるバージョンの MySQL クライアントを使用した可能性について、担当者に確認したところ「踏み台サーバーを新しく構築したタイミングで事象が発生するようになった」ということが分かりました。以前使っていた踏み台サーバーの MySQL クライアントまでは分からなかったので断定はできませんが、バグレポートの内容と起きている事象は同じです。したがって、今回の原因は異なる MySQL クライアントで select * from t1 というクエリが実行されてキャッシュされたことで、エラーにつながった可能性が非常に高いと判断しました。 対応策の検討 事象が発生してしまった場合は、 RESET QUERY CACHE を実行するのが最も簡単な解消方法ですが、そもそも事象が発生しなくなる方法についても検討しました。 試しに Aurora MySQL のバージョンを 2.07.2 からバージョンアップした時の発生有無について調査しました。その結果、2.07.x 系の最新パッチバージョンである 2.07.9 だと引き続き事象が再現しました。しかし、マイナーバージョンも上げて 2.11.x 系で試したところ、 2.11.1 でも 2.11.2 でも事象が発生しなくなりました。マイナーバージョンアップに伴って、クエリキャッシュに関連した何らかの修正が入った可能性があります。したがって予防策としては Aurora のバージョンを 2.11.x 系にバージョンアップすると良さそうです。 まとめ 本記事では、 DBRE 活動の一環として行なっているデータベースに関する課題解決の事例として「Aurora MySQL でレコードが存在するのに SELECT すると Empty set が返ってくる」という不思議な事象を調査した話をご紹介しました。原因は、クエリキャッシュが有効化された Aurora MySQL 2.07.x 系に対して異なる MySQL クライアントから同じクエリを実行すると結果がおかしくなるという MySQL のバグ によるものでした。事象発生時の解消方法としては(一時的なパフォーマンス劣化に注意は必要なものの) RESET QUERY CACHE を実行するのが最も簡単な方法です。また、Aurora 2.11.x 系では事象の発生が確認できなかったため、Aurora のバージョンアップを実施するのが最も確実な対応かと思います。もしくは 2024 年 10 月 31 日には Aurora 2 系がサポート終了となるため、早期に Aurora 3 系へとバージョンアップするのも手かと思います。 そもそもかなりレアケースですので、あまり気にする必要はないかもしれませんが、どなたかの参考になれば幸いです。なお、今回の調査はいろいろな方のご協力によって行うことができました。 KINTO テクノロジーズ DBRE チームでは一緒に働いてくれる仲間を絶賛募集中です!カジュアルな面談も歓迎ですので、 少しでも興味を持っていただけた方はお気軽に Twitter DM 等でご連絡ください。併せて、 弊社の採用 Twitter もよろしければフォローお願いします! Appendix : 再現手順 以下の再現手順は、OS が Amazon Linux 2 の踏み台サーバー上で動作することを確認しています。また、Aurora MySQL のバージョンは 2.07.x 系であることを前提とします。(全てのパッチバージョンで再現するかは確認しておりませんが、少なくとも最新のパッチバージョン 2.07.9 での再現は確認しています。) まず、踏み台サーバーに接続し、MySQL 5.6 系のクライアント(5.6.35)をインストールします。 sudo mkdir -pvm 2755 /usr/local/mysql-clients-56; sudo curl -LO https://dev.mysql.com/get/Downloads/MySQL-5.6/mysql-5.6.35-linux-glibc2.5-x86_64.tar.gz; sudo tar -zxvf mysql-5.6.35-linux-glibc2.5-x86_64.tar.gz -C /usr/local/mysql-clients-56/; cd /usr/local/mysql-clients-56/; sudo mv -v mysql-5.6.35-linux-glibc2.5-x86_64 mysql56; sudo ln -s /usr/local/mysql-clients-56/mysql56/bin/mysql /usr/local/bin/mysql56 次に、MySQL 5.7 系のクライアント(5.7.41)をインストールします。 sudo mkdir -pvm 2755 /usr/local/mysql-clients-57; sudo curl -LO https://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.41-linux-glibc2.12-x86_64.tar.gz; sudo tar -zxvf mysql-5.7.41-linux-glibc2.12-x86_64.tar.gz -C /usr/local/mysql-clients-57/; cd /usr/local/mysql-clients-57/; sudo mv -v mysql-5.7.41-linux-glibc2.12-x86_64 mysql57; sudo ln -s /usr/local/mysql-clients-57/mysql57/bin/mysql /usr/local/bin/mysql57 そのまま MySQL56 でデータベースに接続します。 mysql56 -h xxx -u xxx -p サンプルのデータベースとテーブルを作成し、データを INSERT します。 create database d1; use d1; create table t1 (c1 int, c2 int); insert into t1 (c1, c2) values (1, 1); insert into t1 (c1, c2) values (2, 2); insert into t1 (c1, c2) values (3, 3); クエリキャッシュをセッションレベルで有効化し、クエリを発行してキャッシュさせます。 set session query_cache_type = 1; select * from t1; 次に、別のウインドウから同一の踏み台サーバーに接続し、 MySQL57 でデータベースに接続します。 mysql57 -h xxx -u xxx -p クエリキャッシュをセッションレベルで有効化します。 use d1; set session query_cache_type = 1; MySQL56 から実行したクエリと 1 文字違いのクエリを実行すると、正常にデータが返ってきます。 Select * from t1; MySQL56 から実行したクエリと同じクエリを実行すると、 Empty set が返ってきます。 select * from t1; これで事象が再現できました。 解消するためには、クエリキャッシュをリセットします。 RESET QUERY CACHE;
アバター
Hi, KINTOテクノロジーズのFloです! 本日の記事では、先日社内で初めて開催したハッカソンイベント、Innovation Daysで優勝したチームにインタビューをします!🥳 背景 Global KINTO Innovation Daysは、2022/12/14~21にかけて開催されたハッカソンのようなチームビルディングイベントです。グローバル開発Gのメンバー30人が6チームに分かれ、チームワークを高めつつ新しいアイデアを生み出すことを目的にしていました。 前回・前々回の記事で 本イベントの準備 についてや 本イベント当日の様子 についても読めますので、ぜひそちらもご参照ください。 はじめに まずはじめに、優勝チームである United Nations チームを紹介します。 (左から時計回りに)アンワル、モジ、クリス、アンジェラ、ドゥック アンワル :自動車業界で5年間事業開発マネージャーを務め、現在はKINTOテクノロジーズのグローバルビジネス開発に携わる。 モジ :UI/UXプロダクトデザイナー。主にユーザーファーストのプロダクト開発を重視し、クロスファンクショナルに他のチームと連携しながら、ビジネスゴールとユーザーニーズの双方を満たすようなデザインを行う。 クリス :フルスタックエンジニアで、KINTOテクノロジーズでは主にフロントエンドエンジニアとして従事。 アンジェラ :DevOpsチームに所属。5年以上のクラウド経験とシステムアーキテクチャ経験2.5年以上を持つクラウドエンジニア。15年以上のシステム開発経験がある。 ドゥック :IDプラットフォームチームに所属するバックエンドエンジニア。フロントエンドの開発経験もある。 インタビュー開始! Q01. Innovation Daysに参加した理由は何ですか? アンワル :エンジニアとの協力し、共に学びながら、会社に取って意味のあるモノを開発するという、滅多にない機会だと思ったからです。 モジ :デザインの課題に触れつつ、モビリティの新しいソリューションの開発に貢献したいと思ったからです。 クリス :KINTOに対して何か新しいものを作れる良い機会でしたし、新しい技術やアイデアを試してみたいという気持ちもありました。 ドゥック :このようなイベントに参加するのは初めてだったので、面白そうだと思いました。 アンジェラ :他チームメンバーとのコミュニケーションを増やしたかったのと、新しいことに挑戦してみたいという気持ちから参加しました。 Q02. チーム名”United Nations”の由来は? クリス :国籍やバックグラウンドなどバラバラなチームだったので「United Nations」と名付けました。(他のチームもほとんど同じ感じですが😅) Innovation Daysでマネージャーからフィードバックを受ける様子 Q03. 技術スタックとメンバーの役割・責任について教えてください。 クリス :運営チームがメンバーの経歴などを見ながら、それぞれのチームに分けたので、バックエンドやインフラなどの役割はほぼそれに従いました。私はフロントエンド開発を担当し、Nuxt.jsを利用しました。グローバル開発Gで利用していることと、私自身がこのメタフレームワーク経験が豊富だったためです。 ドゥック :私はバックエンドを担当しました。Pythonは普段から最も一般的な言語で、また私にとっても使いやすいので、これを使うことにしました。 アンジェラ :私はプラットフォーム担当でした。KINTO社内で使われているもので、プロジェクト関連の機能が備わっていたので、環境はAWSを選定しました。 モジ :最終ピッチ時のプレゼンデッキを担当しました。2日間しかなかったので、我々はモックアップのデザインより、開発プロセスやプレゼンデッキを優先することにし、限られた時間の中で調査結果やアイデアのゴールを効果的に伝えるため、Figmaを使ってプレゼンデッキを作成しました。また、バリュープロポジションを作るのはMiroボードで共同作業し、ユーザーとビジネス両方のニーズをさまざまな角度から総合的に検討することができました。 アンワル :私はビジネスモデル開発がメイン担当で、このハッカソンで企画するプロジェクトが市場ニーズに合うか、実現可能なビジネスモデルか、さらにステークホルダーに対して効果的なアプローチができるかなどを検討しました。チームメンバーが自由に意見を出し合い、それぞれ期待された役割以上のものを手際良くこなしたことがポイントだったと思います。 Q04. チームには英語と日本語を話す人がいますが、コミュニケーションはどうでしたか? アンワル :ほとんどのメンバーが英語と日本語の両方を話せるので、基本はどちらかで話しましたが、わからないことがあれば都度、誰かが通訳して全員が同じ理解でいることを確認しました。 モジ・アンジェラ :そうですね、わからない部分があれば逐一声を上げ、チームの誰かが説明してくれました。 Q05. 企画・設計フェーズはどんな感じでしたか? アンワル :とにかく楽しかったです。本番前、4回しかミーティングできませんでしたが、メンバーそれぞれ違うアイデアを出し合ってスムーズに進めることができました。 モジ :時間の制約を考えると、そこまで深くアイデアを練ることはなかったですね。急いで行うタスクが多く、メンバーがヘルプを求めた際には他の誰かがサポートするようにしていました。 クリス :本番前のチームミーティングは4回ほどしかありませんでした。他のチームではどうだったかわからないが、私たちには足りなかったです。 アンジェラ :それが逆によかったのかもしれません。 クリス :そうですね、限られた時間を最大限に活用しました。 アンワル :良かったのは、全員がいろいろなアイデアを出してくれて、それをひとつにまとめようとしたことです。例えば、モジはkudos機能(Likeのようなもの)、アンジェラはGoogle プラットフォームについてアイデアを持っていて、それをどう組み合わせるかを議論しましたね。こうやって私たちのプロジェクトは始まりました。 ドゥック :そうそう、考えたアイデアをすべて実現したかったんです。 クリス :最初は多数決でアイデアを選定するつもりだったのですが、1つに絞るのはなかなか難しかったので、アイデアをいくつか組み合わせることにしました。 Q06. 今回のイベントで最も驚いたことは何ですか? アンワル :メンバー同士のコミュニケーション能力ですね。私たちのチームダイナミクスは、とても良いバランスだったと思います。 クリス :私は、みんなのモチベーションと協調性の高さに驚きました。モジも言っていた通り、一人ひとりが主体性をもって、それぞれのタスクに取り組んだことが効いたと思います。 アンジェラ :普段は一緒に仕事をしないメンバーだったので、私としてはチームメンバーそれぞれの強みを持っていたことにびっくりしました。 ドゥック :メンバーのみんながとてもエネルギッシュだったので、本当に驚きました。普段のチームはバックエンドがメインになるので、雰囲気が違います。 Innovation Daysにてグループマネージャーにアイデアをプレゼンしている様子 Q07. 今回、一番大変だったのは何でしょう?また、それをどう越えましたか? クリス :作りたいものがたくさんあったので、2日間という短時間で何を開発するか絞るのが大変でした。実際にどの機能を開発するか、また、プレゼンの際にはビジュアル的にどの部分を見せるか、などの優先順位をつけなければいけませんでした。 モジ :ターゲットが若年層で、近年のソーシャルメディアを活用したコミュニケーションを念頭に、考えているビジョンを実現できる技術があることもアピールしたかったです。 クリス :そうですね、内部ではやったことのない技術を導入したかったので、それもチャレンジでした。 アンジェラ :そのために、チームからフィードバックをもらいつつ技術的な分析を行い、自分の持つ知識と組み合わせて、機能的に満足できるまでテストを繰り返しました。 クリス :あとは、本業でアサインされているチームやプロジェクトがバラバラなので、全員が集まって議論する時間がなかったことも大変でした。 アンジェラ :時間が限られているので、次の会議までに各自が行うアクションアイテムを整理し、次の会議でフォローする、という流れにしていました。 ドゥック :技術以外の部分でお話すると、Innovation Daysは神保町オフィスで開催されたのですが、会議室の数に限りがあり、広さもまちまちでした。運営チームは会議室をくじ引きで割り当てましたが、我々チームは一番狭い部屋になってしまいました。 (ごめんね😅💦 by 運営チーム) モジ :そう、一番狭い部屋だったのですが、こればっかりはどうしようもなかったですね。笑 アンワル :そうですね、お互いにサポートし合って、できるだけ心地よく過ごせるよう、最善を尽くしたつもりです。笑 クリス :もうひとつ、大きなチャレンジは、アンワルが残念ながら一身上の都合でイベントの第2部まで参加できないことでした。オフラインでは一緒にいられませんでしたが、都度Slackでステータス共有したり、常にアンワルをループに入れ続けることでなんとか乗り越えました。 Q08. 優勝する自信はありましたか? アンワル :そうですね、おそらくイベント参加者全員が同じような勝利のスピリットを持って参加していたと思います。私たちは 「We win as a team or we lose as a team (チームとして勝つか、チームとして負けるか)」 というモットーを掲げ、それを貫いてきました。 モジ :勝つことよりも、ネットワークを作り、コミュニケーションをとり、楽しみながら、既成概念にとらわれないクリエイティブな発想に挑戦することに重点を置いていたと思います。 ドゥック :他のチームのアイデアもとてもよかったと思いますが、私たちのアイデアには誇りを持っています。 チームランチ! Q09. このような短期間のアイデア検討と開発で学んだこと:今回の経験から今後活かしたいことはありますか? クリス :全体として大きく変えることはないですが、*"Keep, Problem, Try Retrospective"* と、振り返りは行いました。当時は時間が足りないことに問題があると思いましたが、これは計画の課題でもあります。おそらく、時間が足りないことが課題なのではなく、限られた時間をうまく使えるようにもっと事前に計画を立てることが必要なんだと思います。 Q10. ハッカソンなどのこういったイベントは、PoC的なものですが、実際に日々取り組んでいるプロジェクトとはどういった違いがありますか? モジ :先ほども言った通り、時間的な制約やリソースが違いました。 アンワル :私にとっては、普段関わらなかったメンバーのバックグランドを知って、有意義な意見交換ができたことですね。普段とは違って新鮮に感じたし、ワークしやすかったです。 アンジェラ :上下関係がなく、全員がフラットな関係でそれぞれが得意とすることができたことが要因だと思います。 クリス :アンワルと自分がリーダーの役目を途中で交代したのですが、正直このロールは必要なかったですね。 アンワル :そうですね、必要次第でみんながリードしていました。みんながリーダーです! クリス :そうですね、共和国みたいな感じで。 Q11. どういったポイントが優勝につながったと思いますか? モジ :協調性ですね、間違いなく!メンバー全員がとても個々のタスクをうまくこなし、互いに協力し合ったことが結果に繋がったと思います。クリスは「selfless extra effort(無償の努力)」という表現を使いましたが、これは単に仕事を引き受けるだけでなく、何がベストかを研究し、アイデアや意見をチームと共有するという精神をうまく表してくれたと思います。 クリス :我々はWebサービスを開発したのですが、それぞれの得意分野がうまく活かせました。あと、メンバー一人ひとりが自分の意見や考えを積極的に発言することに加え、相手の意見にも耳を傾け、尊重していたことも挙げられると思います。会議中は意見が一致することもあれば反発することもありましたが、最終的には必ず結論を出して、前に進ことができました。 アンジェラ :そうですね、メンバーに対する尊敬と信頼が大きな勝因でした。普段は違うチームなので、準備期間中に違うトピックで会議するのですが、次の会議までのTo Doを決めた際にはそれぞれがそのタスクをやってきてくれると信じていました。 ドゥック :また、KINTOテクノロジーズでは今までなかったアイデアだったので、それを実現した方法などもとてもよかったように思います。 Q12. 最後に、こうしたハッカソン等のイベントに参加したいと思っている方々へアドバイスをお願いします。 クリス :私がアドバイスするならば、迷わず新しいことに挑戦し、チームメイトとのコミュニケーションを大切にすべき、という点ですかね。 アンワル :また、あまり形式ばらないほうがいい気がしますね。 (特に日本では形式や型を守りがちなので )。より効果的に、そして 本音で コミュニケーションをとるために堅苦しさを捨てるべきですし、今回私たちがカジュアルな感じでなければ、こういった結果にはならなかったと思います。 アンワルは授賞式には出席できず。。 参考 今回のインタビューが、読んでくださった方々がアイデアソンやハッカソンに参加するきっかけとなったなら嬉しいです! また、Toyota Motors North Americaが主催したハッカソンにグローバル開発Gが参加した際のレポート記事もぜひご覧ください: TMNA Swarm Hackathon参加レポート また、グローバ開発Gにご興味のある方は以下の記事をご参照ください: グローバル開発グループ Pt.1 グローバル開発グループ Pt.2 グローバル開発グループ Pt.3
アバター
こんにちは、KINTO Technologiesグローバル開発部でフロントエンド開発をしているクリスです。 普段フロントエンド開発でコンポーネントを開発する際はpropsを利用して必要な情報を渡す、という話はよく耳にすると思います。Angular, React, Vue, Svelteといった今よく使われているフレームワークではそれぞれの書き方でこの機能を実現しています。すべてのフレームワークを言及すると非常に長い記事になってしまうので、今回はグローバル開発部で良く使っているVueについて話したいと思います。 コンポーネントの再利用性を考える時に、propsだけでは実現しにくい可能性があります。そこで登場するのがslotという機能です。本記事は両者について説明し、利用事例を比較したいと思います。 Propsで情報を渡す 例えば、タイトルがついているテーブル情報を再利用できるコンポーネントとして実装する必要があるとします。タイトル、ヘッダーとデータを渡すためにそれぞれのpropsを渡せば、やりたいことがすぐ実現できます。 # コンポーネント <template> <div> <h5>{{ title }}</h5> <table> <tr> <th v-for="header in headers" :key="header">{{ header }}</th> </tr> <tr v-for="(row, i) in data" :key="i"> <td v-for="(column, j) in row" :key="`${i}-${j}`">{{ column }}</td> </tr> </table> </div> </template> <script> export default { name: 'DataTable', props: { title, headers, data }, } </script> # コンポーネントを呼び出す親 <template> <DataTable :title="title" :headers="headers" :data="data"/> </template> <script> // コンポーネントのimport文を省略します export default { data() { return { title: 'Title', headers: ['C1', 'C2', 'C3', 'C4'], data: [ { c1: `R1-C1`, c2: `R1-C2`, c3: `R1-C3`, c4: `R1-C4`, }, { c1: `R2-C1`, c2: `R2-C2`, c3: `R2-C3`, c4: `R2-C4`, }, ] } }, } </script> 上記のコードで以下のテーブルを作成することができます。(CSSによる簡単なスタイリングをつけていますが、本記事と関係ないため割愛します。) Propsの利用について少し補足すると、Vue.jsではTypeScriptを利用しなくても、簡単な型チェックができたり、呼び出し元からもらったデータに対してバリデーションをかけられたりします。以下が例になりますが、詳しくはVue.jsの 公式ドキュメント から確認してみてください。(本記事の全サンプルコードにこのような設定をつける長くなってしまうため、割愛します) <script> export default { props: { title: { // String型のprop。二つ以上の型があり得る場合は[String, Number]などで書きます type: String, // このpropは必ず呼び出し元から渡してもらう必要があります required: true, // propのバリデーションチェック。Booleanを返すことで結果を判定します validator(value) { return value.startsWith('Title') } }, }, } </script> Propsのみを利用する際の問題点 型の指定、値のバリデーションなどの機能がついているpropsは確かに便利ですが、やりたいことによっては物足りないと感じてしまう時があります。例えばこのような要件を聞いたことありませんか? テーブルセルに表示している値を条件によって太文字だったり、斜体だったり、テキストの色を変えられるようにする テーブルの各行に一つ以上のアクションを起こすボタンを表示できるようにし、条件によってdisabledできるようにする 聞くと普通に納得できそうな要件ですが、propsのみで実現しようとすると、複雑なコードになりがちです。 セルのスタイルを変更するには、判定のロジックもpropsとしてコンポーネントに渡すようにするか、この値はスタイル変更が必要というマーキングをデータオブジェクトに追加する必要があり、データ行ごとにボタンをつけるには以下のようにボタンの情報をpropsとしてコンポーネントに渡す必要があります。 例えば最初に出したサンプルコードを追加実装すると、以下のようなコードになります。 <template> <div> <h5>{{ title }}</h5> <table> <tr> <th v-for="header in headers" :key="header">{{ header }}</th> </tr> <tr v-for="(row, i) in data" :key="i"> <!-- 受け取ったスタイルを判定する関数でクラス情報を取得 --> <td v-for="(value, j) in row" :class="cellStyle(value)" :key="`${i}-${j}`" > {{ value }} </td> <!-- ボタンがある場合はボタンの列を別途用意 --> <td v-if="buttons.length > 0"> <button v-for="button in buttons" :class="button.class" :disabled="button.disabled(row)" @click="button.onClick(row)" :key="`${button}-${i}`" > {{ button.text }} </button> </td> </tr> </table> </div> </template> <script> export default { props: { title, headers, data, // セルのスタイルを決めるロジックをpropsとして受け取る cellStyle, // ボタン情報をpropとして受け取る buttons, }, } </script> <template> <!-- クラス情報を返す関数とbuttonsに関する情報をpropsとして渡す --> <DataTable :title="title" :headers="headers" :data="data" :cell-style="cellStyle()" :buttons="buttons" /> </template> <script> export default { data() { return { // その他のdata情報を省略 buttons: [ { text: '編集', class: 'btn-primary', disabled: (rowData) => { // ボタンをdisabledにするかどうかの判断ロジック }, onClick: (rowData) => { // ボタンの押下後ロジック }, }, { text: '削除', class: 'btn-danger', disabled: (rowData) => { // ボタンをdisabledにするかどうかの判断ロジック }, onClick: (rowData) => { // ボタンの押下後ロジック }, }, ], } }, methods: { cellStyle() { return (val) => { // 必要なスタイルクラス情報を返すロジック } } } } </script> こちらのスクショはボタンの表示とともに、条件に応じてセルのテキストにスタイルをかけたり、表示したボタンをdisabledにするロジックを入れた結果になります。 ただ、もしさらにセル内のhtml構造そのものを制御したい場合(例: <p> タグ、 <span> タグ、 <li> タグなどを入れる)、 htmlコードを文字列のpropsとして子コンポーネントに渡し、v-htmlを使って表示する必要があります。 v-htmlというのも便利なやり方ですが、htmlコードを文字列で構築するため、たくさんの動的な要素を入れると読みづらくなってしまいます。 上記の話をまとめると、propsのみを利用する場合は 子コンポーネントとしてどう受け取るか を深く悩む必要があります。 Slotでpropsの足りない部分を補う そこでslot機能の出番です。 公式ドキュメント にもこの機能の説明がありますが、コンポーネントにslot枠を作成し、呼び出し元から指定したtemplate枠内のhtml情報を渡すことによって、実装したい内容を対象のslot枠に当てはめることができます。 上記のイラストはあくまでイメージですが、左側の箱はpropsを利用したコンポーネントで、右側の箱はslotを利用したコンポーネントです。 Propsの場合、各入口が狭く、型も決まっているため、実装者からはコンポーネントが決めた情報しか渡せないイメージですが、slotの場合は入口がだいぶ広くなるため、何をコンポーネントに渡すかは実装者がより決定権を持っています。 例えば、前半で例としてあげたデータテーブルの実装でslotを使ってみるとします。 <template> <div> <!-- default slot --> <slot /> <!-- tableというslot --> <slot name="table" /> </div> </template> <template> <DataTable> <!-- コンポーネントの中にhtmlコードを書くと、自動的にコンポーネント側で宣言されたslotに当てはめます --> <!-- 特にtemplateで囲まなければdefaultのslotに当てはめます --> <h5>Title</h5> <!-- tableというslotに当てはめます --> <template #table> <table> <tr> <th v-for="header in headers" :keys="header">{{ header }}</th> </tr> <tr v-for="(row, i) in data" :keys="`row-${i}`"> <td v-for="(column, j) in row" :class="{ 'font-italic': italicFont(column), 'font-weight-bold': boldFont(column) }" :key="`row-${i}-col-${j}`" > {{ column }} </td> <td> <button :disabled="editDisabled(column.c1)" @click="edit(column.c1)">編集</button> <button :disabled="destroyDisabled(column.c1)" @click="click(column.c1)">削除</button> </td> </tr> </table> </template> </DataTable> </template> <script> export default { // data情報を省略 methods: { edit(id) { // その行のデータを編集するロジック }, destroy(id) { // その行のデータを削除するロジック }, italicFont(val) { // 斜体にする判断ロジック }, boldFont(val) { // 太文字にする判断ロジック }, editDisabled(id) { // 編集ボタンをdisabledする判断ロジック }, destroyDisabled(id) { // 削除ボタンをdisabledする判断ロジック } }, } </script> この例でいうと、コンポーネントにpropsを渡しておらず、かなりすっきりして見えますが、一つ問題があります。それは、実装者の好きなようになんでも実装できてしまうことです。例えば上記のコンポーネントを利用すると、親ファイルで以下のように指定のタグ(タイトルはh5タグ、テーブルはtableタグなど)を利用し、適切なスタイルをつけるべきなのに、実装者への共有不足、もしくは実装の知識不足で別のタグを利用してしまう可能性があります。そうなると、実際の見た目では違く見えてしまうかもしれませんし、テストの際に各画面サイズで崩れていないか再確認する必要があります。 <template> <DataTable> <!-- h5ではなく、h1を利用 --> <h1>Title</h1> <template #table> <!-- <table>, <tr>や<th>を利用せず<div>を利用 --> <div> <div> <div v-for="header in headers" :keys="header">{{ header }}</div> <div></div> </div> <div v-for="(row, i) in data" :keys="`row-${i}`"> <div v-for="(column, j) in row" :key="`row-${i}-col-${j}`"> {{ column }} </div> <div> <button :disabled="editDisabled(column.c1)" @click="edit(column.c1)">編集</button> <button :disabled="destroyDisabled(column.c1)" @click="click(column.c1)">削除</button> </div> </div> </div> </template> </DataTable> </template> テーブルなのにすべて <div> タグを利用するとは極端な例かもしれませんが、必要以上な自由度は与えない方が無難です。会社やチームによって解釈が違いますが、私にとっての理想は、デザイナーと相談した上で、必要な部分だけ自由を与えることです。どの部分が実装時の仕様に応じて自由に実装してもらっていいか、どの部分が必ず一つのやり方に従ってもらわないといけないかを決めてから、propsの利用や、slotを使い分けるべきと思います。 <template> <div> <!-- 必ずテキストを<h5>に入るようにpropsを利用 --> <h5>{{ title }}</h5> <!-- 必ず<table>タグを利用 --> <table> <tr> <!-- header情報をpropsで渡すことで、必ず<th>を利用する --> <th v-for="header in headers" :key="header">{{ header }}</th> </tr> <!-- 渡されたデータの行数に応じて動的にslotを生成 --> <!-- v-bindを利用して、呼び出し元のtemplateにデータを渡す --> <slot name="table-item" v-for="row in data" v-bind="row" /> </table> </div> </template> <script> export default { data() { return { title, headers, data } } } </script> <template> <!-- タイトルとヘッダーはpropsで渡す --> <DataTable title="Title" :headers="headers" :data="data"> <!-- コンポーネント側のv-bindされたデータを受け取る --> <template #table-item="row"> <!-- 行のデータを受け取り、表示方法を定義 --> <tr> <td v-for="(column, i) in row" :key="`col-${i}`"> {{ column }} </td> <td> <button :disabled="editDisabled(column.c1)" @click="edit(column.c1)">編集</button> <button :disabled="destroyDisabled(column.c1)" @click="click(column.c1)">削除</button> </td> </tr> </template> </DataTable> </template> ちなみに、slotを利用する際に、コンポーネント側で this.$scopedSlots を利用することで、どのslotが呼び出し元に利用され、またどのように利用されているか確認することができます。ユースケースは様々ありますが、例えば利用されているslotの中でどんなタグが利用されているか調べることができます。これは先述したslotの自由度が高すぎる問題に対して、一種のマイルドなバリデーションをかけることが可能になります。 <template> <DataTable title="Title" :headers="headers" :data="data"> <template #table-item="row"> <!-- <tr>ではない場合は何かしらの方法で実装者にお知らせするなど --> <div> <td v-for="(column, i) in row" :key="`col-${i}`"> {{ column }} </td> </div> </template> </DataTable> </template> まとめ 最後のまとめですが、Vueによる再利用コンポーネントの開発ではpropsを利用するのが一番簡単なものの、本記事にある事例のように柔軟性が欠けています。一方、slotを利用すると、実装者がより自由に実装できますが、様々な理由で、想定しなかった実装方法を利用したことによって、品質担保ができなくなる可能性があります。 そこで、コンポーネントの開発関係者を交えてあらかじめコンポーネントのどの部分にどのレベルの自由を与えるかを決めた上、与えてもいい自由度に合わせてpropsとslotを使い分けて開発し、コンポーネントの利用者に対してもドキュメントなどを通して、仕様を理解してもらったほうがいいと思います。
アバター
こんにちは(こんばんは)、Svelte不定期連載その2です。 過去の記事はこちら SvelteKit + Svelte を1年間くらい使ってみた知見など※SvelteKit メジャーリリース対応済み Svelteと他JSフレームワークとの比較 - Svelte不定期連載-01 今回はSvelteのユニットテストについて書いていこうと思います。 モジュールはこちら。 Vitest + jsdom + @testing-library/svelte の3つを使用して行います。 Vitest viteというツールを使ったテストフレームワークです。 viteを使用しているため非常に高速に動作します。 https://vitest.dev/ jsdom Node.jsでDOMを使うライブラリです。 HTMLをパースし、web APIをコールすることができます. https://github.com/jsdom/jsdom @testing-library 様々なフレームワークをサポートしているテストライブラリです。 Svelteだけではなく、ReactやVueなどももちろんサポートしています。 https://testing-library.com/docs/svelte-testing-library/intro/ 環境設定 まずは以下でモジュールたちを追加します。 ※パッケージマネージャーはお好みで、今回はyarnで行います。 yarn add vitest jsdom @testing-library/svelte @types/jest ※今回はTS(TypeScript)で行うので @types/jest も追加します。testファイルにも型を追加したいためです。 無論、TSで書かれている場合は必要ありません。 config 次はvite.config.jsにtest用の記述を追加します。 import { sveltekit } from '@sveltejs/kit/vite'; /** @type {import('vite').UserConfig} */ const config = { plugins: [sveltekit()], // ここから下を追加 test: { // testの対象ファイル include: ['src/**/*.{test,spec}.{js,ts}'], globals: true, // testの環境 environment: 'jsdom' } }; export default config; jsdomは environment で設定しています。 package.json package.jsonにも以下を追記します。 ※書かなくても yarn vitest で実行できます。 "test": "vitest" これでテストの準備ができました。 実際にテストをしてみよう よくある加算減算ボタンのあるコンポーネントでテストを書いていこうと思います。 コンポーネント側 まずテストしたいコンポーネントを用意します。 <script lang="ts"> let count:number = 0; </script> <!-- 減算するボタン --> <button on:click={() => (count -= 1)} aria-label="減算">-</button> <!-- 定義したcount変数 --> {count} <!-- 加算するボタン --> <button on:click={() => (count += 1)} aria-label="加算">+</button> testing-libraryの方で加算・減算とそれぞれ読み取る必要があるため本記事ではaria-labelで設定します。 これでコンポーネントの作成は終わりです。 簡素ですが以下のようなコンポーネントが画面に描画されます。 プラスボタンを押すと加算処理、マイナスボタンを押すと減算処理が実行されます。 完成図 テスト では単体テストのファイルに移ります。 コンポーネントの数や好みにもよりますが、同階層に置くほうが、視線やカーソルが行ったり来たりしなくて好きです。 import { render, fireEvent, screen } from '@testing-library/svelte'; // $lib はsrc/libのエイリアス import Counter from '$lib/components/Counter.svelte'; describe('Counter.svelte', async () => {  // 初期値 test('カウンターの初期値は0', async () => { render(Counter); expect(screen.getByText('0')).toBeTruthy(); }); test('減算処理', async () => { render(Counter); // ボタンを定義 const decreaseButton = screen.getByLabelText('減算'); // イベントを定義 await fireEvent.click(decreaseButton); const counter = await screen.findByText('-1'); expect(counter).toBeTruthy(); }); test('加算処理', async () => { render(Counter); const increaseButton = screen.getByLabelText('加算'); await fireEvent.click(increaseButton); const counter = await screen.findByText('1'); expect(counter).toBeTruthy(); }); }); これでテストも用意できました。 テスト単位で紐解いてみてみましょう。 test('カウンターの初期値は0', async () => { render(Counter); expect(screen.getByText('0')).toBeTruthy(); }); カウンターの初期値は0 というテスト項目に基づいて、 まず import Counter from '$lib/components/Counter.svelte'; で呼び出している Counter コンポーネントをrender(描画)してます。 そして、** Counter コンポーネントが初期値で持つ値が0かどうかの審議を toBeTruthy というマッチャーで行っています。** ※マッチャーとはテストを評価する際の関数といった理解でおおよそ大丈夫です。 詳しくはJest公式をご覧ください。 Jest 続いて減算処理のテストについて。 加算処理・減算処理ともに、同じようなロジックなので今回は減算処理のみ触れます。 test('減算処理', async () => { render(Counter); // ボタンを定義 const decreaseButton = screen.getByLabelText('減算'); // fireEventでイベントを定義 await fireEvent.click(decreaseButton); // const counter = await screen.findByText('-1'); expect(counter).toBeTruthy(); }); 減算処理 のテストでは、下記の流れでテストをしています。 コンポーネントを描画 コンポーネント内のボタンを定義 クリックイベントを設定 実際に減算された値は-1であるかの真偽 testing-library、async/awaitでスッキリしていて、わかりやすくSvelteとの親和性よいですね。 加算処理のテストは findByText の値が違うだけで、他部分は重複するので割愛します。 実行してみる これを yarn test すると このような感じでテストをパスすると、グリーンでPASSしました。というような結果がコンソールに出力されます。 ではテストに失敗してみます。 <script lang="ts"> // 0 => 1 let count:number = 1; </script> <!-- 減算するボタン --> <button on:click={() => (count -= 1)} aria-label="減算">-</button> <!-- 定義したcount変数 --> {count} <!-- 加算するボタン --> <button on:click={() => (count += 1)} aria-label="加算">+</button> テストファイルでは初期値は 0 を想定しているので、 1 という値がセットされているとエラーになります 間違えてた際でも、以下のようにエラーが出力されます。 またこのように下にエラーの詳細が次のように並びます。カウンターコンポーネントの初期値は0を想定しています。 というようなエラー文が表示されているのがわかります、 簡単ではありますが。上記で単体テストができました。 設定ファイル、テスト実行ファイルともに記述が少なくかけるので重宝しそうです。 以上、Svelteでユニットテストの回でした。 次回は SvelteKitにStorybook を導入してみます。 次回もお楽しみに!
アバター
KINTOテクノロジーズ株式会社 開発支援部の有留です。 全社会議体などの運営や、エンジニア育成、研修などを担当しています。 KINTOテクノロジーズ(以下、KTC)では、業務を通じたエンジニア自身の成長を、会社として応援しています。そのため、社外コミュティ参加や、外部イベントでの登壇も積極的に後押ししています。(社長の小寺、副社長の景山も、外部主催のイベントで度々登壇しています) 2023年2月8日、分析グループ所属の若手エンジニア和田さんが、一般社団法人中部経済連合会さま主催のイベント、中経連×デジタルリテラシー協議会「 デジタル人材育成セミナーin中部 」にゲストとして招かれ、パネルディスカッションに登壇しました! どんな内容で登壇したの? KTCの業務は? など、気になったことを、登壇者の和田さんにインタビューしました。 ー 自己紹介をお願いします! 和田 : こんにちは! KTCでデータサイエンティストとして働いている、和田と申します!社内外からの分析リクエストに対応したり、自社開発アプリのAI機能を開発することが主な仕事です。 本日はよろしくお願いします! 有留 : よろしくお願いします! ー 和田さんは、どんなキャリアを経て、KTCに入社されたのですか? 和田 : 大学では社会情報学を専攻していました。社会情報学というとあまり馴染みがないかもしれないですが、情報通信技術を社会実装して、社会課題を解決するぞ!という、情報学の中では応用寄りの分野です。 大学卒業後、2019年に自動車部品メーカーに新卒入社し、生産管理システムに関わる仕事を経験しました。その後、2022年に現職へと至っています。 ー 今回はどんなテーマ、どんなキッカケで登壇されたんですか? 和田 : 「 デジタル人材育成セミナーin中部 」は、中部圏の様々な企業の経営層、中堅層の方々を対象に「これからは全社員がデジタルリテラシーの獲得をすべきだ」という内容を説くイベントでした。 イベントでは、デジタルリテラシーの獲得に繋がる具体的な資格を3つ推奨していました。「ITパスポート」「データサイエンティスト検定」「G検定」です。 イベントの後半に、日本ディープラーニング協会 理事 事務局長の岡田隆太朗氏と、資格の取得を通じてデジタルリテラシーを身につけたパネリスト4名とで、「資格を取得してよかっとこと」「苦労はあった?」「仕事への影響は?」などのディスカッションを行いました。 私はG検定と、その発展であるE資格を保有しています。資格保有者の参加するコミュニティ内でイベントのパネリスト募集があり、登壇の機会を得ることができました。 イベント会場の様子 有留 : 「G検定」私も最近耳にする機会が多いです。 資格について、詳しく教えてください! ー G検定について、詳しく教えてください! 和田 : G検定は、ディープラーニングの基礎知識を問われる資格です。 GはジェネラリストのGで、専門用語の意味はもちろん、技術の歴史、法規制についての知識もカバーした資格です。数学やコーディングの知識はあまり問われないので、非エンジニアの方にもおすすめの資格です!また、関連資格としてE資格という、ディープラーニングの理論理解や実装能力が問われる資格もあります。 どちらかの資格を保有していると、CDLEというコミュニティに参加することができます。パネリスト募集があったコミュニティというのは、このCDLEです。 CDLE とは、日本ディープラーニング協会(以下、JDLA)が実施する検定・資格試験(G検定およびE資格)の合格者のみが参加できるコミュニティです。合格者同士の交流・情報交換の場を提供しています。このコミュニティは、非営利目的で活動しています。 ※ CDLEコミュニティサイト 、CDLEガイドラインより引用 有留 : 合格者同士のコミュニティがあるんですね。 共通の学びがあることで、話も盛り上がりそうですね! ー そもそも、なぜ資格を取得されたのですか? 和田 : 体系的な知識の獲得に、資格の取得が最も効率の良い方法だと考えたからです! 私がAIの勉強を始めた頃、最初はインターネット上のサンプルコードを参考に、仕組みもよく分からないまま機械学習やディープラーニングのコーディングをしていました。初めは手元で何かが動くことが楽しいだけでしたが、次第に仕組みにも興味が湧き、少し難易度の高い書籍や、技術解説のブログを読むようになりました。 しかしそのような学習法ではピンポイントの知識は得られても、 分野を体系的かつ網羅的に学ぶのは大変で・・・。 そこで、体系的な知識の獲得に「資格試験のシラバス」という先人の知恵が詰まった教材を活用するべく、資格試験に取り組むことにしました。例えるならば、知識の容器に好きな石ころ(ピンポイントの知識)を詰めて隙間だらけだったところに、シラバスから水(体系的な知識)を注ぎ、容器の隙間を埋め尽くそう!といった感じです(伝わるかな?) ![知識のイメージ](/assets/blog/authors/s.wada/image.png =250x) 知識獲得のイメージ 有留 : 確かに新しいことを始めるとき、「何から始めよう」と迷ってしまうことって、私もよくあります。独学で学んでも、その知識が断片的なものだと心もとないですよね。 ー 資格を取得するにあたって、苦労したことや工夫したことは? 和田 : 技術の使い方については独学の経験からある程度理解していましたが、その背景や基礎技術、技術に至る歴史、法律関係については学び直しでした。 加えて、当時のE資格は特定のフレームワークを用いず、numpyによるスクラッチ実装を前提とした問題形式だったため、scikit-learnやkerasなどを使用していた身からすると、慣れない記法に苦労しました。ただ、不足する知識を補いたい!という当初の目的にはピタリと合致していたので、苦労のし甲斐はありました(笑) 有留 : 資格だからこそ、苦手意識のある分野も含めた網羅的・体系的な学びが必要で、その分苦労しそうですね。 ー 新しい資格やジャンルを学んだことで、変化はありましたか? 和田 : 人工知能に関連する用語を一通り学んだことで、これまで読めなかった難易度の高い書籍や、論文にも手が伸びるようになりました。スラスラとはいきませんが、「読める、読めるぞ・・・!」といった感じです(笑) ![読めるぞ!](/assets/blog/authors/s.wada/yomeru.png =250x) 有留 : 自分自身が成長している実感を得られると、苦労も報われそうですね! ー 資格を取得してよかったことはありましたか? 和田 : 昨今、「AI×○○」で高い価値を生み出せるシーンが多いですよね。 今後自分が接する様々な領域に対し、「ここにAIを掛け合わせれば・・・!」という目線を得られたことは、自身の強みに繋がると考えています。今話題のChatGPTに代表されるような、敷居の低いAIサービスがこれからも続々と出現し、この流れは一層強くなっていくのではないかと考えています。 ー KTCでは、「学び」を後押しする制度や文化はありますか? 和田 : 自身の学びを共有する文化があり、グループ内、全社向けなど様々なスコープで勉強会が開かれています。ちょっとした情報共有も盛んで、Slackの情報共有チャンネルでは、日々気になるTechニュースが飛び交っています。 また、業務に役立つ書籍は気軽に購入申請ができ、拠点間で共有する オンライン本棚 で様々な書籍にアクセスすることができます。 機会があれば、今回の私のようなイベントへの登壇も比較的自由にすることが可能です! ー KTCには、どんな社員が多いですか? 和田 : KTCの社員について、入社直後の感想は「色んな人がいる!!」でした(笑) 前職は、ほぼ新卒100%の会社だったので、中途採用100%という環境は衝撃でした。誰もが過去の経験で培った得意分野を持っていて、その長所を活かし合って仕事を成すのはとても刺激的です! 私自身も、AI領域のプロとしての仕事を求められるので、とてもやりがいがあり、成長できる環境だと思います! ー 和田さん自ら、「学びカルチャー」を推奨する工夫はされていますか? 和田 : 自身のスキルや直近学んだこと、興味のあることなど、できるだけ自己開示に努めています。 すると「こんな記事を見つけたよ」と他者からインプットを受けたり、「ここ教えて」とコミュニケーションが生まれて、教える中で新しい気づきがあったりと、いいことづくめです。 ー 最後に、記事を読まれている方にメッセージをお願いします! 和田 : 今回は技術的な話があまりできませんでしたが、機会があれば担当しているAIプロダクトの話などについてもブログにできればと思います! ここまで読んでいただき、ありがとうございました!! We are hiring! KINTOテクノロジーズでは一緒にモビリティの未来を創る仲間を募集しています。カジュアル面談なども行っておりますのでご興味をお持ち頂けましたらぜひお気軽にご連絡ください。 @ card
アバター
こんにちは(こんばんは)、始まりました。 Svelte不定期連載その1です。 前回 はざっくりとSvelteKitを動かすまで、を書いてみました。 (SvelteKitのメジャーアップデートに伴って内容もアップデートしましたのでよかったら一読ください) 今回は、 Write less code をコンセプトとしたSvelteと他のJSフレームワークで、それぞれ書き方にどんな特徴があるのかを比較してみます。 Svelte / React / Vue.js 仮想DOMであるReactとVue.js、仮装DOMを持たないSvelteではありますが、比較対象としてはよくあげられるので改めて比較します。 ※Svelte(v3系) / React(v18系) / Vue.js(v3系) で行います。 Fetch&ループ処理 Fetchとループ処理はセットで書かれることが多いので、まとめて記述してそれぞれを比較してみます。 Svelte まずはSvelteから。 Svelteは独自の構文がありますね、 await/ block構文があるのですっきり書けるのが特徴です。 <script> const fetchItems = async function() { const items = await fetch('URL'); return await items.json(); } </script> {#await fetchItems()} <p>Loading</p> {:then items} {#each items as item} <p>{item.name}</p> {/each} {:catch error} <p>{error.message}</p> {/await}   React Svelteと比べるとコードの行数が増えました。 とはいえ、本来であれば分割なりをするので、このまま書くことは滅多にないでしょう。 useStateではじめに定義するあたりがReactらしいのでしょうか。 function FetchComponent() { const [error, setError] = useState(null) const [isLoaded, setIsLoaded] = useState(false) const [items, setItems] = useState([]) useEffect(() => { fetch('URL') .then(res => res.json()) .then( (result) => { setIsLoaded(true); setItems(result) }, (error) => { setIsLoaded(true); setError(error) } ) }, []) if (!isLoaded) { return <div>Error: {error.message}</div>; } else if (!isLoaded) { return <div>Loading...</div>; } else { return ( <ul> {items.map(item => ( <li key={item.id}> {item.name} </li> ))} </ul> ); } } Vue.js 次はVue3、 2系と比べるとReactに書き心地が似てきた感覚があります。 <script setup> import { ref } from 'vue' const items = ref(null) const error = ref(null) fetch('URL') .then((res) => res.json()) .then((json) => (items.value = json)) .catch((err) => (error.value = err)) </script> <template> <div v-if="error">{{ error.message }}</div> <div v-else-if="items"> <ul v-for="(item,index) in items"> <li>Number:{{index+1}} Name:{{item.name}}</li> </ul> </div> <div v-else>Loading</div> </template> Reactive これもコンポーネントを使い回す現代のフロントエンドで必須の仕組みですね。 Svelte Svelteでは接頭辞に $: をつけることでリアクティブになります。 $: をつけない限り依存する値が変更されても実行されません。 他のフレームワークになれているとこのあたりは最初面食らうかもしれません。 いざ使うと明示的だなぁと感動します。 $: foo = false React 細かい説明を省きますが、Reactでは useState で定義するのが一般的です。 Svelteと比較すると、 useState が $: と同等の役割でしょうか。 import {useState} from 'react' const [foo] = useState(false) Vue.js Vueの場合は reactive で生成すると変更されます。 <script setup> import { reactive, computed } from 'vue' const state = reactive({ count: 0 }) const increment = () => { state.count++ } </script> propsについて 今回は基礎である文字列を受け渡す方法で比較してみます。 三者三様かと思いきや、ほとんど同じような書き心地になってきました。 Svelte <script> export let name </script> <p>Hello, I'm :{name}</p> <script> import ChildComponent from './ChildComponent.svelte' </script> <ChildComponent name="Svelte" /> React const ChildComponent = (props) => { return <h1>Hello, I'm :{props.name}</h1>; } export default ChildComponent; import ChildComponent from './ChildComponent.jsx' function App() { return ( <ChildComponent name="React" /> ); } export default App Vue.js <template> <p>Hello, I'm {{name}}</p> </template> export default { props: { name: String, } } <script> import ChildComponent from './ChildComponent.vue' </script> <template> <ChildComponent name="Vue" /> </template> 子コンポーネントをimportして値を渡す。 どれもわかりやすい書き方でした。 総評 短いですがSvelteと、React、Vue.jsを比較してみましたがいかがでしたでしょうか。 それぞれ良さがある中で、 Write less code をテーマにしたSvelte。 とっつきやすさがやはり際立っている印象があります。 よきSvelteライフをお過ごしください。 そして初めての方はSvelteを是非書いてみてください、とても楽しいフレームワークです。
アバター
Hi, I’m Flo from KINTO Technologies! Today I would like to interview the winners of our first-ever Innovation Days! 🥳 Background Global KINTO Innovation Days was a hackathon-like event held between the 14th and 21st of December 2022, where 30 of our colleagues in Global KINTO, divided into 6 teams joined forces in an effort to bring new ideas to our organization while improving our teamwork. You can read more about how we prepared for the event and how the event went . Team Introduction Before starting, let me introduce you to our winners: team United Nations ! (from left, clockwise) Anwar Moji Chris Angela Duc Anwar : Business development manager with 5 years of experience in the Automotive industry, working to expand KINTO Technologies Globally. Moji : Product designer with expertise in UX/UI, mainly focusing on creating user-centered digital products by working closely with cross-functional teams, to ensure that the designs meet both business goals as well as user needs. Chris : Full stack engineer but mainly working in frontend field in KINTO Technologies. Angela : From the DevOps team. Cloud Engineer with over 5 years of cloud experience, and 2.5 years of System Architecture experience. Over 15 years of system development experience. Duc : Backend engineer working on ID platform team and also have a little frontend experience. Let's Begin! Q01. Why did you join the Innovation Days? Anwar : Because it offered a unique opportunity to learn, collaborate with engineers, and create something meaningful for the company. Moji : To gain exposure to new design challenges and contribute to the development of innovative solutions that have the potential to revolutionize the mobility industry. Chris : I see this as a breakthrough opportunity for us to create something new for KINTO, I also wanted to try out some new technologies and ideas. Duc : I wanted to join because this is my first time participating in such an event. Angela : I wanted to have a deeper collaboration with colleagues from other teams. Also, I wanted to try something new. Q02. Where did you get the team name from? Chris : We named our team “the United Nations“ because we have a very diverse team (but to be honest, the other teams are mostly the same as well! 😅) getting feedback from managers during the innovation days event Q03. Can you tell us more about your tech stack and the team's roles and responsibilities? Chris : The events planning team divided all teams so that there is one person in each role, so we more or less followed that template. I was in charge of Frontend development and we used Nuxt.js for it because it is being used in Global Development Division and I am familiar with this meta-framework. Duc : I was in charge of the backend. Since Python is the most common language among us and also quite easy to use, so we decided to use it. Angela : I was in charge of the platform. For it, we chose AWS because it is what was used in the company and had features relevant to the project. Moji : I was in charge of the presentation deck for the pitching. Since we had limited time, we prioritized the development process and presentation deck over designing mockups. So I used Figma to create the presentation deck to effectively present our findings and goals to the audience within the limited time we had. We also used Miro to collaborate on creating a unique value proposition. By working together and gaining a holistic understanding of both the user's and business's needs from various perspectives. Anwar : I was in charge of business model development. To help ensure that the hackathon project is aligned with market needs, has a viable business model, and is effectively communicated to stakeholders. I think the key was that each member of our team shared their thoughts freely and executed their responsibilities (and even more) in a timely and organized manner. Q04. You have both English and Japanese speakers in the team - how did communication go? Anwar : Almost all of the members spoke both English and Japanese so we would speak in either, and when a team member did not understand something, somebody was translating for them to make sure everyone understood and was on the same page. Moji and Angela : Yes, we would usually be the ones raising our voices so that when we needed to understand something, someone from the team would explain. Q05. How were the planning and designing phases? Anwar : First of all, it was fun. Although having only 4 meetings together, everyone came up with different ideas which made it easier to move forward as we had a lot of ideas to work around with. Moji : We didn’t do that much planning considering the time constraints. A lot was done on the fly and whenever a team member felt there was a gap that needed to be filled, someone would volunteer and do the job. Chris : We only had about 4 team meetings before the actual event. I don’t know how it was for the other teams but for us, it was not enough. Angela : And yet, we were effective. Chris : Yes, so we made it worth our time. Anwar : The good thing was that everyone contributed with different ideas which we tried to combine together into one. So for example, Moji had an idea about kudos, Angela about Google Platform, and then we discussed how to combine them together. And that's how our project came to be. Duc : Yeah, I wanted to make all of these amazing ideas real. Chris : At the beginning, we tried to pick out the best idea by voting, but it was difficult to choose only one so we decided to combine a few. Q06. What surprised you the most during this event? Anwar : My surprise was how good the communication between the teammates was. Our team dynamics were very well balanced, in my opinion. Chris : For me, it was everyone's level of motivation, and how cooperative we were. Moji mentioned it before but each of us took initiative and ownership of what needed to be done and contributed effectively. Angela : As it was the first time for us to work together, what surprised me the most was discovering the strengths of each one in our team. Duc : I was really surprised by how energetic all the attendees were. My current team is mainly backend, and it is a little bit quieter there. presenting their ideas to our GM during the event Q07. What were your biggest challenges and how did you overcome them? Chris : The product we wanted to create was huge so I would say narrowing down the scope of what to develop in the short period of time we had, which was two days. To overcome this, we had to prioritize the features and choose which features to actually develop and which to mock visually on the frontend side for the presentation. Moji : Since we were targeting young generations, and having in mind the way they communicate in social media nowadays, we wanted to showcase as well that we have the technical capabilities to materialize our vision. Chris : Yes, we also wanted to implement a technology that none of us had tried before, so working with that was also challenging. Angela : For that, I did a technical analysis with feedback from the team, combined it with previous knowledge I had, and tested it until we were happy with the functionality. Chris : Another challenge was to find time for all of us to gather and discuss the project since we are all in different teams and projects. Angela : To overcome this, since the time we had was limited, we used to discuss action items to do till the next meeting and followed them up at the beginning of the next one. Duc : As the Innovation Days was held at our company and the number of meeting rooms was limited and varied in size, the organizing team decided to raffle the rooms leaving it to the luck of the team leads and unfortunately, we had the smallest room out of all the participants. Moji : Yes, we had the smallest room and probably this was the only point where we could not do anything about it. Anwar : well, we tried our best to support each other and make it as comfortable amongst each other as possible. Chris : another big challenge was the fact that Anwar was unfortunately not able to stay with us for the second part of the event due to personal reasons. We tried to overcome it by sharing our status and keeping Anwar in the loop through Slack. Q08. Did you think you would win? Anwar : Well, I think all participants of the event had the same winning spirit! We had a team motto, “ we win as a team or we lose as a team “ and we stood by it. Moji : I think the focus for us was not about winning, but networking, communicating well together, having fun while challenging ourselves to think creatively and out-of-the-box; and this mindset and how we approached it, is what helped us to win the competition. Duc : I think other teams' ideas were also very good, but I am so proud of mine that I feel ours had the most potential. team lunch! Q09. About lessons learned in this period of quick ideation and development: would you change anything from how you did it this time? Chris : Although we wouldn’t change any major things of what we did overall, we did conduct a Keep, Problem, Try Retrospective , where we reflected on what we did. A problem we identified then was the time constriction, although that was part of the challenge by design. Probably what we would all change is to plan more in advance so that not having time to review well wouldn’t be an issue. Q10. Events such as these, or hackathons, are more PoC-like; how is it different from projects that you work on a daily basis? Moji : I mentioned it a bit earlier, but time constraints and resource constraints, mainly. Anwar : For me, what was very interesting was to get to exchange ideas with friendly colleagues from different backgrounds. It felt fresh and also easy to work with than with my usual work. Angela : There was no hierarchy among us, no power structure, it was just equal colleagues working together on what each does best. Chris : Anwar and myself switched the leading role, but to be honest, it felt like it was not needed. Anwar : Everyone led when necessary! Chris : Yeah! Like a republic. Q11. What would you say were the elements that make you stand out from the rest of the teams? Moji : I would definitely say it was our collaborative spirit. All of the members worked very well, collaborating with each other and I think that made us create an impact and stand out as a result. Chris used the expression “selfless extra effort“, which I think summarizes very well that spirit: not only taking ownership of tasks but also going the extra mile of researching what could be best and sharing ideas and input with the team. Chris : I think we utilized well the fact that each of us has our own expertise in terms of web development. Secondly, every one of us in the team expresses their opinion and idea proactively, yet listen and respect others at the same time. During our meetings, there were agreements and disagreements, but in the end, we could always come up with a final conclusion and make progress. Angela : I also think that respect and trust were big factors. We are from different teams so during our preparation period we would normally work on different topics, but when we set the goal to finish something for this event until the next time we meet, I fully trusted that each of us would accomplish those goals. Duc : I think our idea was very novel and how we implemented it was also quite good. Q12. To close this interview, I would like to ask you one last question: what would you advise those interested in joining hackathons or events such as this one? Chris : I would advise them to not hesitate to try out new stuff and make sure to communicate with their teammates. Anwar : I would add that sometimes I think it’s good to not stay too formal ( especially here in Japan where people tend to keep forms ). Dropping formalities for more effective and honest communication is necessary, and I think it wouldn’t have been what it became without us being as casual. missing Messi during the awarding Further reading I hope the above interview has inspired you to go and join an ideathon/hackathon! You can also read the article about the time the global group participated in a hackathon organised by Toyota Motors North America: Swarm Hackathon Event Report And if you want to learn more about the global group, you can read the following articles: Global Group Introduction Pt 1 Global Group Introduction Pt 2 Global Group Introduction Pt 3
アバター
はじめに 愛車をリフォーム・アップグレードできるサービス KINTO FACTORY プロジェクトに参画している金谷です。今回は、JIRAとGitHub Actionsを活用し、複数環境へのデプロイのトレーサビリティを向上した取り組みを紹介します。 なお前回は、決済チームで リモートモブプログラミングに関する記事 を書きました。 背景と課題 私はKINTO FACTORYプロジェクトの開発工程の後半から参画しました。プロジェクトのうちECサイトのフロントエンドチームリーダーを担当することになりましたが、担当する中で以下の課題があると考えました。 GitHub issues, JIRA, Excelがタスク管理に使われており進捗が管理しにくい どのタスクがどの環境にデプロイされているか分かりにくい テスト環境へのデプロイ時のリリースノート作成が面倒 ![ExcelのWBS+ガントチャートの例](/assets/blog/authors/kanaya/traceability_excel_gantt.png =480x) ExcelのWBS+ガントチャートの例 はじめに進捗の管理のしにくさですが、私が参画した時点では、タスクの管理ツールはGitHub issues, JIRA, ExcelのWBS+ガントチャートの3種類が存在し、どれも使っている、という状態でした。そのために、必要な情報を一元管理できていないことで、スケジュールやタスクの管理が難しくなっていました。 次に、どのタスクがどの環境にデプロイされているか分かりにくい問題です。開発中では、デプロイの対象環境が2つあり(開発環境とテスト環境)、開発中のタスクがどの環境にデプロイ済みなのかを把握することが難しくなっていました。 最後に、テスト環境へのデプロイ時のリリースノート作成が面倒問題です。テスト環境には、我々エンジニアだけでなく、品質保証を担当するQAチームの方々もテストに使っていたため、いつどの内容をテスト環境にデプロイしたのかを連絡する必要がありました。連絡方法としてリリースノートを作る運用にしていたのですが、このリリースノート作成に毎回5分程度は時間を取られており、非常にストレスに感じていました。 これらの課題に対して、デプロイのトレーサビリティを上げることを目標としました。少なくとも、課題の2と3 (環境ごとのデプロイ管理問題、リリースノート生成問題)は解消されることが期待されます。また、後述の仕事の仕方の変化により、課題の1(進捗管理が難しい問題)の解消も狙っていきます。 デプロイのトレーサビリティを上げる方針 まずトレーサビリティですが、 DevOps 技術: バージョン管理 | DevOpsの能力 には以下のよう書かれております。このうち複数環境の差分を出さない、または出てもすぐに確認ができることが求められます。なお、全ての依存関係のバージョン管理は、フロントエンドの場合に npm の package.json, package-lock.json で管理できるため、本記事では省略します。 どの環境を選んでも、その環境を作成するために使用するすべての依存関係のバージョンをすばやく正確に判断できなければなりません。 また、環境の 2 つのバージョンを比較し、その環境間の変更点を理解する必要があります。 どのタスクがどの環境にデプロイされているかを管理できるようにする、トレーサビリティを上げる方針として、以下を行いました。 JIRAでタスクやデプロイを全部管理する リリースノートは自動生成に頼る JIRAでタスクやデプロイを全部管理する JIRAには、 課題の開発情報を見る機能 があります。コードの状況、レビュー状況、ビルド、デプロイの状況が分かるため、開発に必要な情報をすべてJIRAに集約することにしました。 JIRAとGitHubの連携には、以下の作業を行う必要があります。 JIRAとGitHubを連携する設定を行う JIRAのチケットとGitHubのプルリクエストを紐付けるためにブランチ名にJIRAチケット番号を含める GitHub Actionsでデプロイする際に環境を設定する このうち2.については、エンジニア各人の作業に委ねられる部分でした。エンジニア各人にJIRAチケット番号を含めていただくにあたり、GitHub issuesとExcelの利用を廃止し、JIRAに統一することにしました。JIRAに統一することで、エンジニア各人もタスク管理がしやすくなり、進捗管理する側も、JIRAの ロードマップ を使うことで一元管理できるようになりました。 JIRAロードマップの例 3.については、 environment パラメータを渡してデプロイすることで、 environment に渡したデプロイ状況がJIRAにも連携されます。 参考までに我々が使用しているGitHub Actionsによるデプロイの一部コードを掲載します。 environment パラメータにて、更に $${ inputs.env }} を渡しており、環境ごとのキーが作られるようになります。 $${ inputs.env } にデプロイ先の環境名が入るため、デプロイ先がJIRAに連携されるようになります。 DeployToECS: needs: [createTagName, ecr-image-check] if: ${{ needs.createTagName.outputs.TAG_NAME != '' && needs.ecr-image-check.outputs.output1 != '' }} runs-on: ubuntu-latest environment: ${{ inputs.env }}-factory-frontend steps: - 具体的な処理 結果として、開発状況はJIRAのロードマップとチケットで管理し、各チケットを見ることで、レビュー中なのか、マージ済みだが未デプロイなのか、どの環境までデプロイできているのかが管理できるようになりました。 JIRAの各チケットに記載されるステータス また各チケットではなく、チケット全体を通してデプロイ状況を見える化することもできます。各チケットがいつどの環境にデプロイされたのかを見ることができて便利です。 各環境へのデプロイ状況の見える化 :::message GitHubにもProjectの機能である程度実現できますが、ロードマップの機能や QAチームが使っているツールとの連携 も鑑み、JIRAに統一しています。 ::: リリースノートは自動生成に頼る リリースノートの自動生成は、 GitHubの自動生成リリースノート 機能を使うことにしました。リリースノート自動生成は、 GitHubのリリース機能 のうち、リリースノート部分をプルリクエストのタイトルとリンクを一覧表示する機能です。リリースノート自動生成は、いくつかのルールを設定することで、よりよい対応ができます。こちらを紹介します。 リリース内容のカテゴリを決める リリースノートに列挙されるプルリクエスト一覧は、デフォルトではカテゴリ分類されておらず、非常に見にくくなります。カテゴリ分類を使うことで、リリースノートが整理されて見やすくなります。 カテゴリはラベルで表現されます。今回は、特に主要な変更と不具合修正をリリースノートのカテゴリとして表示したかったため、それぞれを表現するラベル(enhancement, bug)を作成しました。 また、対象リポジトリに .github/release.yml というファイルを作り、以下の内容を書くことで、カテゴリごとのプルリクエストタイトル一覧を生成できます。 changelog: categories: - title: 主要な変更 labels: - 'enhancement' - title: 不具合修正 labels: - 'bug' - title: その他 labels: - '*' 生成されたリリースノートのイメージは以下の通りです。enhancementラベルが付いているプルリクエストのタイトルが「主要な変更」に、bugラベルが付いている場合は「不具合修正」にそれぞれ分類されるようになりました。enhancement, bugラベルが付いていないプルリクエストは、すべて「その他」に分類されます。 プルリクのレビュー時点でカテゴリ仕分けやタイトル修正を行う リリースノートを生成してから手動で仕分けることもできますが、リリースノートを生成した時点では、記憶を取り戻して仕分けをする必要があり、大変です。そのため、プルリクエストのレビュー時点で、カテゴリに相当するラベルを付けることで運用しています。合わせてタイトルも、修正内容に合ったタイトルかどうか見ています。 細かいtipsとして、ラベルの付け忘れを防ぐために、リファクタリングなどに対しては、 others ラベルを付与しています。これにより、レビューしてカテゴリ仕分け済みであることが分かるようにしています。 結果 以上の取り組みにより、抱えていた課題は無事に解決できました。また、特にJIRAロードマップは他のチームでも参照され、今はKINTO FACTORYプロジェクト全体で使われるようになっています。 GitHub issues, JIRA, Excelがタスク管理に使われており進捗が管理しにくい → JIRAのチケットとロードマップに集約され一元管理できるようになった どのタスクがどの環境にデプロイされているか分かりにくい → チケットに環境ごとのデプロイ状況が見えるようになった テスト環境へのデプロイ時のリリースノート作成が面倒 → 2〜3分かかっていた作業が10秒に激減した 今後の展開 本番環境へのデプロイによって、 DevOps Four Keys のうち速度に関する2つがJIRAで計測できます。デプロイ頻度や変更のリードタイムについて、現状と目指すべき指標をチーム内ですり合わせて、継続改善を行っていきます。 本番環境へのデプロイ頻度 マージから本番環境へのデプロイまでのリードタイム KINTO FACTORYプロジェクトでは、サービス成長を一緒に実現してくれるチームメンバーを募集しています。この記事やKINTO FACTORYに興味を持った方は、ぜひ以下の求人一覧もご覧ください! 【KINTO FACTORYフルスタックエンジニア】KINTO FACTORY開発PJT/東京 【KINTO FACTORYバックエンドエンジニア】KINTO FACTORY開発PJT/東京 【KINTO FACTORYフロントエンドエンジニア】KINTO FACTORY開発PJT/東京
アバター