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

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

初めてのスクラム導入!そこで起きた課題をどう乗り越えたか!

はじめに

はじめまして。スタイル・エッジのOと申します。
現在、あるプロジェクトにてプロダクトオーナー(PO)を務めております。

本プロジェクトでは、試験的にスクラムを導入し、1年以上にわたり運用を続けてきました。
しかし導入当初は、スクラムの経験者が一人もおらず、手探りでのスタートだったため、うまくいかないことも多くありました

今回は、スクラムを導入する中で直面した課題と、それに対してどのように改善を進めてきたのかを、ナレッジとしてご紹介いたします。
同じような悩みを抱えている方の参考になれば幸いです!

スクラムでうまくいかなかったこと

デイリースクラム(朝会)が進捗報告だけの場になっていた

デイリースクラムの目的は、その日の作業を明確にし、計画を調整し、問題があれば迅速に対処することです。
一般的には15分程度が推奨されており、シンプルで効果的なミーティングであるはずですが、当時は以下のような課題がありました。

  1. 各々の進捗報告のみで終わってしまい、チーム全体に共有すべき情報が時々、漏れていることがあった
  2. タスクをホワイトボードで共有していたが、構成に課題があり「誰が」「どれくらいの量の作業をしているのか」が分かりづらかった
  3. 誰が何をしているのか分からないため、自然とフォローの意識も希薄になっていた
  4. 「処理中」の中に多様なステータスが混在し、タスク状況が読み取れなかった。開発中なのか?コードレビュー中なのか?検証中なのか?が分かりづらかった
  5. 1人ずつ「今日やること」を順番に話していくだけで、30分以上かかっていた。話すことに時間がかかって、スプリントゴール達成に向けた協力や相談ができていなかった
    各メンバーのタスク共有用ボード

このように毎日メンバー全員が集まる時間を確保していましたがあまり効果はなく、「チームでプロダクトを作る」というよりは、「各々が自分の作業を進めているだけ」の状態に陥っていました。
違和感は覚えつつも、何が原因か分からず、スクラムに詳しいメンバーもいなかったため、デイリースクラムを漫然と続けていました。 しかし、スクラムマスターがチームに加わったことで、少しずつ改善が進み始めました。

デイリースクラムの改善

  1. 全体で共有すべき事項をまとめる「全体共有欄」を設け、最初にそこを確認する運用に変更しました

    全体共有欄
    これにより、全体共有すべきことを先に話すようになり、連絡漏れも減りました

  2. メンバー別にタスク管理できるページを作成しました

    タスク管理欄
    メンバーごとの作業量が可視化され、誰がどれくらいの量の作業をしているか把握しやすくなりました

  3. 2の改善で忙しい人や困っている人がわかりやすくなり、自然とフォローも増えていきました

  4. ステータスでタスク状態が可視化され、各メンバーのタスク進捗状況が一目で把握できるようになりました
    例として「処理中」でまとめていたステータスを「着手中」「クライアント確認中」「リリース待ち」と細分化しました

  5. 当日の予定を記載しておき、共有したいことをピンポイントで伝えるようにして時間短縮しました

    今日の予定や共有したいこと欄

    この工夫により、メンバー同士が「困っていること」「相談したいこと」などを話す時間を確保できるようになりました
    その結果、課題が発生したときに相談→早期解決が可能になりました

現在は2週間のスプリントで運用しており、1週目の金曜日には「予定通りリリースできそうか?」を朝会で確認しています。
難しそうな場合は、優先度をつけてリリース対象を見直す運用に変更しました。

上記改善により毎スプリント、クライアントに価値を提供できる状態になり、クライアントから良い評価も頂けるようになりました。

デイリースクラムの課題は解決した一方で、リファインメントにも課題がありました。

リファインメントでプロダクトバックログアイテムの見直しが進まない

リファインメントの目的は、プランニング前にプロダクトバックログアイテム(PBI)の内容を確認し、疑問点を解消することです。
しかし、当時の私は「リファインメント前にPBIをどのような状態にするべきか」や「プランニングに必要な情報は何か」といった基本が明確でなく、効率の悪い進め方をしていました。

具体的には、次のような課題がありました。

  1. 「各ロール(PO・デザイナー・開発メンバー)がどのタスクを担当するのか」が可視化されておらず、1スプリントの各ロールごとのタスクがわかりにくかった
  2. 優先度が高いPBIが多く、その全てのPBIをリファインメントで説明できるよう準備していたため、時間がかかっていた
  3. 優先度は数値で管理していたが、スプリント内での優先順位が明確になっていなかった
  4. デザイナー向けの説明に開発メンバーが長時間付き合う必要があり、非効率だった

POになったばかりということもあり、リファインメントがうまく進まないことで、プランニングも滞り、悪循環が続いていました。

リファインメントの改善

リファインメントについては以下のような改善を実施しました。

  1. 各ロール(PO・デザイナー・開発メンバー)が「次スプリントで何をやるか?」を可視化しました
    リファインメントの改善
    これによって1スプリント内のチーム全体のタスクがパっとみてわかるようになりました
  2. POは次スプリントに向けたPBIに絞って準備をするようにしました
  3. タスクの優先度を可視化し、POがMUSTでリリースしたいタスクはどれか?といった話をリファインメントで相談するようにしました
    タスクの優先度は左側の方が高い
  4. POとデザイナーが定期的に打ち合わせを行い、事前に次スプリントのPBIを確認するようにしました
    これにより、リファインメント当日は開発メンバーへの説明に集中でき、理解促進や疑問解消に注力できるようになりました

結果として、PO自身の業務時間にも余裕が生まれ、クライアントとのコミュニケーションや現場課題の発見に、より多くの時間を充てられるようになりました。

終わりに

ここまで、スクラムイベントで直面した課題と、その改善策についてご紹介してきました。
私自身、スクラムの経験が浅く、迷うことも多々ありましたが、「まずは基本を知ることの大切さ」を強く実感しています。

最近では、優先順位の付け方やレトロスペクティブの進め方についても、さらなる改善に取り組んでいます。
今後も試行錯誤を重ねながら、より顧客にとって価値の高いプロダクトを作るために、チームに合ったスクラム運用を目指していきます。

一緒に働く仲間を募集しています!

スタイル・エッジでは、共に働いてくれる仲間を募集しています! もし興味をお持ちいただけましたら、ぜひ採用サイトをご覧ください!

recruit.styleedge.co.jp

AWS Security Hubを導入して社内セキュリティ対策の標準化を推進 〜改修・予防編〜

はじめに

こんにちは!スタイル・エッジのSREチームでPjMを担当しているpeipeiです!
サービス全体の信頼性を高めるべく、SRE文化の浸透や、それを支える仕組みづくりに日々取り組んでいます。

前回の記事では、AWS Security Hub(以下、Security Hub)を導入する背景や、可視化による課題発見についてご紹介しました。

今回はその続編として、Security Hubを活用したセキュリティ対策の「標準化」をどのように進めているのかをご紹介します。

対応方針 〜「可視化 ⇒ 改修 ⇒ 予防」のアプローチ〜

