スタイル・エッジ技術ブログ

士業集客支援/コンサルティングのスタイル・エッジのエンジニアによるブログです。

Amazon S3にある大量のオブジェクトをGoogleドライブに移行しました

はじめに

こんにちは!システム事業部のNTです。

ある日、クライアント様から「スタイル・エッジ提供のシステムに保存されているファイルを、Googleの共有ドライブに移行したい」というご相談をいただきました。

システムに登録されている現状のファイル数を確認すると約50万にもなり、共有ドライブに移行するにあたっては作業時のパフォーマンスが大きな課題となりました。
また、システム上で表示されているファイル名と、ストレージとして利用しているAmazon S3に保存されている実際のオブジェクト名が異なるため、移行時にオブジェクトのリネーム処理を挟む必要があることも課題でした。

上記2つの課題を克服しながら進めた オブジェクトのリネーム作業Googleドライブへの転送作業 について、使用したサービスと具体的な手順をご紹介します!

generated by DALL-E3

利用したサービスについて

移行作業をスムーズに進めるために、今回は以下4つのサービスを組み合わせて活用しました。

  • Amazon S3バッチオペレーション + AWS Lambda
  • Rclone + Google Drive API (Google Cloud)

Amazon S3バッチオペレーションとは

Amazon S3バッチオペレーションは、S3上に格納されているオブジェクトに対して一括操作を実行できるサービスです。
大規模なデータセットの一括処理が必要な場合に特に有効で、具体的には以下のようなケースで利用されます。

  • 数百万から数十億個のオブジェクトに対して一括コピーや複製をする場合
  • オブジェクトへのタグ付与や削除、メタデータの変更等を一括で行う場合

S3バッチオペレーションでは、操作対象のオブジェクトを指定するためにマニフェストファイル(CSV形式やS3インベントリレポートなど)を使用します。

実際のジョブを設定する際には、マニフェストファイルの指定やLambda関数の呼び出しといった操作を行います。
Lambda関数を利用することで、オブジェクトのリネームやフィルタリングなどの柔軟な操作が可能です。 aws.amazon.com

Rcloneとは

Rcloneは、クラウドストレージ上のファイル管理を行う、オープンソースのコマンドライン型プログラムです。
UNIXの rsynccp に相当するコマンドを使用でき、ファイルの転送、コピー、バックアップ、暗号化などの機能を有しています。
Amazon S3やGoogle Drive、Dropbox等、標準的なサービスを含めて70を超えるクラウドストレージに対応しています。

rclone.org

今回、Rcloneの利用を決めた理由は大きく二つあります。

  • 並列でのファイルアップロードが可能で、転送速度の高速化が期待できる
  • スクリプトやコマンドラインから容易に操作ができ、エラーハンドリングやリトライ機能が充実している

今回は移行対象のファイル数が多かったため、並列実行可能な点は特に魅力的でした!

移行計画について

移行環境

  • 移行元: Amazon S3バケット
  • 移行先: Googleドライブ(クライアント様指定の共有ドライブ)
  • ファイル数: 約50万ファイルで、合計サイズは約180GB。
  • 注意点: S3上のオブジェクト名を、システム上の名前に合わせてリネームする必要がある

ざっくり移行手順

  1. S3上のオブジェクトをリネーム
    S3バッチオペレーションでマニフェストファイルを読み込み、Lambda関数を呼び出してオブジェクトを別バケットにコピーし、リネームします。
  2. S3からGoogleドライブへ転送
    Rcloneを使用してS3からGoogleドライブに転送を行います。

移行準備

S3バッチオペレーションの設定

1. マニフェストファイルの作成

マニフェストファイルを作成するために、下記の情報を取得します。

  • 変更前のファイル名(システム上のファイル名)
  • 変更後のファイル名(S3 上のオブジェクト名)
  • S3 バケット名

取得した情報を基に、CSV形式のマニフェストファイルを作成します。

なお、マニフェストファイルは、「オブジェクトのS3バケット名」と「オブジェクトキー」、「オブジェクトバージョン(※オプション)」という3カラムからなる CSV ファイルです。
オブジェクトキーにはURLエンコードされたJSON文字列を利用でき、後続のLambda関数にパラメータを渡すことが出来ます。

ここでは、「オブジェクトのS3バケット名」と「オブジェクトキー」の2つを指定しています。
システム内のDBに全ての情報が揃っていたので、SELECT文で取得して整形しました。

"S3バケット名","変更前ファイル名,変更後のファイル名"
"s3-bucket","{s3Key:test/フォル1/変更前.xlsx,newKey:test/フォル1/変更後.xlsx}"

次に、Lambda関数に値を渡すために「"変更前ファイル名,変更後のファイル名"(オブジェクトキー)」をURLエンコードしたJSON文字列に変換します。

"s3-bucket","%7B%22s3Key%22%3A%22test%2F%E3%83%95%E3%82%A9%E3%83%AB%E3%83%801%2F%E5%A4%89%E6%9B%B4%E5%89%8D.xlsx%22%2C%22newKey%22%3A%22test%2F%E3%83%95%E3%82%A9%E3%83%AB%E3%83%801%2F%E5%A4%89%E6%9B%B4%E5%BE%8C.xlsx%22%7D"

Lambda関数には、下記のようなJSONがS3 バッチオペレーションより連携されます。

{
"invocationSchemaVersion": "1.0",
    "invocationId": "YXNkbGZqYWRmaiBhc2RmdW9hZHNmZGpmaGFzbGtkaGZza2RmaAo",
    "job": {
        "id": "f3cc4f60-61f6-4a2b-8a21-d07600c373ce"
    },
    "tasks": [
        {
            "taskId": "dGFza2lkZ29lc2hlcmUK",
            "s3Key": "%7B%22s3Key%22%3A%22test%2F%E3%83%95%E3%82%A9%E3%83%AB%E3%83%801%2F%E5%A4%89%E6%9B%B4%E5%89%8D.txt%22%2C%22newKey%22%3A%22test%2F%E3%83%95%E3%82%A9%E3%83%AB%E3%83%801%2F%E5%A4%89%E6%9B%B4%E5%BE%8C.txt%22%7D",
            "s3VersionId": "1",
            "s3BucketArn": "arn:aws:s3:::S3-bucket"
        }
    ]  
}

docs.aws.amazon.com

2. Lambda関数の作成

S3バッチオペレーションから連携されたJSON内に記述された変更前後のファイル名を取得し、オブジェクトを別のS3バケットにコピーするLambda関数を作成します。
JSON内の s3Key には、マニフェストファイルの "変更前ファイル名,変更後のファイル名" の値が格納されており、
この値をデコードすることで、S3バッチオペレーションから任意のオブジェクト名を指定することが可能になります。

import json
from datetime import datetime
from urllib.parse import unquote
import boto3

s3 = boto3.client('s3')

destination_bucket = 'コピー先のバケット名を指定' # 簡略化のため、定数として定義

def lambda_handler(event, context):
    print('Loading function')

    date = datetime.utcnow()
    print('invoke: ' + date.isoformat())

    results = []

    for task in event['tasks']:
        try:
            task_id = task['taskId']
            s3_key_encoded = task['s3Key']
            bucket_arn = task['s3BucketArn']
            source_bucket = bucket_arn.split(':')[-1]

            s3_key = unquote(s3_key_encoded)
            print(f'decoded s3Key: {s3_key}')

            key_data = json.loads(s3_key)
            original_key = key_data['s3Key']
            new_key = key_data['newKey']

            copy_source = {'Bucket': source_bucket, 'Key': original_key}
            s3.copy_object(CopySource=copy_source, Bucket=destination_bucket, Key=new_key)
            print(f'Copied {original_key} from {source_bucket} to {new_key} in {destination_bucket}')

            results.append({
                'taskId': task_id,
                'resultCode': 'Succeeded',
                'resultString': s3_key
            })
        
        except json.JSONDecodeError as e:
            print(f'JSONDecodeError: {str(e)}')
            results.append({
                'taskId': task_id,
                'resultCode': 'Failed',
                'resultString': f'JSON decode error: {str(e)}'
            })
        except Exception as e:
            print(f'Exception: {str(e)}')
            results.append({
                'taskId': task_id,
                'resultCode': 'Failed',
                'resultString': f'Error: {str(e)}'
            })

    return {
        'invocationSchemaVersion': '1.0',
        'treatMissingKeysAs': 'PermanentFailure',
        'invocationId': event['invocationId'],
        'results': results
    }

なお、今回は一時的に別のS3バケットにコピーしてからリネームする方式としましたが、別バケットにコピーすることで次のメリットがあります。

  • 既存バケット内のオブジェクトに影響を与えず、システムを稼働しながらでも安全なリネームが可能
  • 既存バケットとは異なったディレクトリ階層でのオブジェクト保存が可能

Google Cloudの設定とRcloneの準備

1. Google Cloudの設定

Google CloudコンソールからGoogle Drive APIを有効化し、Rclone用のサービスアカウントを発行します。

サービスアカウントとは、ユーザーではなくアプリケーションに使用されるアカウントを指しています。
アカウント固有のメールアドレスで識別も可能です。 cloud.google.com

2. Rcloneの準備

EC2上にRcloneをインストールし、必要な設定を行います。
Google Cloudコンソールで発行したサービスアカウントのキーをRcloneの設定ファイルに追加します。
rclone.org

3. 共有ドライブの作成