私たちはセキュリティ対策を効果的に進めるため、「可視化 ⇒ 改修 ⇒ 予防」の3ステップでアプローチしています。段階的に対応することで、対応の抜け漏れを防ぎながら継続的な改善を目指しています。

  1. 可視化:現状の課題を明確にし、全体の状況を把握する(前回の記事で紹介)
  2. 改修:SREチームがプロダクト開発チームと連携して修正を進める(今回の記事)
  3. 予防:同じ課題が再発しないよう仕組み化する(今回の記事)

社内のAWSアカウントを統合したダッシュボードの作成や、チャット通知機能の実装によって課題の可視化が進んできています。 次のステップは、可視化された課題にどう対処し、再発をどう防ぐかという点です。

2. 改修 - SREチームがプロダクト開発チームと連携しながら修正を進める

対応ガイドラインの作成

改修作業は各プロダクトの担当者が主体で進め、判断が難しい項目についてはSREチームと一緒に対応方針を検討します。

ただし、いきなり各プロダクトへ対応を依頼をしても、担当者によって対応の質や優先度が異なり、結果として期待通りの進捗を確保できない懸念がありました。そこで、対応方針の統一化を図るため、ガイドラインを作成しました。

ガイドラインには、「Security Hubで指摘されているリソースの確認方法」、「優先順位の考え方」、「対応方針を示したナレッジへの案内(後述)」などを記載しています。

また現在では、各プロダクトの担当者が単独でコントロールを「無効化*1」することや、検出結果を「抑制*2」することは避け、SREチームとの協議を経て実施する運用としています。

「無効化」は社内のポリシーとして検出する必要のないコントロールに対して行うようにしており、「抑制」は以下のようなケースで行うようにしています。

【抑制を行うケース】
1. 正当な理由がある場合
  例:正当な理由による特定のポートやプロトコルの使用、業務上必要な例外的なリソース構成。
2. リスクの低い問題であると判断した場合
  例:テスト環境や非本番環境での低リスクな問題、期間限定で許容されるリソース構成。

説明会の実施

プロダクト全体の巻き込みを図るため、各プロダクトの担当者を招集し、ガイドラインに関する説明会を実施しました。説明会ではガイドラインの内容を解説するとともに、質疑応答の時間を設け、参加者が内容を理解し、納得した上で改修作業に取り組めるよう配慮しました。

説明会で寄せられた質問とその回答は、ガイドラインのQ&Aセクションに反映し、内容のさらなる充実を図りました。その結果、説明会後に新たに加わったメンバーも、ガイドラインを参照するだけで適切な対応ができており、ガイドライン作成の効果を実感しています。

ナレッジ共有の仕組み化

Security Hubの対応で共通するケースが多いため、ナレッジをNotionに集約し、全プロダクト担当者が閲覧可能な共有基盤を整備しました。これにより、対応方針の属人化を防ぎ、作業効率の向上を目指しています。

ナレッジでは、「主な対応方針」や「対応時の注意事項」のほかに、「先人の知恵」と「参考資料」という項目を設けています。 「先人の知恵」には、過去に同様の対応を行ったプロジェクト担当者が、対応時のチケットリンクなどを貼り付けています。 また、「参考資料」には、調査時に使用した資料などを記録するようにしています。 これにより、次に対応する人や、過去の対応を振り返りたいときに、ノウハウが蓄積された状態を維持できるようにしています。

当初はSREチーム主導でナレッジ作成を行っていましたが、次第にプロダクト側から「新規でナレッジを書いても良いか?」という声も上がるようになり、文化として定着しつつあります。

対応方針を検討する上で、Classmethod様のAWS Security Hub ガイドは非常に参考になっています。各コントロールの解説が詳細であるだけでなく、説明の裏付けとなる情報源へのリンクも豊富に提供されており、効率的な対応が可能になっています。

無効化処理の効率化

前述の通り、Security Hubのコントロールの中には、社内ポリシーに基づき「無効化」とし、チェック対象外とする項目があります。例えば、弊社では[RDS.13] RDSのマイナーバージョンの自動アップグレードが有効であることが該当します。

弊社は、マイナーバージョンアップであっても、検証環境での動作確認後に本番環境へ手動適用する方針としています。これは、過去に筆者が経験したように(こちら) 、検証が不十分な状態でのバージョンアップは予期せぬ問題を引き起こす可能性があるためです。

そのようなコントロールの無効化を手動でアカウントごとに管理するのは非効率なため、自動化フローを整備しました。具体的には、無効化対象のコントロールを記述したYAMLファイルをLambda関数に渡し、全AWSアカウントの該当項目を一括で無効化します。

工夫したポイント
  • コントロールの無効化リストをGitHub Actions経由でS3にアップロード
    mainブランチへのマージをトリガーに、コントロールの無効化リストが自動でS3にアップロードされる仕組みです。S3のバージョニングも有効にしており、誤操作時の復旧を可能にしています。当初はJSON形式でしたが、コメント記述の容易さからYAML形式に変更しました。
# コントロールの無効化リスト
AWS-SecurityHub-Controls:
  # 2025/01/22 SREにて判断(参考:https://○○○○○)
  RDS.13: "マイナーバージョンアップは自動で行う方針のため無効化"
  # 2025/02/04 SREにて判断(参考:https://△△△△△)
  CloudTrail.5: "CloudWatchLogsではなくS3の保存で十分であると判断して無効化"
  • S3のPUTイベントをトリガーにLambda関数を実行
    CDKでリソース構築していますが、Lambda関数はデプロイ後の自動実行ではないため、トリガーにS3 PUTイベントを選択しました。この関数がAssumeRoleを用いて各メンバーアカウントのSecurity Hub設定を一括変更します。
    リソースはセキュリティ統括アカウントに集約管理し、メンバーアカウントにはAssumeRole用のIAMロールのみを配置しました。セキュリティ統括アカウントへのアクセス権限を最小化し、誤操作による設定変更を防いでいます。

定期的なコントロール項目の見直し

Security Hubのコントロールは常に変化するため、「抑制」や「無効化」設定の放置はリスクになります。また、インフラ構成の変化により、過去の判断が現状に合わないこともあります。継続的な見直しが理想ですが、運用負荷を考慮し、3ヶ月ごとのレビューをルール化し、SREチームのカレンダーに定期イベントとして登録しています。

3.予防 - 同じ課題が再発しないように仕組み化する

CI/CDでのセキュリティチェックの自動化

改修体制の整備に加え、重要なのは脆弱性のあるリソースの新規作成を防ぐことです。Security Hub運用を通じて、既存リソースの改修はシステム影響が大きく、慎重な調整が必要と分かりました。

そのため、開発プロセスにおけるセキュリティリスク検知を目指し、CloudFormationテンプレート等の構成定義ファイルの作成・更新時にチェックを導入しました。具体的には、cfn-nagをCI/CDパイプラインに組み込み、mainブランチへのマージ前に自動検証を行っています。

工夫したポイント
  • cfn-nagの検知除外機能
    セキュリティリスクの検知結果は、社内ポリシーに基づき対応不要な場合も考えられます。そのため、--deny-list-path オプションを利用して、チェック対象から除外できる仕組みを導入しました。
# スキャンの実行(deny-list付き)
cfn_nag_scan --input-path <CloudFormationテンプレート名> --deny-list-path ./settings/cfn-nag_deny_list.yml
# cfn-nag_deny_list.ymlの例
RulesToSuppress:
  - id: W9
    reason: "Public IP for bastion host, access controlled by security group. No operational issues."
  - id: W28
    reason: "Full S3 access required for dedicated batch job user only. No other operations."
  - id: W40
    reason: "Temporary SSH access for maintenance, will be restricted after completion. No operational issues."
  • cfn-nagの複数ファイル解析
    cfn-nagは通常、複数のCloudFormationテンプレートを解析する際、いずれかのファイルでFAILが検出されると、その時点で処理を中断します。
    たとえば、01_NW/vpc-sg.yamlおよび03_APP/apigateway.yamlの両方に脆弱性が存在し、FAILが発生する設定になっていた場合でも、以下の例のように、先にFAILが見つかったファイルのみが対象となり、残りのファイルは解析されません。
# FAIL発生時の挙動(1ファイルのみ解析)
▶︎Run cfn-nag on changed files
------------------------------------------------------------
01_NW/vpc-sg.yaml
------------------------------------------------------------------------------------------------------------------------
| FAIL F1000
|
| Resource: ["RDSSG"]
| Line Numbers: [49]
|
| Missing egress rule means all traffic is allowed outbound.  Make this explicit if it is desired configuration
------------------------------------------------------------
Failures count: 1
Warnings count: 0

Error: Process completed with exit code 1.

この問題を解決するため、FAILが発生しても解析を継続し、全てのファイルの検証結果を得られるようにしました。以下のGitHub Actionsのコードでその処理を実現しています。

      # FAILをカウントしながら複数ファイルを解析する処理
      - name: Run cfn-nag on changed files
        # ... 省略 ...
        run: |
          error_count=0
          for file in ${{ steps.security_check.outputs.changed_files }}; do
            cfn_nag_scan --input-path "$file" --deny-list-path ./settings/cfn-nag_deny_list.yml || {
              # FAIL発生時にカウントアップ
              error_count=$((error_count + 1))
            }
          done
          echo "ERROR_COUNT=$error_count" >> $GITHUB_OUTPUT

      - name: Fail if cfn-nag errors occurred
        if: steps.run-cfn-nag.outputs.ERROR_COUNT > 0
        run: |
          echo "cfn-nag validation failed for ${{ steps.run-cfn-nag.outputs.ERROR_COUNT }} file(s)."
          # 全ての解析後にFAILがあればワークフローを失敗させる
          exit 1

この変更により、複数のファイルにわたるセキュリティリスクを網羅的に検知できるようになりました。以下の例のように、複数のファイルでFAILが検出されます。

# 改善後の挙動(複数ファイルを解析)
▶︎Run cfn-nag on changed files
------------------------------------------------------------
01_NW/vpc-sg.yaml
------------------------------------------------------------------------------------------------------------------------
| FAIL F1000
|
| Resource: ["RDSSG01"]
| Line Numbers: [49]
|
| Missing egress rule means all traffic is allowed outbound.  Make this explicit if it is desired configuration
------------------------------------------------------------
Failures count: 1
Warnings count: 0

------------------------------------------------------------
03_APP/apigateway.yaml
------------------------------------------------------------
| FAIL F38
|
| Resource: ["APIGatewayRole"]
| Line Numbers: [146]
|
| IAM role should not allow * resource with PassRole action on its permissions policy
------------------------------------------------------------
Failures count: 1
Warnings count: 0

▶︎Fail if cfn-nag errors occurred
cfn-nag validation failed for 2 file(s).
Error: Process completed with exit code 1.

この実装により、パイプラインは複数のCloudFormationテンプレートに潜在するセキュリティリスクを漏れなく検出し、開発者は全ての問題を把握した上で対応を検討できるようになりました。その結果、セキュリティのシフトレフトが促進されることを期待しています。

今後の展望

Security Hubの改修運用を継続的に推進し、サービス全体のセキュリティレベル向上を図ります。将来的には、新規ローンチされるすべてのプロダクトにおいて「Security Hub指摘ゼロ」を目指していきます。

また現在、手作業で行っているSecurity Hubの検知項目に対するイシュー起票プロセスについては、運用効率化を目的に、自動化の仕組みの構築を検討しています。

連載予定

投稿日 タイトル
5月 SREが“消火活動”に本気で向き合って見えた、信頼性向上へのリアルな一歩
6月 SREとしてはお休み
7月 ポストモーテム文化の醸成

※内容や更新予定日は変更になる可能性があります。
連載を通じて、SREチームのチャレンジや日々の気づきを発信していきます。
「SREって何から始めればいいの?」と悩む方や、自社の運用課題に向き合っている方の参考になれば嬉しいです。


一緒に働く仲間を募集しています!

スタイル・エッジでは、SRE文化の構築に向けてともに挑戦していただける仲間を募集中です。
ご興味をお持ちいただけた方は、ぜひ採用サイトもご覧ください!
recruit.styleedge.co.jp

*1:Security Standards内の特定のコントロール自体を無効化する。(チェック自体をやめる)

*2:特定の検出結果(Finding)に対して理由をつけて除外(チェックはするが通知はしない)

AWS Security Hubを導入して社内セキュリティ対策の標準化を推進 〜取り組み背景・課題の可視化編〜

はじめに

こんにちは!スタイル・エッジのpeipeiです。

前回の記事では、プロダクト横断型SREチームの発足についてお話ししました。 SREチームとして、組織全体の信頼性向上を目指し、複数のプロダクト開発チームを支援しながらSRE文化の構築に挑戦しています。

今回はその連載の続編として、AWS Security Hubを活用したセキュリティ対策の標準化 についてお話しします!

AWS Security Hubとは

AWS Security Hub(以下、Security Hub)は、AWS環境のセキュリティ監視・評価を一元化し、さまざまな基準に基づいたコンプライアンスチェックを実施するサービスです。 また、Amazon GuardDuty、Amazon Inspector、Amazon MacieなどのAWSセキュリティサービスや、サポートされているサードパーティ製品の検出結果(Findings)を統合できます。

実際に使ってみると、Security Hubを有効にするだけでAWSのベストプラクティスに沿ったチェックが自動で実行されるため、とても便利です。しかし、初めて使うと検出されるFindingsの量が多すぎて、「どこから手をつければいいの?」と戸惑ってしまいました。本記事では実践を通して得られた効率的な活用方法についても紹介します。

セキュリティ対策の標準化に動いたきっかけ

特に深刻なセキュリティインシデントは発生していませんが、会社の規模拡大に伴いシステムの社会的責任が一層重くなっていることを実感しており、セキュリティレベルのさらなる向上が不可欠となっています。

各プロダクト開発チームでセキュリティ対策は進められていましたが、システム全体の標準化を進めることで、より高いセキュリティレベルを実現できると考えました。以前からSecurity Hubを導入していましたが、セキュリティの抜け漏れを防ぐため、SREチームが全体の統率を担うことにしました。

対応方針〜「可視化 ⇒ 改修 ⇒ 予防」のアプローチ〜

セキュリティ対策を効果的に進めるため、私たちは「可視化 ⇒ 改修 ⇒ 予防」の3ステップでアプローチを行っています。

  1. 可視化:現状の課題を明確にし、全体の状況を把握する
  2. 改修:SREチームがプロダクト開発チームと連携しながら修正を進める
  3. 予防:同じ課題が再発しないよう仕組み化する