今回は、後述する理由により移行先の共有ドライブを複数用意していただきました。
各ドライブのメンバーとして、Rclone用サービスアカウントのメールアドレスを追加します。
サービスアカウントを追加することで、Rcloneでのファイル転送が可能になります。

移行時の課題点と対応策

課題①:Lambda関数の同時実行数の制限

AWS Lambdaの同時実行数は、デフォルトで1,000に制限されています。
操作対象のオブジェクト数が多い場合には同時実行数の上限を超えてしまう可能性があるため、サービスクォータの引き上げも視野に入れる必要があります。

今回は10万ファイルごとに操作し、同時実行数は最大で1,040ほどになりました。
デフォルトの1,000を超えてしまいましたが、念のためにサービスクォータを引き上げていたので、問題にはなりませんでした!

課題②:Google 共有ドライブの制約

共有ドライブの最大アイテム数上限が約50万のため、すべてのファイルを1つのドライブに移行することは不可能でした。 support.google.com

今後ファイルが増加することも見越し、今回は複数の共有ドライブに分散して移行しました。

おわりに

移行作業には、以下の時間を要しました。

  • ファイルのリネーム:約10万ファイルを処理するのに、30~45秒
  • Rcloneでの転送:並列転送を実施したことにより、約15時間

特にリネーム処理は、想定していた以上に時間が短く衝撃でした!
大量のS3オブジェクトの操作や移行時は、S3バッチオペレーションやRcloneの利用を検討してみてはいかがでしょうか?

スタイル・エッジでは、一緒に働く仲間を絶賛大募集しています。 もし興味を持っていただけましたら、以下の採用サイトも一度覗いてみてください! recruit.styleedge.co.jp

AWS DMSを使って再構築したDBへデータ移行させた話

はじめに

こんにちは。システム事業部のTAKです。
記事を書くのは1年7ヶ月ぶりで、今回で3回目となりました。
入社直後に立ち上げに関わった社内システム(以下、システムA)も5年目に突入しようとしています。
運用年数を重ねると、構成が混沌としてくることがあります。
突貫対応による不要な枝葉や緊急対応として迂回したネットワーク構成、新しいシステムや要件に対応するために別VPCに切り出されたリードレプリカなど……

そう、混沌としてきたのです!

そうした経緯があり、今後のシステム間連携などを考えるうえで、システムAが持つRDSのVPCやサブネットなどのネットワーク構成を再構築することとなりました。
稼働を継続しながら別途構築を進める新RDS環境に対して、データを同期させる方法としてAWS DMSを利用しました。 DMSを利用して良かった点や注意する点などをお伝えできればと思います。

created by DALL-E

AWS Database Migration Serviceとは

AWS Database Migration Service (DMS) は、データベースの移行を簡単に行うためのマネージドサービスです。
オンプレミスやクラウドのデータベースをAWSに移行し、ダウンタイムを最小限に抑えることができます。
リアルタイムでのデータ複製も可能なので、運用停止できないようなサービスのDB移行に最適です。 aws.amazon.com

移行の背景

前述したように、5年以上前に構築されたネットワーク構成のため、現在のシステムAを扱ううえで技術的負債が溜まっていました。
具体的にはRDSの属するVPCのCIDRが社内ルールに則っていなかったり、リードレプリカが別VPC上に構築されていたりしました。
その結果、MySQLのバージョンアップやメンテナンス等でB/Gデプロイが使用できなかったり*1、IP管理や設定に支障がでる*2など負の要因がでてきていました。
また、基本的に24時間稼働させる必要があるRDSであったため、今回のデータ移行を含めメンテナンスを行う際は極力停止しない方針で進める必要がありました。

RDSを停止させずに移行したかったのでDMSを採用しましたが、システム停止時間を取れるのであればdumpでのデータ移行が手軽で、コストもあまりかからないと思います。

環境構成

移行前

移行後

DB再構築にあたって、新たに環境を用意しました。
図の背景色が青色が移行前のVPC(以下、旧VPC)、緑色が再構築用のVPC(以下、新VPC)です。
DMSによって継続的なデータ同期が有効になっている状態で、EC2(Webサーバー)のDB参照先を移行後のRDSに切り替えることで、システムを停止せずに切り替えを実施しました。

移行の流れ

移行手順

  1. 新VPCを新たに作成
  2. 新VPCにRDSインスタンス(以下、移行先RDS)を作成
  3. 移行先RDSに見直しを行ったセキュリティグループを適用
  4. DMSで、現行稼働中のRDS(以下、移行元RDS)から移行先RDSへの移行タスクを作成しタスクを実行
  5. 移行元RDSを参照していたサーバーのDB接続先を、移行先RDSのエンドポイントに変更
  6. システムが正常稼働していることを確認後、旧VPCおよび移行元RDSを削除

移行先にテーブルを用意する

DMSはデータ移行を行うサービスであるため、移行先のDBやテーブルは別途手動で作成する必要があります。
DMS実行時にテーブル等がなければ自動で作成してくれますが、データから推測して作成されるため、移行元と完全一致とならないので注意が必要です(詳しくは後述します)。

DMS利用におけるシステム構成

DMS環境は以下の4つを用意する必要があります。

機能 説明
ソースエンドポイント 移行元DBへの接続情報
ターゲットエンドポイント 移行先DBへの接続情報
レプリケーションインスタンス データベース移行処理を実行する環境(EC2/サーバレス)
データベース移行タスク どのレプリケーションインスタンスで、どのソースエンドポイントからどのターゲットエンドポイントに対して、どのスキーマからどんな条件でデータ移行を行うか等、DMSのメイン処理を扱う

DMS環境のVPCはネットワーク構成上扱いやすいよう新VPC内に作成していますが、旧VPCや別のVPCとしても問題ありません。

DMSを利用して

良かった点

  • データ移行先DBについても移行元DBと同じく書き込み可能であるため、Webサーバー側のDB接続先の設定を切り替えるだけでDBの移行が完了した
    • リードレプリカを昇格させて切り替える方法でも似たような動作となるが、昇格作業後からWebサーバー側設定の切り替えの間に同期が停止してしまうため、短時間ではあるがシステム停止が発生してしまう
  • (今回はAWSのDBを使用したが)ソースエンドポイントはAWS以外のDBや、MySQL⇨PostgreSQLなど異なるデータベースを指定することも可能

注意すべき点

  • レプリケーションインスタンスの稼働コストや(リージョンやAZが異なる場合は)データ転送コストがかかる
    • DMSでの移行が終わり次第、速やかにレプリケーションインスタンスを停止する必要がある
  • MySQLの場合、移行元DBのバイナリログ設定を「ROW」にしておく必要がある*3
  • 移行先のテーブルや列が無い場合、DMSが移行データから推測して作成してしまう
    • AUTO_INCREMENTの設定が欠落してしまったり、符号なし数値型(unsigned integer)だったものが符号付き数値型(signed integer)で作成される場合がある
    • データベース移行タスクの設定で「ターゲットテーブル準備モード」がデフォルトのまま(ターゲット上のテーブルを削除)だと常に発生するため、データのみ削除する「切り捨て」とするほうがよい
    • DMS同期中に移行元DBのテーブル列追加や変更を行った場合は、移行先DBのテーブルを再構築しないと推測で列を追加されてしまう(それらしく作られるため、移行後すぐには気付きづらい)

おわりに

設定や準備に時間とコストがかかる面はありますが、停止せずにDB移行を安全に実施できる仕組みがあるのはありがたいです。
特に移行先DBが書き込み可能なまま同期できるのが衝撃でした。
無停止でのDB移行を考える際は、DMSの利用を検討してみてはいかがでしょうか?

スタイル・エッジでは、一緒に働く仲間を絶賛大募集しています。
もし興味を持っていただけましたら、以下の採用サイトも一度覗いてみてください! recruit.styleedge.co.jp

*1:B/Gデプロイを行う際は、紐付くすべてのリードレプリカが同一リージョン、同一VPCである必要があるなど制約がある

*2:VPCのCIDRが同一のものがあり、直接VPCピアリングすることができなかった

*3:https://docs.aws.amazon.com/dms/latest/userguide/CHAP_Source.MySQL.html#CHAP_Source.MySQL.CustomerManaged

CCNA合格体験記

はじめに

こんにちは!
システム事業部のaoです。新卒で入社して3年目になります。
現在、ネットワークチームに所属し、社内およびクライアントのネットワーク設計から保守まで担当しております。
今回は、先日合格したCCNAの合格体験記を書いていきます!

created by DALL-E

CCNAとは

CCNA(Cisco Certified Network Associate)は世界最大のネットワーク機器メーカーであるCisco Systemsが提供する資格です。CCNAを取得することで、ネットワークの基礎知識およびCisco機器(ルーター、スイッチ、アクセスポイント)に関するスキルを証明できます。
出題内容と割合は以下の通りです。

出題範囲 割合
ネットワークの基礎 20%
ネットワークアクセス 20%
IPコネクティビティ 25%
IPサービス 10%
セキュリティの基礎 15%
自動化とプログラマビリティ 10%

参考:https://learningnetwork.cisco.com/s/ccna-exam-topics

試験時間は120分でした。

CCNAを取得しようと思ったきっかけ