今回は可視化について詳しく見ていき、改修と予防については次回の記事で紹介します。

1. 可視化 - 現状のセキュリティ課題を明確にする

まず、課題を明確にしなければ、適切な対策を講じることはできません。Security Hubを中心に、全AWSアカウントのセキュリティ状況を一元管理できる仕組みを構築しました。

セキュリティ統括アカウントの作成と集約

複数のAWSアカウントを運用しているため、各アカウントごとにSecurity Hubの管理を行うのは手間がかかります。そこで、すべてのAWSアカウントのセキュリティ状況を一元管理するために、「セキュリティ統括アカウント」を作成しました。

さらに、アカウントの集約だけでなくリージョンごとの集約も行い、より効率的に管理を行えるようにしています。これにより、セキュリティ統括アカウントから全てのAWSアカウントのセキュリティ状況を一元的に把握できるようになりました。

※ 弊社では現在、AWS Organizations(以下、Organizations)を一部の環境を除いて使用していません。そのためアカウントの紐付けは、セキュリティ統括アカウントからメンバーアカウントを招待し、受諾するプロセスで行っています。

Security Hub でのメンバーアカウントの追加と招待 - AWS Security Hub

ダッシュボードの作成

Security HubのFindingsを可視化するために、事前に条件を設定したダッシュボード(カスタムインサイト)を作成しました。このダッシュボードを使用することで、AWSアカウントごとのセキュリティリスクを一覧表示し、全体の状況を一目で把握できるようになりました。

特に、重要度の高いFindingsをフィルタリングすることで、優先度の高いリスクから効率的に改修を進めやすくなっています。

カスタムインサイトの作成 - AWS Security Hub

通知機能の実装(Chatwork連携)

Security HubのFindingsをリアルタイムで関係者に通知するため、Chatworkへの自動通知機能を構築しました。これにより、セキュリティリスクの高いリソースが作成された際にChatworkへ即時通知され、迅速な検知が可能になりました。

当初、私はOrganizationsを使用せずに統合管理している場合、各アカウントごとにAWS EventBridge(以下、EventBridge)とAWS Lambda(以下、Lambda)をデプロイする必要があると思い込んでいました。しかし、Organizationsを使用していなくても、EventBridgeとLambdaはセキュリティ統括アカウントにのみ作成すれば問題ありませんでした。

セキュリティ統括アカウントでは、他アカウントのFindingsも同一のEventBridgeで検知できます。 また、統括アカウントで発行されるFindingsには対象アカウントのIDが含まれているため、どのアカウントで検出されたFindingsかを識別できます。

また、EventBridgeのイベントパターンを制御することで、通知対象を重要度の高いものに限定し、それ以外のFindingsは定期的に AWSコンソールで確認する運用としています。 本来であれば全ての通知に対応するのが理想ですが、検知されるFindingsの量が膨大で「狼少年アラート」になってしまうリスクがあるため、特に重要なもののみを通知し、それ以外は定期的にAWSコンソール上で確認する方針としました。

EventBridgeのイベントパターンのイメージ

{
  "detail-type": ["Security Hub Findings - Imported"],
  "source": ["aws.securityhub"],
  "detail": {
    "findings.Compliance.Status": ["FAILED"],
    "findings.Severity.Label": ["CRITICAL, HIGH"] <= 通知するレベルを限定する
  }
}

おわりに

ここまで、「可視化」のために実施した取り組みを紹介しました。 Security Hubを活用してセキュリティリスクを一元管理し、カスタムダッシュボードや通知の仕組みを整えることで、全体のセキュリティ状況を明確に把握できるようになりました。 今後は、Security Hubが検知したセキュリティの問題に対して、自動的にチケットを作成して担当者をアサインする仕組みを導入したいと考えています。

可視化はあくまでスタートラインで、それだけではリスクは減りません。重要なのは、その結果をどのように活用し、具体的な改修や予防策へと繋げるかだと考えます。

次回は、

2.「改修」:SREチームがプロダクト開発チームと連携しながら修正を進める
3.「予防」:同じ課題が再発しないよう仕組み化する

について詳しく解説していきます!
次回の記事も、ぜひチェックしてみてください!

連載予定

投稿日 タイトル
4月 AWS Security Hubを導入して社内セキュリティ対策の標準化を推進 〜改修・予防編〜
5月 SREが“消火活動”に本気で向き合って見えた、信頼性向上へのリアルな一歩
6月 SREとしてはお休み

※内容や更新予定日は、追加・変更の可能性があります。

連載を通して、SREチームのチャレンジや、日々の気づきをお伝えしていきます。我々と同じように「SREって何から始めればいいの?」と悩む方や、自社の運用課題を抱える方の参考になっていければ幸いです。

一緒に働く仲間を募集しています!

スタイル・エッジでは、SREチームの立ち上げを共に進めてくださる仲間を募集しています!
もし興味をお持ちいただけましたら、ぜひ採用サイトをご覧ください!
recruit.styleedge.co.jp

【連載始めます】SREチームを立ち上げました!

はじめに

こんにちは!スタイル・エッジのpeipeiです。

この度、弊社ではプロダクト横断型の SREチーム を発足しました!
組織全体の信頼性向上を目指し、複数の開発を支援するチームとして、SRE文化の構築に挑戦しています。

この技術ブログの連載を通して、私たちが取り組む課題や解決のプロセスをリアルにお伝えしていきます。

SREとは

SRE(Site Reliability Engineering)は、システムの信頼性を向上させるためのエンジニアリング手法とされています。
Googleが提唱したこの概念は、システムの可用性・スケーラビリティ・パフォーマンスを維持、向上させることを目的とし、運用をコードで自動化することで開発と運用のギャップを埋めるという考え方に基づいています。
また、SLO(Service Level Objective)*1やエラーバジェット*2を活用し、信頼性と開発スピードのバランスを最適化することが特徴です。

一般的にはこのように説明されますが、私たちは SREの定義を簡潔にし、次のように解釈しています。

ユーザーへ提供しているサービスの機能を
利用したいタイミングで、意図したとおりに快適に利用できる状態を実現するために、
ソフトウェアエンジニアが設計やアプローチを考え、実践する活動。

SREチームが取り組む課題

弊社のSREチームは以下の課題に取り組んでいきます。
今後も様々な課題にぶつかる中で、取り組みが増えていくことかと思います。

  1. SLI(Service Level Indicators)*3を通じてシステムの状態を数値化し、SLOとしてユーザー体験の基準を設けることで適切な信頼性の目標を定める。
  2. システム障害やヒヤリハットを通じて得た学びを活かし、組織全体で改善を進める文化を育む。効果的なポストモーテム*4文化を確立し、各チームへ再発防止策を横展開する仕組みを構築する。
  3. オブザーバビリティを向上させることで、システムの健康状態を可視化したり、障害発生時の原因を迅速に特定できるようにする。
  4. 各チームのセキュリティ対策を標準化し、ばらつきをなくすことで、組織全体の安全性を向上させる。
  5. インフラストラクチャのコード化を推進し、自動化によって手作業のオペレーションを削減する。また、インフラ構成の一貫性を保ち、静的解析ツールを活用してセキュリティチェックを自動化することで、安全性と運用効率を向上させる。
  6. 各チームで実施したEOL(End of Life)対応や運用ノウハウをSREが集約し、類似の作業や他のプロダクトにも活用できる仕組みを整える。
  7. トイル*5を削減し、手作業によるミスを防ぐことで、システムの信頼性を向上させる。また、運用負荷を軽減し、エンジニアがより価値の高い業務に集中できる環境を整える。