1年ほどネットワーク業務に携わっていましたが、業務でよく使う技術を中心に理解していったため、基礎的な知識が断片的に身についていました。知識を整理し、自信をつけるために受験しました。
また、福利厚生の「STEP BY YOU」により、自己負担無しで受験できました。(詳細は後述します。)
CCNAの受験料は、46,860円(2024年4月時点)とIT系の資格試験の中では非常に高額ですが、経済面での重荷が消え、受験の後押しになりました。

STEP BY YOUとは

スタイル・エッジでは、社員の成長を支援する「能力開発応援制度:STEP BY YOU」を導入しています。
この制度は、カンファレンス参加、書籍購入支援、資格取得支援、スクール受講の4つの柱で構成されています。
社員は関連するカンファレンスやセミナーへの参加費用、技術書や専門書の購入費用、資格試験の受験料、スクールの受講料を全額補助されます。
個々の成長が会社全体の発展に繋がると考え、社員一人ひとりのスキル向上を全力でサポートしています。
※内容は2024年7月時点のものです。

CCNAの勉強方法

本格的に勉強した期間は、約3.5ヶ月です。

使用した参考書とサービス

  • 1週間でCCNAの基礎が学べる本 第3版
  • 基礎からわかる!CCNA最短合格講義
  • Cisco CCNA問題集[200-301 CCNA]対応(以下、黒本)
  • ping-t(有料版)  ※ping-tとは、IT系の様々な資格に対応したWeb問題集です。

「1週間でCCNAの基礎が学べる本」を読み、黒本にチャレンジしたところあまり解けませんでした。
こちらの本は、CCNAの出題範囲の約3割程度がわかりやすく記載されていると感じましたが、出題範囲の全てをカバーしているわけではないように思いました。
そこでより詳しく説明している「基礎からわかる!CCNA最短合格講義」を読み、知識量を増やしながらping-tを中心に問題を解き始めました。
黒本がping-tよりも難易度が高く感じたため、より深く学びたい時は黒本を使っていました。
最終的に、ping-tは4周、黒本は1.5周程度、勉強していました。

試験勉強中に意識していたこと

どうしても、試験勉強に切羽詰まると出題率の低い箇所の学習は後回しにしてしまいがちです。
しかし、今回の受験は知識を整理して業務で活用できるようにし、自信をつけるためのものだったので、出題頻度の低い単元でも、業務で必要とする箇所は重点的に勉強するようにしました。
例を挙げると、CCNAではルーティングの優先順位やスイッチの設定の出題率が高い一方で、DHCPやNATの出題率は低いですが、業務で必要な知識なので諦めないようにしていました。

また、実際の機器を触った方が理解が一気に進むのでおすすめです。
CCNAの勉強期間には、業務にてCiscoスイッチでセグメント分けの構築を行う機会がありました。CCNAの範囲であるVLANやACLを実機で設定することで理解を深めることができました。

結果

最終的に、模試の得点率が90%になった状態で受験しました。本番は、76%程度で合格しました。

実際に受験してみて思ったこと

問題文が理解しづらい

他の方の合格体験記にもある通り、全体の約20%は直訳的で読み取りづらい文章になっており、問題の意図を理解できませんでした。
Cisco機器の設定で使用する用語は英語が元になっています。用語をそのまま覚えるのではなく、日本語でイメージして覚えておくことが大事だと感じました。

コマンドシミュレーション問題(LABO問題)

コマンドシミュレーション問題は、和訳に不自然な箇所があり問題の意図を理解することに時間がかかったり、長いコマンドを打った後で削除しようとすると画面が固まってしまう場合があることを認識しておいた方が良いと思いました。
また、いつコマンドシミュレーション問題が出題されるかわからないので、時間配分が難しいです。私の場合、1問目のコマンドシミュレーション問題に時間をかけすぎてしまい、2・3問目をきちんと解ききることができませんでした。
わからない問題は諦めて飛ばす覚悟が必要だと思いますが、1度回答した問題は見直せないので注意が必要です。

本番の受験環境

今回の試験では、PearsonVUEからオフライン受験で申し込みました。試験中のメモは、紙ではなくホワイトボードとペンを使用します。普段と違う道具を使うため、不安な方はそれらの道具を用いて練習しても良いかもしれません。   

私が受験した際は、最後の問題を解いて、次へ進むボタンを押した瞬間に結果が表示されました。心の準備が必要だと思います。
(他の方の合格体験記では、結果が表示されなかったパターンもあるらしいので、人によるかもしれません。)

CCNAを勉強してよかったこと

理解が足りていなかった基礎的な部分を学び直すことができました。
ネットワーク構成の設計や検証する際にとにかく「第何層」かを意識する癖がつきました。OSI基本参照モデルやTCP/IPモデルを用いて「なぜこの挙動なのか」という理解が早くなったと思います。

©2024 styleedge

最後に

ネットワークは奥深いですが、CCNAを取得できたことでネットワーク知識に少し自信がつきました。
今後は、CCNAと業務で培ったオンプレの知識を生かしてクラウドにも挑戦していきたいと思っています。

スタイル・エッジでは、一緒に働く仲間を絶賛大募集しています。
もし興味を持っていただけましたら、以下の採用サイトも一度覗いてみてください!

recruit.styleedge.co.jp

参考

GitHub Copilot Businessをチームに導入してみた

はじめに

こんにちは。システム事業部イネイブリングチームに所属しているOrochiです。
イネイブリングチーム*1は、開発チームの開発生産性向上の活動や技術支援などを行っています。
その取組みの一環として、GitHub Copilot Businessを導入しました。
現在は、開発チームメンバーであれば申請することで誰でも使える状態になっています。

Created by DALL-E

この記事では、GitHub Copilot Businessを導入した背景や注意点、導入後の効果などについて紹介します。

GitHub Copilotとは?

この記事を読んでくださっている方は既にご存知かと思いますが、GitHub Copilotは、OpenAIのCodexモデルを利用したコード補完ツールで、プログラミング中に次に書くべきコードを提案してくれます。 docs.github.com docs.github.com

Visual Studio Codeなどのエディタに拡張機能を導入することで利用可能です。
また、GitHub CLIを使用するとコマンドライン上でも動作します。

Copilot Chatという機能もあり、こちらを使用することで特定のファイルに記載されている処理の解説などをCopilotにお願いすることができます。
例えば、私はOSSのフレームワーク理解のために、内部処理の解説をさせています。

Laravelの内部処理を聞いている画像

導入の背景

弊社の開発チームがGitHub Copilotを導入するに至った背景には、以下の社内課題とニーズがありました。

エンジニアの不足

会社の成長と共に、開発チームのプロジェクト数が増える一方で、エンジニアの増員が追いつかない状態です。
それに対して、イネイブリングチームとしては効率化に関する各種施策を打ち出していましたが、開発チームの協力が不可欠でありながらも、日々の業務に追われて協力の余裕がない状況が続いていました。

記述量の多いコード

最近は、従来のプロダクトの反省を踏まえて、スパゲッティコードを避けるための適切なファイル分割と責務の分離を徹底しユニットテストを必ず書くようになりました。
しかし、それに伴ってコードの記述量が増加し、物量的に開発スピードが出なくなるといった問題が発生していました。

導入を決定した理由

これらの課題に対処するため、GitHub Copilotの導入を決定しました。その理由は以下の通りです。

生産性向上の効果が期待できた

他社の技術ブログなどで情報収集をし、
「繰り返しの作業を迅速にできるようになった」
「開発効率が上がった」
という記事が多かったため、コードの記述スピードや正確性を向上させ、反復的な作業やテストコードの自動生成に効果が期待でき、組織の開発生産性の向上に繋がると考えました。

既存のGitHubアカウントの活用

弊社では現在ソースコードの管理をオンプレミスのGitLabで行っていますが、イネイブリングチームの施策として、CI/CDの拡張性などを加味してGitHubへ移行する計画がありました。
そのため、一部では既にGitHubを利用しており、サブスクリプションも進めていたのでスムーズに導入できました。

チームメンバーの興味

現場からもCopilotを使用したいという話は上がっていて、その後押しもあり意思決定は比較的スムーズにできました。上司の理解があったのも大きかったです。

注意点とデメリット

社内のコードの漏洩やライセンス侵害などの可能性

GitHub Copilotを利用する際に、社内のコードがGitHub Copilotの学習に利用され、外部に漏れるリスクが懸念されました。また、OSSのコードを参考にコード補完を行うため、脆弱性を埋め込んだり、ライセンス侵害をしたりといったリスクも考えられます。

弊社は、公開コードに一致する提案を無効に設定しているため、OSSのコードをGitHub Copilotが提案しないようにしています。*2
このため、ライセンス侵害の可能性は低く、リスク許容としています。
脆弱性に関しては、複数人でコードレビューすることでセーフティネットを張っています。

若手メンバーの成長を妨げる可能性がある

AIサービス全般に言えることですが、今は「考える」ということをAIに任せられるため、経験年数の浅い若手メンバーが最初からGitHub Copilotなどを使用してコーディングをすると、AIなしではコードが書けない人材になってしまうことが懸念されました。

こちらは、コードレビューでコードの意図を問うようにしたり、ペアプロを通して教育していくことで、何も考えずにGitHub Copilotから提案されたコードを採用するのではなく、自分で書いたコードとして判断する癖をつけるように対応しています。

導入後の効果とメリット

コーディング効率の向上

GitHub Copilotの導入により、特にテストコードや反復的な作業において、コーディング効率が大幅に向上しました。具体的には、次のような効果がありました。

テストコードの自動生成

同じようなテストコードを何度も書く手間が省け、効率が向上しました。
また、ある程度コメントでどういったテストを書いてほしいかを示すことで、全く新しいテストコードも生成可能になりました。

プロダクトコードの自動生成

プロダクトコードもGitHub Copilotに提案してもらえるようになり、こちらも作業効率が向上しました。
適切なファイル分割と責務の分離を徹底していたため、Copilotから提示されたコードは比較的正確なものであることが多いです。

フレームワークやライブラリの理解の補助

弊社のアプリケーション開発では主にLaravelを使用し、中には設計思想に応じた拡張を加えているものもあります。GitHub Copilotによって、Laravelや拡張部分の理解が進み、実装のスピードが向上しました。

開発チームのフィードバック

GitHub Copilot導入後、他社の技術ブログやSPACEフレームワーク*3を参考に開発チームにアンケートを実施し、以下のようなフィードバックを得ました。

GitHub Copilotは何をする時に使用しますか?

一日の仕事の中で、コーディングに費やす時間はどれくらいですか?

GitHub Copilotを使用することでより生産的に仕事ができるようになりましたか?

GitHub Copilotを使用することでより早く繰り返しの作業を完了できるようになりましたか?

GitHub Copilotを使用することでより早くタスクを完了できるようになりましたか?

GitHub Copilotを使用すると繰り返し作業に費やす精神的な負担が軽減されましたか?

1日あたり、GitHub Copilotを使用することでおおよそどれくらいの時間を節約できましたか?

その他の結果

GitHub Copilotを使用するとコーディングのストレスが軽減されますか?

GitHub Copilotを使うことで仕事に充実感を感じられるようになりましたか?

GitHub Copilotを使うことでより満足度の高い仕事ができるようになりましたか?

GitHub Copilotを使用することで作業に集中できる状態に入りやすくなりましたか?

GitHub Copilotを使用することで検索にかかる時間が短縮されましたか?

GitHub Copilotについて使い方やここが便利!など共有したいことがあれば教えてください。

  • 同じようなことを何回も記述することが減った
  • 別の箇所に書いてある内容を読み取って書いてくれるので、リファクタなど一箇所変えれば他のところも読み取って書き換えてくれる
  • コード補完がとにかく便利
  • 人が書いたコードを読み解くときにGitHub Copilotに説明してもらえるのも便利
  • ショートカット設定しておくと、キーボードから手を離さなずにできてめちゃくちゃ便利

その他の意見

  • フレームワークのソースコードを読み取るのが楽になった
  • 三項演算子やスプレッド演算子などの名称を調べるときに便利。スプレッド演算子はgoogleで検索しても「...」などでは引っ掛からなかった
  • 共有というほどではないですが、コードを説明してくれることと書いているときに補完してくれることがありがたい
  • コードの候補がでるので、時短になる
  • Seederやテスト用コードの自動作成・自動修正などは正確に作成してくれる

GitHub Copilotについてここが使いづらい....など共有したいことがあれば教えてください。

  • 長いコードを書いてくれる時は少し時間がかかるので、もう少し早くなったらいいなと思う
  • コード補完を一度に複数行生成されちゃうと、一部だけacceptするようなことができません。全部acceptしてから直すみたいな手間があるのはちょっと使いづらいかも
  • 設問にあった「繰り返し作業」をさせる発想がなかったので、試してみたい
  • 便利な使い方集や、自分はこうやって使っている、という情報共有できる場があると良さそうだなと感じた
  • コード以外のことを調べることができない

導入後アンケートまとめ

全体的に肯定的な意見が多く、生産性の向上や繰り返し作業のスピードアップなどが実感としてある回答が多くありました。
導入前の解決したい問題にアプローチすることができたので、導入して効果が出ているということが開発者のアンケートでもみることができました。
一方で、長めのコード生成に関してはまだ課題感を感じているメンバーもいるため、改善を期待すると共に、GitHub Copilotを扱う上でのテクニックやナレッジについても、見つけ次第随時共有していこうと思います。

おわりに

GitHub Copilotの導入によって、開発チームのプロジェクトによってはベロシティが1.5倍になったという報告もあり、導入効果は目に見えて出ていると感じます。
導入フローは整備しましたが、まだ使用していないメンバーもいるので、よりGitHub Copilotを利用・活用できるように働きかけ、利用率の増加とさらなる開発生産性の向上を目指し、よりよいアウトカムをだせるように活動を続けていきます。

スタイル・エッジでは、一緒に働く仲間を絶賛大募集しています。
もし興味を持っていただけましたら、以下の採用サイトも一度覗いてみてください! recruit.styleedge-labo.co.jp

参考資料

*1:チームトポロジーという組織設計のフレームワークにおける、チームタイプの一つ。

*2:公開コードに一致する提案を有効または無効にする設定について

*3:Satisfaction and well being/Performance/Activity/Communication and collaboration/Efficiency and flow の頭文字をとったもの。開発者の生産性を測るフレームワークの一つ。

Aurora Serverless v2を導入してみた

はじめに

こんにちは。システム事業部のgakiです。新卒入社3年目にして初めての技術ブログ執筆です。 私事ではありますが、最近AWS Certified Solutions Architect - Professionalに合格したこともあり、最近は特定の言語やプロダクトに偏ることなくAWS周りの相談に乗ったり、新規案件のインフラ設計なども行っています。

さて、今回のテーマはAurora Serverless v2についてです。基本的な仕様や導入のきっかけについて書いていきます。

generated by DALL-E3

Aurora Serverless v2とは何か

AuroraはAmazon RDSのサービスの1つで、高可用なデータベースを提供しています。 Aurora Serverless v2はAuroraの中でも比較的新しいサービスで、負荷状況に合わせてDBのスペックを自動でスケールアップ・ダウンする機能が備わっています。

スペックを表す単位としてACUというものがあり、1ACUに対して約2GiB のメモリと対応する CPU、ネットワークが付与されます。 詳細は公式ドキュメントをご覧ください。

docs.aws.amazon.com

Aurora Serverless v2 導入のきっかけ

弊社のあるプロダクトではデータベースにAuroraを使用しています。しかし、プロダクトの規模が大きくなるにつれて、高負荷のSQLが特定の時間帯に多く実行されることによりCPU使用率が100%に張り付き、レスポンス性能が低下するといったことに悩まされていました。 サーバレス導入前は、下記のような構成で運用していました。

システム側での検索や帳票出力など、高負荷な読み取りSQLはリーダーインスタンスに振り分けることで負荷分散していました。しかし負荷はデータ量の増加に伴い上昇していき、SQLのチューニングは定期的に行っていたものの対処しきれず、DBインスタンスのサイズを上げて何とか負荷に対応していました。高負荷時のレスポンス性能を改善するため、スパイクアクセスに対して優れたスケーリング性能を発揮するデータベースである、Aurora Serverless v2を採用しました。

導入手順

Auroraでは、1つのクラスターの中でプロビジョンドインスタンスとサーバレスインスタンスを共存させることができるため、段階的な導入が可能でした。 下記の簡単な手順でクラスターにサーバレスインスタンスを追加することができます。

  • Auroraクラスターのメニューから、「アクション」を選択
  • DBインスタンスクラスに「Serverless v2」を選択

また、既存のプロビジョンドインスタンスをサーバレスインスタンスに変更することも可能です。

置き換え方法

今回は、本番環境で稼働しているリードレプリカのみサーバレスに置き換える方針で進めることになり、具体的な作業は下記の流れで行いました。

  1. 置き換え前 リーダーインスタンスと同数のサーバレスインスタンスを仕込んでおく
  2. 置き換え中 カスタムエンドポイントの宛先DBをレプリカ(プロビジョンド)→レプリカ(サーバレス)に変更

障害時は、カスタムエンドポイントの宛先DBをレプリカ(プロビジョンド)に戻すことで復旧が可能です。

今回Aurora Serverless v2を導入するプロダクトではリードレプリカへの接続はカスタムエンドポイントを使用していたため、置き換え作業はカスタムエンドポイントのターゲットインスタンスの変更で対応しました。

移行後に感じたメリット/デメリット

メリット

パフォーマンスの向上

導入前後で比較すると、高負荷時のレスポンス速度がかなり改善しました。導入前はデータを大量に出力する際にタイムアウトが発生することもありましたが、Aurora Serverless v2を導入して以降、タイムアウトは一度も発生していません。

メンテナンスコストの削減

現在このプロダクトで運用しているAurora Serverless v2は、以前のDBの1/8~4倍のスケーリング幅で柔軟に稼働しています。そのため、毎日のように来ていたDBのCPUアラートが止まり、高頻度の調査・メンテナンス作業から解放されました。

高速なスケーリング

実用性を評価する上で一番気になるところといえば、スケーリングの速度かと思います。

最大ACU使用時のCPU使用率と、サーバレスインスタンスのACUの推移を下記の通り比較してみました。 (Aurora Serverless v2ではCPUUtilizationの値がその時点でのACU数に対する使用率となるため、数式を利用し最大ACU使用時に対応する値に換算しています)

この図から、スケールアップは負荷の上昇の1分以内には開始しており、スケールダウンは比較的ゆっくりと行われるということが分かるかと思います。