連載予定

投稿日 タイトル
3月 AWS Security Hubを導入して社内セキュリティ対策の標準化を推進 〜取り組み背景・課題の可視化編〜
4月 AWS Security Hubを導入して社内セキュリティ対策の標準化を推進 〜改修・予防編〜
5月 SREが“消火活動”に本気で向き合って見えた、信頼性向上へのリアルな一歩

※内容や更新予定日は、追加・変更の可能性があります。

これから連載を通して、SREチームのチャレンジや、日々の気づきをお伝えしていきます。我々と同じように「SREって何から始めればいいの?」と悩む方や、自社の運用課題を抱える方の参考になっていければ幸いです。次回をお楽しみに!

一緒に働く仲間を募集しています!

スタイル・エッジでは、一緒に働く仲間を募集しています!
スクラムマスターやプロダクトオーナーも絶賛募集中です。
もし興味をお持ちいただけましたら、ぜひ採用サイトをご覧ください!
recruit.styleedge.co.jp

*1:事業者がサービスを展開するにあたって設定する、サーバーやネットワークなどの「稼働率」「性能」「可用性」「セキュリティ」といった項目ごとのパフォーマンスの目標値や品質の評価基準のこと。

*2:SLOからの逸脱が許容される範囲を数値化したものであり、信頼性と変更速度のバランスを取るための指標として機能する。

*3:サービスの性能や信頼性を測定する具体的な指標であり、ユーザー体験に直接影響を与える要素を数値化することで、客観的な評価を可能にする。

*4:システム障害発生後に、その原因や影響、対応プロセスを振り返り、今後の再発防止策を策定するための分析・ドキュメント作成のプロセスのこと。

*5:手作業、繰り返される、自動化が可能、戦術的、長期的な価値がない、サービスの成長に比例して増加する、といった特徴を持つ作業のこと。

インセプションデッキでスクラム改善してみた!

はじめに

こんにちは!システム事業部のkamです。
私は2024年9月に中途で入社し、現在はあるプロジェクトのスクラムマスターとして業務に携わっています。

以前、弊社のブログで公開された記事「アジャイルソフトウェア開発ってなんなのだ。 - スタイル・エッジ技術ブログ」では主にスクラム・アジャイルの概要について解説していました。

今回は、私が実際にスクラムマスターとして取り組んでいる内容の一つであるインセプションデッキについてお話ししたいと思います。

自己紹介

とはいえ、まずは私が何者なのかを簡単に自己紹介します。

私は40代のエンジニアで、これまでPM(プロジェクトマネージャー)やPL(プロジェクトリーダー)、SM(スクラムマスター)、PO(プロダクトオーナー)などの役割を経験してきました。
その一環として、認定スクラムマスター(CSM)と認定スクラムプロダクトオーナー(CSPO)の資格も取得しています。
また、以前はスクラムに関する講義を行い、知見を共有する活動もしていました。ファシリテーションやコーチングも好きです。

前述したとおり、最近入社したばかりですが、「面白そうな会社だな」と感じて入社を決めました。
もちろん大変な部分もありますが、実際に働いてみて「やっぱり面白い」と感じることが多いです。

プロジェクト概要

現在携わっているプロジェクトの体制は以下の通りです。
メンバー構成はPO(プロダクトオーナー)1名、SM(スクラムマスター)1名(私)、開発メンバー5名、デザイナー1名です。
複数のお客様先で使用されているシステムを、より使いやすく改善する取り組みを2週間スプリントで進めています。

インセプションデッキとは

インセプションデッキとは、チームの立ち上げ時などに、プロジェクトのビジョンやゴールをチーム内外で共有するためのものです。
これにより、チームの相互理解を深めることを目的としています。

インセプションデッキでは、以下の10のワークを通して、プロジェクトの全体像を描きます。

  1. 我々はなぜここにいるのか:プロジェクトの目的・背景を明確にする
  2. エレベーターピッチ:短い言葉で、プロジェクトの価値や目的を伝える
  3. パッケージデザイン:製品やサービスを「商品」として表現し、魅力を整理する
  4. やらないことリスト:プロジェクトのスコープ外を明確にし、フォーカスを絞る
  5. ご近所さんを探せ:ステークホルダーや協力者を明確にする
  6. 解決案を描く:どのように課題を解決していくかの方向性をまとめる
  7. 夜も眠れない問題:チームが抱えている懸念やリスクを整理し、対策を検討する
  8. 期間を見極める:いつまでに何を達成すべきか、スケジュール感を決める
  9. トレードオフスライダー(優先順位):「コスト」「品質」「スピード」など、優先すべき要素を決める
  10. 何がどれだけ必要なのか:必要なリソースやスキルを洗い出す

インセプションデッキをやってみた!

私がスクラムマスターとしてジョインする以前から、インセプションデッキ自体はチームで行っていたようです。

しかしながら、作成したインセプションデッキを各スクラムのイベントでうまく使えていませんでした。
そのため、

  • プロダクトゴール・ロードマップがあやふやで、POが何を調査すればよいかわからない・・・
  • エンジニアの優先度が明確になっておらず、開発が遅れてリリース予定も遅れてしまう・・・

という状態でした。
プロジェクトのビジョンやゴールの共有が不足しているのではないかと感じ、スクラムマスターとしてインセプションデッキを作り直すことにしました!

ただ、インセプションデッキを一度にすべて作り直すには、まとまった工数が必要になります。
そこで、日々の業務と並行し、毎週決まった時間に少しずつインセプションデッキをブラッシュアップしていくことにしました。

手始めに「ご近所さんを探せ」を行い、ステークホルダーや協力者についてチームで話し合いました。

「ご近所さんを探せ」をすることによって、これまで一部のメンバーしか把握できていなかった関係者を全体で共有することができました。
「エレベーターピッチ」も見直し、次回以降は「トレードオフスライダー」や「夜も眠れない問題」などのワークを進めていきます。

少しずつではありますが、POの優先順位の決定がスムーズになりました。
インセプションデッキを作成する過程で、プロジェクトのビジョンをより深く考えることができ、それが大きな改善につながったのではないかと感じています。

これからの展望

現在、システム事業部では4つのプロジェクトで「PO(プロダクトオーナー)、SM(スクラムマスター)、開発チーム」の体制で開発を行っています。
そのうちの1つのプロジェクトで、新たにSMが誕生したので、そのフォローも私が担当しています。
さらに、POの経験が浅いプロジェクトもあるため、POの成長を支援することも私の重要な役割の一つです。

2024年12月から「アジャイル推進セクション」が新たに発足しました。
私もここに携わり、よりスクラム含めたアジャイルの推進をし、より効果的で柔軟な開発体制を構築し、さらなる成長を目指していきます!