一方でAuroraは、負荷上昇に対応する別機能として、Aurora Auto Scalingというものも提供しています。
こちらはスケールアウトの方式で、一時的にインスタンスを増設し負荷に対応するというものです。 しかし、増設インスタンスのプロビジョニング時間が追加でかかるため、負荷上昇への反応速度は10分ほど遅れます。 以前、同プロダクトにてAurora Auto Scalingを導入していましたが、増設インスタンスの立ち上がりが遅く、実際に稼働し始める頃には負荷は減少傾向になっているといったことが多くありました。

docs.aws.amazon.com

以上から、現状のAmazon RDSが提供するスケーリングの仕組みの中では、Aurora Serverless v2が最も負荷の増減に流動的に対応できるサービスであると考えられます。

ただ、スケーリングの速度が常に高速かと言われると、そうでは無いようです。 下記公式ドキュメントによると、スケーリングのスピードはその時点でのACU数が多い方が速くなる、との記載がありました。 docs.aws.amazon.com

現在、同プロダクトのAurora Serverless v2は4ACU~32ACUで設定しておりますが、プロダクトの要件として1分以内のスケーリングで速度面は十分なため、性能面の問題は無いと感じています。 ACUの範囲については、負荷テストなどを実施した上で良い落とし所を見つけていただければと思います。

デメリット

持続的な負荷がある場合はコスト効率を上げにくい

Aurora Serverless v2は使用量による従量課金です。そのため、持続的な負荷がある場合は料金が高くなる可能性があるため注意が必要です。
というのもAurora Serverless v2は、その負荷に相当するプロビジョンドDBインスタンスを利用した場合と比較すると高コストであるためです。

東京リージョンの料金で汎用インスタンスであるdb.m5.largeと、それに相当する4ACUのAurora Serverless v2で料金を比較してみました。

1時間あたりの料金1
db.m5.large USD 0.235
Aurora Serverless v2 (4ACU) USD 0.8

このように、持続的な負荷がある場合はAurora Serverless v2が原因でコストパフォーマンスがあまり向上しなかったり、かえって高コストになってしまう可能性があります。 Aurora Serverless v2 を導入する際は、持続的な負荷が原因で料金がかさまないよう注意が必要です。

移行の教訓とベストプラクティス

負荷状況がAurora Serverless v2のユースケースとマッチするか確認しよう

お話ししてきた通り、Aurora Serverless v2がハマるのは常時低負荷&突発的な高負荷といった状況になります。

例えば下記のような場合は相性が良いかと思います

  • 常時負荷は低いが、1日数回の高負荷なバッチ処理を行うため泣く泣く高スペックのデータベースを稼働させている
  • 検証環境やデータ分析など短い使用時間のために、ほぼアイドル状態のデータベースを稼働させている

このような状況ですとAurora Serverless v2を導入することで、必要な時は必要なスペックで稼働させることができ、一方で大きなコスト削減も見込めます。

負荷が無い時間帯は、最低ACUを下げてコストダウンを図ろう

夜間や休日などにシステムがあまり利用されない場合は、その時間帯だけ最低ACUを下げることでコストの削減が可能です。

下記のように、1コマンドでデータベースの設定値を更新できるため、EventBridgeと組み合わせるなどしてACU調整の自動化も容易に行うことができます。 docs.aws.amazon.com

便利さに依存せず、継続的なSQLのチューニングを

Aurora Serverless v2を導入することで、使用量に応じた従量課金となります。そのため負荷の高いSQLを発行していると、その分だけ料金がかさみます。

導入前になるべくSQLのチューニングを済ませておく

負荷が高くなる要因は事前に取り除いておくと、性能面、コスト面において適切なスケール幅を設定することが可能です。

導入後も負荷軽減が可能なところは改善を続け、スケーリング幅の見直しも行う

改善できそうなSQLがあればチューニングをし、適切なスケーリング幅を設定することで、コスト効率を高められます。
負荷の高いSQLを効率的に検出するために、Performance Insightsの利用を検討してみても良いかもしれません。 docs.aws.amazon.com

おわりに

以上、Aurora Serverless v2導入について書いてみました。品質の維持やメンテナンスコスト削減の面では大きな強みを持っているサービスですが、負荷状況によっては割高になってしまうため状況によって適切な判断が必要となります。

導入の方法は柔軟に決めることができるため、簡単に試すことができるのが大きな強みだと感じています。DBのコスト削減を目指していたり、バースト的な負荷によりレスポンスに懸念があるような状況でしたら、ぜひ導入を検討してみてください。


スタイル・エッジでは、一緒に働く仲間を絶賛大募集しています。 もし興味を持っていただけましたら、以下の採用サイトも一度覗いてみてください!

recruit.styleedge-labo.co.jp


  1. 2024.5.29現在

読書でチームビルディング!?社内輪読会の魅力と実践レポート

はじめに

こんにちは!システム事業部のChunです。弊社技術ブログの運営委員をしており、社内の魅力を発信するため、ネタ探しの日々を送っています。

そんな私が社内チャットでネタを探していたある日、ふと画面をスクロールする指が止まりました。目に入ってきたのは「輪読会」というワード。 どうやらプロダクトチームの有志が同じ本を読み、その内容について意見を交わしあっているという情報を入手しました。

これは面白そう!さっそく実際に会を主催している社員にインタビューしました! 今回はインタビューを通して得られた輪読会のリアルな実態をご紹介いたします!

generated by DALL-E3

なぜ彼らは輪読会を行っているのか?それはどのように行われているのか?

私はその謎を解明すべくAmazonの奥地へと向かった──────

新進気鋭の主催者メンバー!輪読会やってみたレポート!

インタビューした方

1人目

ごっちゃんさん(新卒2年目)

RaKKaR(医業向け顧客管理システム)のバックエンドエンジニア

2人目

みのみのさん(新卒4年目)

LeadU⁺アルファ(士業向け顧客管理システム)のプロダクトオーナー

───さっそくですが、 お二人が輪読会の実施に至った経緯について教えてください!

ごっちゃん

チームで振り返り会を実施した際に、メンバー間で技術的な知見が偏っているよね。との意見が出たことがきっかけでした。

知識の差を埋めるために施行錯誤した結果、今持っている知識のアウトプットだけではなく、新しい知識のインプットもできる輪読会をやってみよう!となり、実際に開催してみました。

みのみの

ごっちゃんたちRaKKaRチームが輪読会をしていることを知ってやってみようと思いました。

もともと私は開発メンバーに技術以外のことも磨いていってほしいなという思いを持っていたんです。システム事業部が掲げる「技術がないエンジニアになるな、技術しかないエンジニアになるな」という言葉に共感していて、どうすればチームビルディングのことなどをみんなに伝えられるかなと考えていました。

そんな時、RaKKaRチームが輪読会をしていることを知り、これなら実現できそう!と思いメンバーに提案したところ、賛同の声が多かったので実施に至りました。

───チームの知識水準を高めることが目的で始まったのですね。続いて、選んだ書籍とその選定理由について教えてください!

ごっちゃん

実施し始めたころは、まずは輪読会に慣れようということで、読みやすいビジネス書を中心に選出していました。ちょうど慣れてきたころに大規模なリファクタを行う予定があったので、「リーダブルコード」、「良いコード/悪いコードで学ぶ設計入門」を読み合わせて備えよう!という理由で選んでいます。その後もマネジメントに関する本など多岐にわたるジャンルで行っています。

みのみの

アルファチームでは「SCRUM BOOT CAMP THE BOOK」という本を選びました。

スピード感が求められるプロジェクトのため、各タスクに使っても良い時間や優先順位を開発メンバーにも意識してもらう必要がありました。そこでスクラム開発をチームに導入しようと思い、私が読んだ中で一番分かりやすく体系的に説明しているこの本を選びました。

───実際に輪読会を開催してみてメリットはありましたか?

ごっちゃん

プロダクトのソースコードについてチームのメンバーみんなで気軽に話し合う機会を作ることができました!

技術書の輪読を行った際は、本の内容と業務をリンクさせて話すことがよくありました。例えば、輪読会中に「リファクタするときはこういった観点も大事だね」や「あのメソッドの書き方は良くないね」といった話で盛り上がったり、コードレビューの際に書籍の内容を引用して説明を行ったりするようになりました。

みのみの

私たちのチームでも、輪読会は日々の業務を振り返る良い機会になっています。本の内容を具体的な業務に置き換えて意見を交わすことで内容の理解度も深まっているなと感じています。

あとはメンバーの学習意欲をかきたてられたことですね! 読もうと思ってつい積んでしまっている本って誰しもあるじゃないですか(笑)

みんなで読もう!という流れができたので、そういった本を読む機会が作れたなと思います。

輪読会で使用したFigJam

───なるほど、業務での経験を踏まえて議論できるのは、社内で行うからこそですね!では、輪読会を実施する上で意識していることはありますか?

ごっちゃん

輪読会への参加ハードルを下げることですね。任意参加でROM専でもOK!という形式をとっています。

また、輪読会当日の朝にどこまで本を読めたのかアンケートをとっています。この結果をもとに議論の範囲を決定しているため、本を読み切ることができなかったメンバーも罪悪感を持つことなく参加してくれています。

本を読むことを義務にしてしまうと、精神的な負担になりますし、読み終わることが目的になって内容が頭に入りにくいですよね。参加者にとって無理のないペースで本を読み進められるように輪読会を開催することが重要だと思います。

みのみの

その場の議論を大事にすることです!