一緒に働く仲間を募集しています!

スタイル・エッジでは、一緒に働く仲間を募集しています!
スクラムマスターやプロダクトオーナーも絶賛募集中です。
もし興味をお持ちいただけましたら、ぜひ採用サイトをご覧ください!
recruit.styleedge.co.jp

エンジニアが司法書士事務所で現場体験!ユーザー視点で見えたシステム改善の秘訣

はじめに

こんにちは、sugitenです!
システムエンジニア歴3年で、現在は自社プロダクトであるLeadU⁺債務整理の開発チームに所属し、開発や運用・保守の業務を担当しています。

前述の通り、私は普段システムエンジニアとして業務に携わっているのですが、後述する経緯により、LeadU⁺債務整理の実際の利用者である司法書士事務所様にて実際の業務を体験させていただく機会がありました。
今回はそんな事務所様での業務体験談について執筆しようと思います!

システム紹介

LeadU⁺債務整理は、弁護士・司法書士事務所向けに開発されたBtoBのシステムです。
債務整理案件の管理運営業務に特化した業務支援システムで、

  • お客様からの問い合わせ~ヒアリング対応
  • 受任後の和解交渉管理から月々の返済シミュレーション
  • 返済開始後は、入金の自動マッチングや銀行とのAPI連携による入金取り込み
  • 出金計画に基づいた出金作業のフルオート化

といった、受付から精算までの様々な業務をシステム上で完結しています。

現場で業務体験をすることになった経緯

私たちの開発チームは、基本的にはクライアント様の要望に基づいて機能修正や新規開発を行っています。
チームには途中参画のメンバーも多く、またクライアント様の事務所を訪れた経験のあるメンバーも少ないということもあり、現場業務やシステムの本質的な理解が不足していたため、開発者発信での改修やUI改善の提案が満足に行えていないという課題がありました。

そんな中、クライアント様から「カスタマーサクセス業務の生産性をさらに上げるため、システムを改善してほしい」という要望がありました。
現場では、電話でお客様とやり取りをしながら、その内容をシステムに入力していくという作業が発生します。
この入力がもっと簡易になれば、会話に集中でき、業務効率アップにつながるのではないかという期待がありました。

これを受けて、私たち開発チームが実際に現場で業務を体験し、システムの問題点を抽出したいという話になりました。
この話をクライアント様に持ち掛けたところ、快くご協力いただき、エンジニアが実際に現場業務を行うことを特別に認めていただきました。

produced by DALL-E

体験記

業務内容

クライアント様の事務所に5日間出向させていただき、事務所の方と同等の十分な研修を受けた後に主に以下のような業務を行いました。

  • 架電業務:問い合わせがあった方へ電話をかけ、借入状況や収支状況をヒアリングしました。
  • 受電業務:広告を見てかかってきた電話を取り、初期対応を行いました。
  • 情報入力:ヒアリングした内容を、電話しながらシステムに入力していきました。

業務中は、システムについて気になった点や使いづらいと感じた部分を都度メモし、後で振り返れるようにしました。

現場業務の感想

エンジニアとして働いてきた私にとって、カスタマーサクセス業務は初めての経験でした。

まず驚いたのが、1日に3回行われる目標達成度合いの確認ミーティングです。
目標達成への意識が非常に高く、チーム全体が一丸となって目標達成に向けて取り組む姿勢は印象的でした。
その日のリーダーによって進行方法が異なり、士気を高めるための工夫が随所に見られました。

また、営業フロアの現場の活気ある雰囲気は、普段静かな環境で開発を行っている私にとって新鮮でした。
メンバー同士の情報共有も盛んで、チームワークの強さも感じました。

何より一番の学びは、直接ユーザーと接することで、システムの使い勝手や改善点についての生の声を聞くことができたことです。これは、エンジニアとして非常に貴重な経験でした。また、現場で働く皆さんのプロ意識や情熱に触れ、自分ももっと頑張らなければと刺激を受けました。

気づき(実際に現場業務でシステムを使ってみて)

実際に現場の業務フローに則ってシステムを操作してみると、いくつかの使いづらい点に気が付きました。

まず、システムで出力したデータをスプレッドシートを用いて別条件で再計算する必要があったり、複数のオペレータが同じお客様に架電をしないように架電時に都度チャットツールで共有していたりと、 私たちには見えていなかったシステム外での運用が多々ありました。

また、頻繁に使用するボタンが見つけにくい位置にあるために操作性が低下していることが分かりました。
具体的には問い合わせ情報入力画面の項目数が多く、お客様と電話をしながら入力する際に相当の時間がかかってしまいました。

上記の気づきに関連して、ボタンの配置や大きさを少し変えるだけでも操作性が大幅に向上するという気付きがありました。
フロントエンドで完結する簡単な修正でも、ユーザーにとっては大きなインパクトがあることを身をもって実感できた貴重な経験です。

業務体験後の変化

心境の変化

実際に現場で働く方々と接し、業務体験をするなかで、ユーザーがどのような課題やニーズを持っているのかを肌で感じられました。
これにより、これまで漠然と捉えていたユーザー像がより具体的になりました。

また、システムの先にいるお客様の方々と直接やり取りをすることで、自分の仕事が社会と深く関わっていることを実感しました。
これは、BtoBのシステム開発においてはなかなか得られない貴重な経験で、自分の仕事が誰かの役に立っているという実感が、仕事へのモチベーションを高めてくれました。

自分が開発したシステムを実際に使い、その中で改善点を見つけることで、システムへの愛着が一層深まりました。ユーザーの立場に立ってシステムを見直すことで、今後の開発においてもユーザー視点を大切にしていきたいと強く感じました。

取り組みの変化

業務体験後は、開発業務への取り組みにも変化が生まれました。
実際にユーザーとして業務のなかでシステムを使う経験をしたことで、ユーザーの立場になって考えることができるようになりました。
具体的には、ユーザーの業務フローや使い勝手を意識し、ユーザー視点での開発を心がけるようになりました。

さらに、ユーザーとの直接のコミュニケーションを通じて、ユーザーからのフィードバックの重要性を再認識しました。
現場での体験を共有することで、クライアント様との関係性が深まり、要望の解像度も上がりました。その結果、ユーザー満足度の高い機能開発が可能になりました。

おわりに

今回の業務体験を通じて、現場での経験がエンジニアとしての成長に大きく寄与することを実感しました。
こうした取り組みができるのも、総合支援をおこなっているスタイル・エッジならではだと思います!

スタイル・エッジでは、エンジニアが積極的に現場を知り、ユーザーに寄り添った開発ができるようサポートしています。
こうした経験を通じて、一緒により良いシステムを作り上げていきませんか?

最後まで読んでいただき、ありがとうございました!

recruit.styleedge.co.jp

React で QR コードを生成して、タイトルを QR コードの下に描画したうえで画像ファイルとしてダウンロードしてみたい!!

はじめに

こんにちは、しおです。少し前までひどく暑かったのに、もうすっかり年の瀬ですね。

冬っぽい画像を生成してみたので、ぜひ癒されてください。

癒されましたね。

さて、いま私が開発で携わっているプロダクトにて、『QR コード*1をアプリケーション内で生成して画像として表示&ダウンロードしたい。その際に、任意のタイトルを画像に埋め込みたい。』という要件が発生しました。

そこで、今回はその対応で実際に使用した qrcode.react という React 向けのライブラリを用いて React アプリケーション内で QR コードとタイトルを描画したうえで画像ファイルとしてダウンロードさせるまでの流れを紹介します。


目次


qrcode.react の紹介

QR コード生成用のライブラリとして、qrcode.react を使用します。

www.npmjs.com

このライブラリは、QR コードを Canvas または SVG で描画でき、JSX でシンプルな記述かつ柔軟なカスタマイズができるためお気に入りです。
公式ドキュメントでも推奨されているように SVG の方が設定に柔軟性があるようですが、今回は後からタイトルを追加で描画するために Canvas を使用します。

qrcode.react では、コンポーネントの読み込み時にいくつかのプロパティを設定するだけで QR コードのアプリケーション内での生成が可能です。

いろいろなプロパティ

先述した通り QR コード描画時にいろいろなプロパティが設定できますが、代表的なものをいくつかピックアップして紹介します。

title

QR コードにタイトルを付与できます。ただし、これは今回のように SVG や Canvas 内に描画するためのものではなく、アクセシビリティやSEO目的で SVG や Canvas に title 要素を埋め込むためのものです。

私がやりたかったことは QR コードの下にタイトルを表示することだったので、このプロパティを使用するのではなく自前で実装することにしました。

level

QR コードのエラー訂正レベルを指定するプロパティです。

なお、エラー訂正レベルは、QR コードが部分的に破損した場合でもデータを復元できる割合を示します。

  • L (Low): データの約7%を復元可能。最も冗長性が低く、その分容量も最小。
  • M (Medium): データの約15%を復元可能(デフォルト)。
  • Q (Quartile): データの約25%を復元可能。
  • H (High): データの約30%を復元可能。最も冗長性が高く、信頼性が必要な場合に推奨される。

印刷するような用途でなければデフォルトの M でよいと思いますが、後述する画像埋め込みで複雑な画像を埋め込む際は、高めのレベルに設定しておくと安心かもしれません。

imageSettings

QR コードの中央に画像を埋め込むための設定を行うプロパティです。任意の画像を QR コード内に配置できます。 企業ロゴやブランドロゴ等を指定するケースが多そうです。

  • src: 埋め込む画像のURL。
  • height: 埋め込む画像の高さ。
  • width: 埋め込む画像の幅。
  • excavate: 画像部分の背景(QR コード)を削除するかどうか(true/false)。
  • x: 画像の左上の X座標(QR コードの左上を基準)を指定。デフォルトは QR コードの中央に自動配置。
  • y: 画像の左上の Y座標(QR コードの左上を基準)を指定。デフォルトは QR コードの中央に自動配置。
  • opacity: 画像の透明度(0: 完全に透明 から 1: 完全に不透明)を指定。デフォルト値は 1。
  • crossOrigin : クロスオリジンの画像を取得する際のリクエスト設定を指定。anonymous(認証情報なし) または use-credentials(認証情報あり)を指定。

作業手順

実際に qrcode.react を React プロジェクトに導入して実装していく手順を紹介します。なお、React プロジェクト自体の構築手順については今回触れません。

今回の実装で使用したライブラリやフレームワークのバージョンは以下の通りです。同じ環境で再現したい方は、これらのバージョンを参考にしてください。

  • React: 18.3.1
  • qrcode.react: 4.1.0

インストール

まずは qrcode.react ライブラリをプロジェクトにインストールします。

npm install qrcode.react

基本的な QR コードの描画機能を実装

早速、QR コードを React コンポーネントとして描画してみます。

以下のように QRCodeCanvas コンポーネントを使用して、ユーザーが入力した URL を元に QR コードを生成します。

import React, { useState } from 'react';
import { QRCodeCanvas } from 'qrcode.react'; // 今回は Canvas を利用

const QR_CANVAS_SIZE = 200; // 初期値は 128

const App = () => {
  const [url, setUrl] = useState('');

  return (
    <div style={{ textAlign: 'center', marginTop: '50px' }}>
      <h1>QR Code Generator</h1>
      <input
        type="text"
        value={url}
        onChange={(e) => setUrl(e.target.value)}
        placeholder="Enter URL"
        style={{ padding: '10px', width: '300px' }}
      />
      <div style={{ marginTop: '20px' }}>
        <QRCodeCanvas value={url} size={QR_CANVAS_SIZE} />;
      </div>
    </div>
  );
}

export default App;

これで、ユーザーが入力した URL に基づいて QR コードが生成されるシンプルなアプリケーションが完成しました。簡単ですね。

ユーザーが入力したタイトルを追加で描画する機能を実装

次に、生成された QR コードの下にユーザーが入力したタイトルを描画してみます。canvas の 2D 描画コンテキスト (getContext('2d')) を利用して、テキストを追加します。

まずは、共通で使用する定数類を記述します。

const QR_PADDING = 40;
const QR_PADDING_VERTICAL = QR_PADDING * 2;
const QR_PADDING_HORIZONTAL = QR_PADDING * 4;
const QR_CANVAS_SIZE = 200; // 初期値は 128
const TITLE_PADDING = 5;
const TITLE_PADDING_VERTICAL = TITLE_PADDING * 2;
const TITLE_FONT_SIZE = 30;
const TITLE_FONT_FAMILY = 'sans-serif';
const TITLE_BG_COLOR = '#fafafa';
const TITLE_FG_COLOR = '#131313';

次に、描画用の関数を定義します。タイトルの描画位置を決めるのに手こずっていますね。

  const drawQRCodeWithTitle = (
    qrCodeCanvas: HTMLCanvasElement,
    ctx: CanvasRenderingContext2D,
    qrCodeImage: HTMLImageElement,
    title: string
  ) => {
    const titleHeight = (TITLE_FONT_SIZE + TITLE_PADDING);
    const qrAreaHeight = QR_PADDING_VERTICAL + qrCodeImage.height;
    const titleAreaHeight = titleHeight + TITLE_PADDING_VERTICAL;
    qrCodeCanvas.height = QR_AREA_HEIGHT + TITLE_AREA_HEIGHT;

    // 背景を準備
    ctx.fillStyle = TITLE_BG_COLOR;
    ctx.fillRect(0, 0, qrCodeCanvas.width, qrCodeCanvas.height);

    // QR コードを描画
    ctx.drawImage(qrCodeImage, QR_PADDING_VERTICAL, QR_PADDING);

    // タイトルを描画
    const titleXPosition = qrCodeCanvas.width / 2;
    const titleYPosition = qrCodeImage.height + QR_PADDING_VERTICAL;
    ctx.fillStyle = TITLE_FG_COLOR;
    ctx.font = `${TITLE_FONT_SIZE}px ${TITLE_FONT_FAMILY}`;
    ctx.textAlign = 'center';
    ctx.fillText(title, titleXPosition, titleYPosition);
  };

最後に、URL もしくはタイトルが変更されたときに先述の drawQRCodeWithTitle() 関数を実行する useEffect フックを作成します。

  useEffect(() => {
    const canvas = document.querySelector('canvas'); // ※①
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const qrCodeImage = new Image();
    qrCodeImage.src = canvas.toDataURL('image/png'); // ※②

    qrCodeImage.onload = () => {
      canvas.width = qrCodeImage.width + QR_PADDING_HORIZONTAL;
      canvas.style.display = 'block';

      drawQRCodeWithTitle(canvas, ctx, qrCodeImage, title);
    };
  }, [url, title]);