輪読会の進行をファシリテーションが上手なメンバーに任せています。同じ人が話しすぎたり、一度も話さない人がでないよう進めてもらい、参加メンバー全員で話ができるようにしています。

また、メンバーの出社日が揃うタイミングで会議室に集まって実施しています。 座席もメンバーの性格を考えて話しやすい位置に座ってもらっていますね。

よく話す人とよく話す人の間に挟まると、話すことが苦手な人でも話しやすいみたいです!

───ごっちゃんさん、みのみのさん、本日はありがとうございました!最後に、輪読会の将来的な展望や、今後取り組みたいことがあれば教えてください!

ごっちゃん

チームを越えた輪読会もやってみたいですね!全く別の業務をしているメンバー同士で本の内容について意見を交換するのも新しい発見がありそうです。

また、今後チームに入ってくる後輩に向けた輪読会も実施したいです。どんな本を読めばいいのかだけでなく、先輩がどんなことを考えているのかを伝えられる機会になりそうです!

みのみの

輪読会を通して知識の標準化をすすめ、チーム全体のスキルアップを目指したいです。

また、本やセミナーで紹介されるノウハウをそのまま自分たちのプロダクトに適用できるとは一概には言えないと思っています。正解がないプロダクト開発の中でもチームの中で自分たちで話し合って共通認識を持ち、目指していく方向を揃えられる機会にしていきたいです!

おわりに

以上、輪読会やってみたレポートでした!

インタビューする前は、輪読会ってなんだろう、一人で読むのと何が違うの?と思っていましたが、その実態はスキルアップやチームビルディングにつながる素敵な取り組みでした!

スタイル・エッジには輪読会をはじめとして、社員自身がスキルアップやチームビルディングのために「やってみよう!」と思ったことを実施できる文化があります。

今後も、どんなメンバー発信の取り組みが行われるか楽しみです!

スタイル・エッジでは、一緒に働く仲間を絶賛大募集しています。 もし興味を持っていただけましたら、以下の採用サイトも一度覗いてみてください!

recruit.styleedge.co.jp

【モダン開発 #4】モダン開発で工夫した8個のこと

generated by DALL-E3

はじめに

モダン開発連載もいよいよ最終回。わたくしneueがお届けいたします。

番外編の冒頭でも触れていた内容にはなりますが、設計思想とフレームワークの良いとこ取りをしようとすると、必然的に両方に即したアプローチが求められます。中には思想同士が競合する部分もあり、折衷案を出すことすら困難で取捨選択を迫られることもあります。

そういったアーキテクチャの不和を、開発者が極力意識せずにフレームワーク利用の延長線ぐらいの感覚でプロダクト自体の設計・実装を進めることができ、量産されていくコードも自然に一貫性を保たれるような開発者インターフェイスを提供するには、水面下で多くの工夫を凝らす必要がありました。

まだ改善の余地がある状態ではありますが、本記事ではモダン開発の下地に取り入れた工夫をご紹介いたします。似た立場で開発基盤の設計と格闘している方にとって、何かしらの参考になれば幸いです。

※本記事で扱われている言語はPHP、フレームワークはLaravelです

具体例の紹介

各概念における抽象クラスの作成

量産対象となる、フレームワークであらかじめ用意されているクラス(Controller, FormRequestなど)および、独自に追加したクラス(Entity, ValueObjectなど)ほぼ全てに、独自の抽象クラスを作成しました。

一貫して抽象クラスを継承させておくことで、以下のようなメリットを得ることができます。

  • あらかじめ全体共通のふるまいを定義できる
  • 後から全体共通のふるまいが必要になった際に、具象クラスの修正が不要
  • 各具象クラス及び生成されたインスタンスが何者であるかを外部から判定しやすくする
<?php
// 最初の時点は何のふるまいが存在していなくても…
abstract class BaseEntity {}

// あらかじめ継承しておけば、具象クラスの継承先を後から全て書き換える必要がなくなる
class User extends BaseEntity {}
<?php
// ユーザークラスがエンティティなのかを調べることができる
if (is_a(User::class, BaseEntity::class, true)) {}

// ユーザーインスタンスがエンティティのインスタンスなのかを調べることができる
$user = User::create(~~~);
if (is_a($user, BaseEntity::class)) {}

継承先からは継承元のふるまいを一部変更することはできても、打ち消しをすることはできないため、全体ではないが一部で共有されるふるまいは別の形で実現します。

  • 下位概念が対象範囲のケースは、上位概念を継承した下位概念の抽象クラスを作成し、そちらに定義
  • 個別の判断が必要なケースは、トレイトに定義
<?php
abstract class BaseEntity {
    // エンティティとしての共通のふるまい
}

abstract class BaseRootEntity extends BaseEntity {
    // 集約ルートエンティティとしての共通のふるまい
}

abstract class BaseLocalEntity extends BaseEntity {
    // ローカルエンティティとしての共通のふるまい
}
<?php
class User extends BaseModel
{
    // ふるまいをトレイトとして抽出し、対象となる具象クラス側でのみ参照する
    use SoftDeletesTrait;
    ︙
}

量産対象となる具象クラスの記述量を減らす

レイヤーを分割したことで、情報の受け渡しにおいて同じような情報を複数回記述する必要が出てきました。

例えば、レイヤー間でDTOに詰めたり取り出したりする工程であったり、同クラスの静的メソッド間での持ち回し(例:エンティティのファクトリーメソッドに渡ってきた引数一式をバリデーションやコンストラクターに渡す)など、個数が多くなりがちな属性周りにはこの問題が付いて回ります。

情報の一部を選択して渡すケースではさておき、全て(または一定条件に一致するもの)を渡すケースについては、列挙することが人為的ミスの温床にもなりかねません。
もっと簡素な書き方で実現できないかと頭を捻り続け、結果的には黒魔術に手を染めることになりました。

LaravelDataの活用

レイヤー間については、番外編で紹介したLaravelDataを基盤としたDTOによって解決しています。

ユーザー入力からユースケースへの情報受け渡しに記載した通り、独自の入力用メソッドさえ定義してしまえば、あらゆる形式(リクエストオブジェクトやJSON文字列など)から、スキーマとして定めたデータ構造に当てはめることができ、受け渡し先が要求する形式で取り出すことが可能です。

入出力ペアごとにDTOの抽象クラスを作り、入力と取り出し補助に関するロジックはそちらにカプセル化しました。

連想配列と引数のアンパックの活用

PHPではメソッドの呼び出し時に、配列やTraversableなオブジェクトを引数リストにアンパックできます。その際、添字配列の代わりに連想配列を渡すと、キー名と合致するパラメーターに対して引数が展開される仕様になっています。

<?php
function sum(int $foo, int $bar, int $baz): int {
    return $foo * $bar + $baz;
}

// 要素の順番の引数として渡されるので結果は`5`
sum(...[1, 2, 3]);

// 要素の順番は関係なく同じパラメータ名の引数として渡されるので結果は`7`
sum(...['baz' => 1, 'bar' => 2,'foo' => 3]);

// パラメータ名として存在しないものが含まれているので
// Error: Unknown named parameter $foobar
sum(...['foo' => 3, 'bar' => 2, 'baz' => 1, 'foobar' => 1]);

同クラス内でほぼ同じパラメーターを持つメソッドへの引数の受け渡しについては、わざわざデータオブジェクトを介在させる必要もないため、上記仕様の活用を起点に対応方法を考えました。

ちなみに、引数の単純な添字配列についてはfunc_get_args()で取得でき、呼び出し元と呼び出し先のパラメーターの数や順序が完全一致していれば、そのまま渡すだけで済んでしまう話ではあるのですが、多くのケースでは一部のみの受け渡しや、値を書き換えてから渡す必要がありました。

下記の課題を解決すれば採用が現実的であると判断し、PHPの機能を調査しました。

  • 自身に渡された引数とパラメーター名を使って連想配列を作れること
  • 自身に渡されなかった(省略された)引数は、デフォルト値に置き換わること
  • 呼び出し先メソッドのパラメーターに存在するキーのみに対象をフィルタリングできること(エラー防止)
  • 手動で一部の値の上書き・引数の追加ができること

リフレクションによる内部情報の利用(黒魔術)

リフレクションを使用することで、クラスや関数など多くの内部情報(型やプロパティ、コメントさえ)にアクセスできるようになるため、課題解決に必要だった以下の情報が入手できます。

  • 呼び出し元メソッドのパラメーター(名前、初期値)
  • 呼び出し先メソッドのパラメーター(指定した名前のものが存在するか)

以下の工程を踏むことで、目的のデータが作成可能になりました。

  1. 自身に渡された引数配列とパラメーター名配列をarray_combineして連想配列を作成
  2. 省略された引数はパラメーターの初期値で置き換え
  3. 呼び出し先メソッドのパラメーターに存在するキーの項目だけに絞り込み
  4. 上書き・項目追加用の連想配列をマージ

ただし、抽象クラス側に定義したこのメソッドを呼び出す際、引数と呼び出し元メソッドの情報を渡す必要が出てきてしまいました。

<?php
class User
{
    public static function create(〜〜〜): User
    {
        $arguments = func_get_args();

        // 引数配列と呼び出し元メソッド名を渡している(複数メソッドから呼び出したい場合)
        static::validateFromArguments($arguments, 'create');

        // 引数配列と値上書き用の連想配列を渡している(一部の値を上書きしたい場合)
        static::createInstanceFromArguments($arguments, [/* 上書き用連想配列 */]);
    }
}