※① document.querySelector('canvas') では単純に最初に見つかった canvas 要素を返すため、本来は ref で操作対象の DOM を指定するのが望ましいですが、今回は QR コードを一つだけ描画するため簡易的な記述を採用しています。
※② toDataURL() は Canvas の内容をエンコードしてデータ URL として取得するため、Canvas のサイズが大きい場合や高解像度の画像を扱う場合、処理に時間がかかることがあり頻繁に呼び出すとパフォーマンスの低下を招く可能性があります。

これで、ユーザーが入力したタイトルを QR コードの下に表示させることができました。

ダウンロード機能を実装

最後に、ユーザーが生成した QR コードとタイトルを含む画像をダウンロードできる機能を追加します。

まずはダウンロード処理用の関数を実装。QR コードを描画した QRCodeCanvas から HTMLImageElement を生成し、その画像を PNG 形式でダウンロードさせます。
canvas.toDataURL() を使って画像データを取得し、a 要素を利用してダウンロード可能にしています。

const downloadQRCodeImage = () => {
  const canvas = document.querySelector('canvas');
  if (!canvas) return;

  const link = document.createElement('a');
  link.download = title
    ? `${title}_qrcode.png`
    : `qrcode.png`;

  const qrCodeImage = new Image();
  qrCodeImage.src = canvas.toDataURL('image/png');

  qrCodeImage.onload = () => {
    link.href = canvas.toDataURL('image/png');
    link.click();
  };
};

ボタンも適当に作っておきます。

<button 
  type="button"
  onClick={downloadQRCodeImage} 
  style={{
    padding: '10px 10px',
    fontSize: '12px',
    color: '#fff',
    backgroundColor: '#a4a4a4',
    borderRadius: '5px',
    cursor: 'pointer',
  }}
>
  Download
</button>

これで、ユーザーは QR コードと入力したタイトルが描かれた Canvas を PNG ファイルとしてダウンロードできるようになります!

全文は下記に貼っておきます。

コード全文

import React, { useState, useRef, useEffect } from 'react';
import { QRCodeCanvas } from 'qrcode.react'; // 今回は Canvas を利用

// 定数類
const QR_PADDING = 40;
const QR_PADDING_VERTICAL = QR_PADDING * 2;
const QR_PADDING_HORIZONTAL = QR_PADDING * 4;
const QR_CANVAS_SIZE = 200; // 初期値は 128
const TITLE_PADDING = 5;
const TITLE_PADDING_VERTICAL = TITLE_PADDING * 2;
const TITLE_FONT_SIZE = 30;
const TITLE_FONT_FAMILY = 'sans-serif';

const App = () => {
  const [url, setUrl] = useState('');
  const [title, setTitle] = useState('');
  const qrCodeRef = useRef(null);

  const drawQRCodeWithTitle = (
    qrCodeCanvas: HTMLCanvasElement,
    ctx: CanvasRenderingContext2D,
    qrCodeImage: HTMLImageElement,
    title: string
  ) => {
    const titleHeight = (TITLE_FONT_SIZE + TITLE_PADDING);
    const qrAreaHeight = QR_PADDING_VERTICAL + qrCodeImage.height;
    const titleAreaHeight = titleHeight + TITLE_PADDING_VERTICAL;
    qrCodeCanvas.height = qrAreaHeight + titleAreaHeight;

    // 背景を準備
    ctx.fillStyle = TITLE_BG_COLOR;
    ctx.fillRect(0, 0, qrCodeCanvas.width, qrCodeCanvas.height);

    // QR コードを描画
    ctx.drawImage(qrCodeImage, QR_PADDING_VERTICAL, QR_PADDING);

    // タイトルを描画
    const titleXPosition = qrCodeCanvas.width / 2;
    const titleYPosition = qrCodeImage.height + QR_PADDING_VERTICAL;
    ctx.fillStyle = TITLE_FG_COLOR;
    ctx.font = `${TITLE_FONT_SIZE}px ${TITLE_FONT_FAMILY}`;
    ctx.textAlign = 'center';
    ctx.fillText(title, titleXPosition, titleYPosition);
  };

  const downloadQRCodeImage = () => {
    const canvas = document.querySelector('canvas');
    if (!canvas) return;

    const qrCodeImage = new Image();
    qrCodeImage.src = canvas.toDataURL('image/png');

    const link = document.createElement('a');
    link.download = title
      ? `${title}_qrcode.png`
      : `qrcode.png`;

    qrCodeImage.onload = () => {
      link.href = canvas.toDataURL('image/png');
      link.click();
    };
  };

  useEffect(() => {
    const canvas = document.querySelector('canvas');
    if (!canvas) return;

    const ctx = canvas.getContext('2d');
    if (!ctx) return;

    const qrCodeImage = new Image();
    qrCodeImage.src = canvas.toDataURL('image/png');

    qrCodeImage.onload = () => {
      canvas.width = qrCodeImage.width + QR_PADDING_HORIZONTAL;
      canvas.style.display = 'block';

      drawQRCodeWithTitle(canvas, ctx, qrCodeImage, title);
    };
  }, [url, title]);

  return (
    <div style={{ textAlign: 'center', marginTop: '50px' }}>
      <h1>QR Code Generator with Title</h1>
      <input
        type='text'
        value={url}
        onChange={e => setUrl(e.target.value)}
        placeholder='Enter URL'
        style={{ padding: '10px', width: '200px', marginBottom: '20px' }}
      />
      <input
        type='text'
        value={title}
        onChange={e => setTitle(e.target.value)}
        placeholder='Enter Title'
        style={{ padding: '10px', width: '200px', marginBottom: '20px' }}
      />
      <div style={{ marginTop: '20px', display: 'flex', justifyContent: 'center' }} ref={qrCodeRef}>
        <QRCodeCanvas value={url} size={QR_CANVAS_SIZE} />;
      </div>
      <button 
        type="button"
        onClick={downloadQRCodeImage} 
        style={{
          padding: '10px 10px',
          fontSize: '12px',
          color: '#fff',
          backgroundColor: '#a4a4a4',
          borderRadius: '5px',
          cursor: 'pointer',
        }}
      >
        Download
      </button>
    </div>
  );
}

export default App;

まとめ

React で QR コードを生成し、さらにその下にユーザーが入力したタイトルを Canvas に描画する方法を紹介しました。
今回は深くカスタマイズしませんでしたが、qrcode.react はプロパティも数多く存在するため、よりカスタマイズした機能を作成することも可能です。

今回、技術ブログでは3回目の執筆をさせていただきました。
1回目と2回目は、それぞれ下記のような記事を書いていたので、お時間ある方はこちらもぜひ読んでいってください!

techblog.styleedge.co.jp techblog.styleedge.co.jp

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

recruit.styleedge.co.jp

せっかくなので、最後に qrcode.react で作成した QR コードも掲載しておきます!それではまた!

*1:※QRコードは株式会社デンソーウェーブの登録商標です。