バックトレースによる呼び出し元情報の参照(暗黒魔術)

PHPにはdebug_backtraceという、バックトレースを生成するエラー処理のための関数があります。
指定したスタックフレーム数まで、呼び出し元のメソッド・関数に関する情報を参照できます。

こちらを利用することで、以下の情報が入手できます。

  • 呼び出し元のクラス名・メソッド名
  • 呼び出し元のメソッドに渡された引数

これらの情報を利用することで、呼び出し元で引数配列を先に取得する必要もなくなり、呼び出し元のメソッドが複数(N:1)のケースにおいて、どのメソッドから呼び出したかという文字列情報も不要になりました。

<?php
static::validateFromArguments();

static::createInstanceFromArguments([/* 上書き用連想配列 */]);

使用にあたっては、スタックフレーム数の指定や、不必要な情報の参照を無効化するオプションを指定しないと、メモリの負荷が高くなってしまうため注意が必要です。

実際の使用にあたって

正直なところ、内部情報や実行時のトレース情報を利用することは、関数の純粋さが大きく失われてしまい抵抗感はあります。 ただし、Laravel内部でも使用されていることや、実装者視点で得られる恩恵があまりにも大きいことから、今回は用途限定で採用することにしました。

また、渡していないデータが暗黙的に参照されること自体も本来アンチパターンであり、コードを読んだだけでは挙動が想像できないため、ささやかな情報としてメソッド名にFromArgumentsといった接尾辞を含めることにしました。

ビジネスルール検証におけるValidatorの活用

DDDにおいて、ドメインオブジェクトが持つビジネスルールは、原則クラス内にカプセル化します。
インスタンスの作成や更新前に各属性に関するルールチェック、後に複数属性にまたがる整合性チェックを遅延バリデーションの形で行い、ルール違反が見つかった場合には例外をスローするといったアプローチが一般的かと思います。

従来パターンにおける課題

属性数が多くなったりビジネスルールが複雑になるにつれて、検証ロジックのコード量・分岐数の増加を招きます。
中でも、属性のルールチェックに関してはルール自体が汎用性が高いものであることが多いにも拘らず、通常の分岐を用いると行数が増加していってしまい、ルールが把握しづらくなってしまいます。

<?php
class User
{
    // 各属性のセッターでバリデートするとしたら…
    private function setAge(int $age): void
    {
        $errors = [];
        if (strlen($age) > 3) {
            $errors[] = 'age.maxDigits';
        }
        if ($age < 0) {
          $errors[] = 'age.positiveInteger';       
        }
        if ($age > 120) {
            $errors[] = 'age.max';
        }
        if (count($errors)) {
            throw new DomainValidateError($errors);
        }
        $this->age = $age;
    }
}

Validatorを用いる前提で宣言的に記述する方式

各属性に関するルールチェックはLaravelのValidatorに依存させる形で実装して、ルールはFormRequestのように宣言的に記述する方式を採用しました。
ルールを、配列を返す静的メソッドに切り出すことで、各属性の一般的なビジネスルールが一目瞭然になりました。

<?php
class BaseEntity
{
    protected static function validate(): bool
    {
        // create, updateXxxなどの呼び出し元に応じて、
        // 全ての属性または一部属性をValidatorを使ってバリデート
    }
}

class User extends BaseEntity
{
    // 型や必須判定は、ファクトリーメソッドのパラメーターの型宣言で担保されているため、こちらには含まない
    private static function rules(): array
    {
        return [
            'firstName' => ['max:80'],
            'lastName'  => ['max:80'],
            'age'       => ['positive_integer', 'max_digits:3', 'max:120'],
        ];
    }
}

オブジェクトが入れ子になっている場合の責務の所在

各オブジェクトで直接検証を行うのはプリミティブ型の属性に限定し、オブジェクト型が指定された属性の検証は、当該オブジェクト側の責務として、再帰的に検証される作りにしています。

例えば、年齢がAgeといった値オブジェクトとして実装されているのであれば、ルールの定義・検証はいずれもUserでなくAge側で行います。

<?php
class User extends BaseEntity
{
    // クラスのプロパティ(コンストラクターのパラメーターから昇格)で、インスタンスが持つ各属性の型を定義
    private function __construct(
        protected string $firstName,
        protected Age $age,
    ) {}

    // 一方ファクトリーメソッドのパラメーターは、ユースケースから渡されるプリミティブ値を受け入れる
    public static function create(
      string $firstName,
      int $age,
    ): User
    {
        // クラスのプロパティがプリミティブ型の属性のみチェック
        static::validate();
        // クラスのプロパティが固有オブジェクト型の属性は変換
        // ※内部的には当該クラスのファクトリーメソッドが呼び出され、ここと同様の判定・生成処理が行われる
        $convertedValues = static::convertObjects(['age' => $age]);

        // 引数の値とオブジェクトに変換された値を用いてインスタンスを作成
        $user = static::createInstance($convertedValues);
        // 作成されたインスタンスの整合性をチェック
        $user->validateIntegrity();

        // ルール違反がなければインスタンスが返却される
        return $user;
    }

    private static function rules(): array
    {
        return [
            // ユーザーの属性ルールとして定義するのはプリミティブ型である名前のみ
            'firstName' => ['max:80'],
        ];
    }
}

class Age extends BaseValueObject {
    public readonly int $value;

    private function __construct(int $age)
    {
        $this->value = $age;
    }

    public static function create(int $age): Age
    {
        // オブジェクト型の属性や整合性チェックを持たない末端のオブジェクトだとシンプルに
        static::validate();
        return static::createInstance();
    }

    private static function rules(): array
    {
        return [
            // 年齢として扱う数値に関するルールは、年齢オブジェクトのルールとして定義
            'age' => ['positive_integer', 'max_digits:3', 'max:120'],
        ];
    }
}

その他ポイント

Validatorを用いると、起点となるユーザーインターフェイス層で違反を扱う際に下記のようなメリットもあります。

  • 特別な工夫なしに各属性のルール違反をひとまとめにできる(ErrorBag
  • メッセージのフォーマットとして、Laravelのメッセージファイルを利用できる

また、サンプルコードにしれっと紛れ込んでいますが、Laravel標準のバリデーションルールには存在しないもので、ビジネスルールとして頻出するものについては拡張しています。(例:正の整数、カタカナのみなど)

今回触れなかった整合性チェックについては、クロージャ形式で混在させることも不可能ではないものの、視認性が高くはなく再利用性も低いため、従来のValidatorを使用しない記述方式で遅延バリデート*1することにしました。

フロントエンドバリデーションとの数値ルール共有

ユーザー体験向上のためには、即座に応答できるフロントエンド時点でも入力バリデーションを行う必要があります。

ビジネスルールとユーザー入力における制限が必ずしも一致するとは限りませんが、多くは連動しています。
ドメイン層とユーザーインターフェイス層で、同一の根拠を持つバリデーションルールを別々に保持することは、差異発生のリスクがあるため、ドメイン層にのみ情報を持ち、そちらに依存させることが望ましいでしょう。

ルールセットとしてフロントエンドと同期するには、TypeScriptの型やZodなどのスキーマの形式で取得可能にするのが1つの理想ではあります。
現時点では、個別で取り出したいケースがある点も考慮し、多重管理リスクの大きい数値系ルールのみを扱いやすい形式で切り出し、Controllerから取得可能にしました。

<?php
class User
{
    // 属性/内容別に数値形式で参照できるようにする
    public static function numericLimits(): array
    {
        return [
            'firstName' => [
                'max' => 80,
            ],
            'lastName' => [
                'max' => 80,
            ],
        ];
    }

    // 数値系ルールはバリデート時に自動変換する仕組みを作り、rulesには記述しない
    private static function rules(): array
    {
        return [
            'age' => ['positive_integer'],
        ];
    }
}

数値系ルールを構造化したオブジェクトをViewに渡すことで、属性値などとして個別参照が可能です。

<x-form-input
  type="tel"

  :maxlength="$numericLimits->user->phoneNumber->max"
/>

Eloquent Modelとの共存

Laravelのコアといっても過言ではないEloquent Modelは、包括的な概念でありつつ、Laravelが提供する多くの機能との関連性を持っています。 レイヤーを越境しやすい性質を持っていることから、DDDやレイヤードアーキテクチャといった設計思想における責務分離の考え方と、最も競合しやすい概念ではないでしょうか。

ORMとして同品質の概念を再開発する自信と時間はさすがにないため、使用自体は許容しつつRepositoryQueryServiceから情報を返す際に、ドメインオブジェクトに変換してから返却するルールにすることで、インフラストラクチャ層から外へはModelを原則露出させない方式を選択しています。

<?php
class UserRepository extends BaseRepository
{
    // クエリメソッドからModelをそのまま返却しない
    public function find(int $id): ?User
    {
        return UserModel::find($id)?->convertEntity();
    }
}

class UserModel extends BaseModel
{
    public function convertEntity(): User
    {
        // Modelのインスタンスを元にEntityのインスタンスを再構築
        return User::reconstruct(
            id:   $this->id,
            name: $this->name,
            ︙
        );
    }
}

Modelの責務としては

  • データベース操作の抽象化
  • データベース上とシステム上で扱うデータ形式の相互変換
  • リレーション先のデータ取得

に留めて、各層からは直接依存させないことで、あくまでORMとしての役割を担ってもらっています。

ユーティリティクラスなどのレイヤー外概念

プログラムで取り扱う概念の中には、問題領域・解決領域いずれも直接の関係はなく、開発そのものや非機能要件にまつわる課題を解決するための概念が存在します。

これらはいずれかのレイヤーに所属させても矛盾が発生することがあるため、切り離して考えることにしました。
(正確には、この領域から各レイヤーへの依存は許可せず、この領域に対する依存については制約を設けない)

情報・関数

システムによって変動することのない、再利用性の高い補助的な手続き(例:文字列加工、年号変換)については、ユーティリティクラスとして外部化し、Laravelのヘルパー同様、エイリアスとして登録することでどこからでも参照できるようにしました。

具体的な手続きに名前を付けて外部化することで、メインロジックをクリーンに保てるのと同時に、本来求められているふるまいが際立つことで、処理のアウトラインを追う目的でのコードリーディング負荷は大きく下げられます。

より良い手段が見つけられた際に仕組みの交換が容易になることもメリットに感じました。

<?php
// 処理全てをその場で書いてしまうと本筋が読みにくい
$originalAttributes = $request->attributes();
$replacedKeys = array_map('\Str::camel', array_keys($originalAttributes));
$attributes = array_combine($replacedKeys, $originalAttributes);

// keyをキャメルケースに変換した属性の配列が欲しいということが分かる
$attributes = \ArrayHelper::replaceKeys($request->attributes(), '\Str::camel');

内製パッケージ

今回のシステムに合わせて開発したものの、システムを選ばずに導入ができそうな機能(例:操作ログ)や、外部サービスとの連携の仕組み(例:Amazon TimestreamのORM)などについては、関連ファイルをあえて一纏めにした状態で管理しています。

異なるシステムが持つ要求の差異を吸収できるほどの汎用化を終えたら、社内向けのパッケージレジストリ上に管理を移行することを前提として、あらかじめ関連コードが分散しないよう関心事にフォーカスしたディレクトリ構成となっています。

また、動作に必要な実装をシステム側で管理する場合は、必ずインターフェイスを提供することで、規格を分かりやすく実行前から基準を満たしているかを確認できるようにしています。

App\Support\Packages\OperationalLogger
├── OperationalLogger.php
├── OperationalLoggerRepositoryInterface.php
├── Http
│   └── Middleware
│       └── RecordOperationalLogs.php
└── Providers
    └── OperationalLoggerServiceProvider.php

文字列フォーマット・設定値

前述の2つとは性質が異なりますが、同じくレイヤー外で扱う情報はあります。

画面に表示する各種メッセージの文字列フォーマット、データベースで管理するほどではない挙動制御のための設定値については、Laravelの設計に則ってlang, config配下に独自の定義を追加することで、一元管理による一貫性の維持とハードコーディング防止に努めています。

<?php
// 引数に使用する値を設定から参照
formatDate($date, config('date.formate.datetime'));

class ItemNotFoundException extends Exception
{
    public function __construct($itemName, $code = 0, $previous = null)
    {
        // 例外のメッセージを動的に生成
        parent::__construct(trans('exception.item_not_found', ['item' => $itemName]), $code, $previous);
    }
}

課題として、config()で返却される値について型の保証が標準では難しいため、エイリアス関数を用意してそちらで返却時の型情報を補完するか、各設定にあるPHPDocの情報を参照してヒントを与えるような、PHPStanの拡張機能を自作する必要性を感じています。

Make系コンソールコマンドの充実

DDD・レイヤードアーキテクチャに則って開発していることもあり、従来のLaravelに比べて開発時に扱う概念(クラス)の種類は多くなっています。

独自クラス(エンティティ、目的別DTOなど)は、Web上に情報が掲載されていないため、

  • 必要プロパティの示唆
  • 定義するメソッドのサンプル
  • 取り扱いにあたっての注意点

といった情報を目に留まる場所に記述して、正しく扱えるようにする必要があるのと、
既存クラスについても、

  • 名前空間の変更
  • 継承元の抽象クラスの独自化
  • コメントの日本語化・PHPDocにおける型定義の詳細化

といった調整箇所が存在します。

Laravelには、各種クラスのファイルを生成するためのArtisanコマンドが整備されており、その中でファイルの雛形(スタブという)も管理されているため、仕組みに乗る形でパターン別の対応を行いました。

  • 既存クラス
    • 静的文字列の変更のみ
    • 引数やオプションの拡張、名前空間の変更、動的文字列の変更が必要
      • スタブファイルの編集
      • 対象のMakeCommandを継承したコマンドを作成・処理の上書き
  • 新規クラス

また、他コマンドからのコマンド呼び出しを活用することで、特定のクラスと実質セットで作成することになるインターフェイスやユニットテストを同時作成するようなオプションを拡張し、作業工程の短縮を図っています。

例:

# User集約内にRelatedPersonというローカルエンティティとそれに紐づく
# エンティティコレクション(エンティティのリストをラップしたクラス)を生成
php artisan make:entity User/RelatedPerson -lc

# Actionとそれに紐づく入力用DTO、ユニットテストを生成
php artisan make:action User/Get -dt

今回は作成しませんでしたが、フローチャート次第で使用スタブが変わるようなケースでは、オプションの組み合わせで判断するのではなく、対話式のインターフェイスにしてみると無効な組み合わせパターンを防ぐことができそうです。

仕組みが整っていない場合にはよくある、類似ファイルを複製して調整を掛ける運用では、記述の過不足が発生するリスクがあるため、スタブファイルをバージョン管理し、基本ルールに変更が生じた場合はそちらにも修正を加える運用が安全です。

静的解析・リントツールの整備

コード品質の維持には、一貫性を守るためのルール整備が必要です。
しかし、ルールを人力だけで徹底しようとすると、かえって作業効率が低下したりレビュー工数が肥大化したりすることが予想されるため、可能な限りルール違反は静的解析で検出できるようにしています。

PHPStanの拡張機能は、Laravel組み込みクラスへの対応としてlarastanを、レイヤードアーキテクチャにおける正しくない方向への依存発生、所定の抽象クラスを継承していない具象クラスの検出に、phpatを活用しています。

また、独自クラスにおける必須指定などの開発時に遵守すべきルール・制約についてもこの層でカバーします。
専用のPHPStan拡張機能を作成することで、実行時に検証を挟まずとも早期検出ができるため、ロジック側に防御的なコードが氾濫することを防止できます。

エディター・CI上でチェックを通過してからレビューに進むフローにすることで、レビュアーの負荷も低下します。

今後の計画

記述量への対策

記述量が多い部分については、まだ全てが解消には至っておらずボトルネックとなっています。
しかし、レイヤーを分割し然るべき粒度で概念を分けるとなると、元の状態に比べてある程度増えることは当然とも感じます。

記述量を減らすといったアプローチだけではなく、1つのクラスの内容を元に関連クラスの内容を生成するジェネレーター・コンバーターの開発であったり、PHPDocの型情報とパラメーターなどの重複部分についてはAIの力を借りて自動補完できるようにするなど、まだ試せていないアプローチもこれからトライしていこうと思います。

自動テスト関連の強化・ハードル低下

自動テストに関しては、前回の記事にもある通り、依存の中心に関するユニットテストに留まっており万全とは言えない状況です。

ユニットテストだけではカバーができていない箇所について、統合テストやE2Eテストを導入できるよう、テスト実装の共通処理を抽象化するなどして実装ハードルの低下を目指しつつ、カバレッジについても基準等を検討していければと考えています。

DevContainerの導入

環境構築の時間を短縮しつつ、設定が近い状態でチーム開発ができるようにするため、ローカル環境の動作に必要なDocker Composeの定義や、VS Codeの設定ファイル雛形・推奨拡張機能の定義をバージョン管理対象に含めて運用しています。
しかし、実際に0から環境構築を行い開発に着手できるようになるまで、一部工程については手順書が必要な状況です。

DevContainer化することで、コマンド一発でエディターのセットアップも含めた開発環境を立ち上げられるようにし、最終的にはGitHub Codespacesを活用して、レビュアーは自身のローカルを汚さずともレビュイーの作業環境をチェックできるような状態を作れるようにしたいと思います。

おわりに

断片的な紹介ではあるものの、気付けばなかなかのボリュームになっていました。
まだ改善の余地が多い熟成度なので、今後も開発現場からのフィードバックに耳をしっかりと向けながら、思考と選択を繰り返していこうと思います。

また、冒頭でもお伝えしましたが、モダン開発連載は今回で終了となります。
振り返ってみると想像以上にチームのキャパシティを超えた挑戦にはなり、一部は方針転換をすることにはなりましたが、プロジェクト開始前に比べて設計に対する捉え方・考え方が大きく成長したメンバーが複数名いたことは、弊社にとっても大きな実りになったと感じています。

仕組みの構築という支援的なポジションではありましたが、その一助となれたことは、個人的に良い経験となりました。
限られたリソースの中でも、こういった投資的な取り組みが持続できる組織でありたいと切に思います。

一緒に探求してみたい方、もっと良い方法がある…と左手が疼いている方、ぜひ一度お話だけでも聞きにきていただけたら幸いです。 recruit.styleedge.co.jp

*1:CHECKS パターン・ランゲージにおけるバリデート手法の一つ。複雑性が高い検証は極力後回しにするという考え方。