TECH PLAY

電通総研

電通総研 の技術ブログ

836

はじめに ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わってる案件では、コンテナ利用、 AWS Fargate利用を進めております。 AWS Fargateのお手軽さは非常に重宝しております。 しかし、そこで問題になってくるのが、管理接続、踏み台系をどうするかです。 アプリケーション本体をせっかく AWS Fargateでやっているので、管理系接続、踏み台的な作業もできるだけ非EC2でやりたいですよね? しかし、以前検証した際には、ローカルPC⇒ AWS Fargate経由⇒RDSといった接続することができませんでした。が、今回、 AWS 様のアップデートにより上記ができるようになりましたので、ご紹介いたします。 前記事: AWS FargateでECS Exec、session managerモロモロのTips 目次 はじめに 目次 実現できること 解説 本機能を使いやすくするために 実現できること 特に踏み台を準備せず、アプリが稼働してるコンテナからポート フォワ ードをし、任意の環境からRDSに接続できます。 接続例を下記に図示します。 AWS CLI で、 AWS Fargateをターゲットにして、 リモートホスト ポート フォワ ード用のドキュメント( AWS -StartPortForwardingSessionToRemoteHost)を実行。 pgAdminから接続。 解説 下記は、 windows 上での実行例です。 aws ssm start-session ^ --profile dev93 ^ --target ecs:%cluster_name% %task_id% %container_runtime_id% ^ --document-name AWS -StartPortForwardingSessionToRemoteHost ^ --parameters "{"host":["XXXXXXXXXXXX.ap-northeast-1.rds.amazonaws.com"],"portNumber":["5432"], "localPortNumber":["15432"]}" " キーとなるのは、 aws ssm start-sessionで、targetをどう指定するかです。 https://github.com/aws/aws-cli/blob/c0edee0a7427b6e7b654df0696015e96105497a3/awscli/customizations/ecs/executecommand.py#L61-L73 で、上記のコードに、 AWS FargateでのSSM呼び出しの書式が書かれており AWS -StartPortForwardingSessionToRemoteHostだけでなく他のドキュメントも呼び出し可能です。 なお、必要となる値は下記より確認可能です。 cluster_name task_id container_runtime_id 本機能を使いやすくするために cluster_nameは、環境に応じた固定的な値ですが、task_id、container_runtime_idは、ESC TASKの再起動のたびに変わってしまう動的な値です。ですので、これらを取得する スクリプト と組み合わせると利用しやすくなります(私は、 クラスタ ー名、サービス名を指定するとtask_id、container_runtime_idを取得する スクリプト と組み合わせて使ってます)。 また、ecsを使いやすくするツールを開発されている方々もおられますので、これらのツールを使ってもよいかもしれません。 ecstaを使ってAmazon ECSコンテナ開発効率を上げよう! github fujiwara/ecsta なお、当初はecstaを触ってみた記事を書くつもりだったのですが2023/02時点で windows 用のバイナリがなかったため今回の内容にしました。 Mac 、 Linux ユーザーの方は、ecstaを使ってみるとよいのではないかと思います。 執筆: @miura.toshihiko 、レビュー: @sato.taichi ( Shodo で執筆されました )
はじめに ISID X(クロス) イノベーション 本部 の三浦です。 筆者の関わってる案件では、コンテナ利用、 AWS Fargate利用を進めております。 AWS Fargateのお手軽さは非常に重宝しております。 しかし、そこで問題になってくるのが、管理接続、踏み台系をどうするかです。 アプリケーション本体をせっかく AWS Fargateでやっているので、管理系接続、踏み台的な作業もできるだけ非EC2でやりたいですよね? しかし、以前検証した際には、ローカルPC⇒ AWS Fargate経由⇒RDSといった接続することができませんでした。が、今回、 AWS 様のアップデートにより上記ができるようになりましたので、ご紹介いたします。 前記事: AWS FargateでECS Exec、session managerモロモロのTips 目次 はじめに 目次 実現できること 解説 本機能を使いやすくするために 実現できること 特に踏み台を準備せず、アプリが稼働してるコンテナからポート フォワ ードをし、任意の環境からRDSに接続できます。 接続例を下記に図示します。 AWS CLI で、 AWS Fargateをターゲットにして、 リモートホスト ポート フォワ ード用のドキュメント( AWS -StartPortForwardingSessionToRemoteHost)を実行。 pgAdminから接続。 解説 下記は、 windows 上での実行例です。 aws ssm start-session ^ --profile dev93 ^ --target ecs:%cluster_name% %task_id% %container_runtime_id% ^ --document-name AWS -StartPortForwardingSessionToRemoteHost ^ --parameters "{"host":["XXXXXXXXXXXX.ap-northeast-1.rds.amazonaws.com"],"portNumber":["5432"], "localPortNumber":["15432"]}" " キーとなるのは、 aws ssm start-sessionで、targetをどう指定するかです。 https://github.com/aws/aws-cli/blob/c0edee0a7427b6e7b654df0696015e96105497a3/awscli/customizations/ecs/executecommand.py#L61-L73 で、上記のコードに、 AWS FargateでのSSM呼び出しの書式が書かれており AWS -StartPortForwardingSessionToRemoteHostだけでなく他のドキュメントも呼び出し可能です。 なお、必要となる値は下記より確認可能です。 cluster_name task_id container_runtime_id 本機能を使いやすくするために cluster_nameは、環境に応じた固定的な値ですが、task_id、container_runtime_idは、ESC TASKの再起動のたびに変わってしまう動的な値です。ですので、これらを取得する スクリプト と組み合わせると利用しやすくなります(私は、 クラスタ ー名、サービス名を指定するとtask_id、container_runtime_idを取得する スクリプト と組み合わせて使ってます)。 また、ecsを使いやすくするツールを開発されている方々もおられますので、これらのツールを使ってもよいかもしれません。 ecstaを使ってAmazon ECSコンテナ開発効率を上げよう! github fujiwara/ecsta なお、当初はecstaを触ってみた記事を書くつもりだったのですが2023/02時点で windows 用のバイナリがなかったため今回の内容にしました。 Mac 、 Linux ユーザーの方は、ecstaを使ってみるとよいのではないかと思います。 執筆: @miura.toshihiko 、レビュー: @sato.taichi ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 僕の所属する部署は、旬な技術に対して、レポートを継続して書いています。そのレポートをGPT4に書いてもらおうというのが今回の記事になります。はやりのプロンプトエンジニアリングですね。 この記事を書いている2023/03/24の時点では、GPT4は、Webページにアクセスして情報を取得することができません。GPT4が学習したデータは、2021年9月までのものらしいので、旬な技術のことはほとんど知りません。 それをどう解決するかが、苦労したポイントでした。もうすぐ、ChatGPTのBrowsing Pluginが出ると思うので、この問題は解決しますが、テク ノロ ジー レポートを書くという本筋は変わらないはずです。 レポートのテーマ キーワードのピックアップ 情報参照サイトのピックアップ 情報参照サイトの要約 プロンプトの実行 実行結果 まとめ 仲間募集 レポートのテーマ レポートのテーマは、 Orbs という ブロックチェーン です。 Orbsを選んだ深い理由はないのですが、2022年度以降に盛り上がっている ブロックチェーン で、僕がほとんど知らない ブロックチェーン を選んでみました。 キーワードのピックアップ 軽くググって、レポートに必要そうなキーワードをピックアップしました。そのキーワードは下記の通りです。実は、キーワードのピックアップが最も重要な作業になります。 GPT4に何を書いてほしいのかは、人が決める必要があります。それを決めないと、出来上がったレポートの良し悪しも判断できません。 Layer-3 Orbs 3.0 PoS V3 Multi-chain Staking Orbs Lambda Orbs VM EVM TON Orbs Governance 情報参照サイトのピックアップ orbs キーワード で、情報の参照サイトを探します。斜め読みして分かりやすいサイトを選びます。1キーワード、1サイトで良いでしょう。 サイトを斜め読みしているので、この段階で、Orbsに対する知識がそれなりに付いてきています。 情報参照サイトの要約 Edge(Bing)で、サイトにアクセスし、CMD/CTRL + Aでページのコンテンツを全選択してコピーします。プロンプトを出し、 summarize と入力し、CMD/CTRL Vでページのコンテンツを貼り付けます。 出力された要約をプロンプトの [inputs]: の下に下記のように貼り付けます。 [inputs]: This page is about the Orbs project and its progress in 2022. The article discusses the key components of Orbs 3.0, including its place in web3, multi-chain staking (PoS V3), and Orbs as a layer-3 (L3) blockchain. The project has made significant progress in each of these areas, including the launch of multi-chain staking on Ethereum and Polygon, the development of Orbs Lambda and Orbs VM as L3 execution services, and the introduction and launch of Orbs governance. The project has also integrated with the TON blockchain and has plans to continue building and developing in 2023. Edgeでなくても、Webページを要約できるなら、何を使っても構いません。 ページの要約と [inputs]: への貼付けを情報参照サイトの数分繰り返します。 プロンプトの実行 これで、プロンプトを実行する準備が整いました。下記のプロンプトを実行しましょう。 You are an experienced writer who is good at writing technology reports. [theme]:orbs [inputs]: ... [c1]:Generate some popular questions about [theme] and save [Q#]. [c2]:Write as detailed an article as possible about all [Q#]. 実行結果 実行結果は下記のようになりました。結構よくできたレポートではないでしょうか。 [c1]: Generate some popular questions about [theme] and save [Q#]. Q1: What is the Orbs project and its main objectives? Q2: How does Orbs fit into the Web 3.0 ecosystem? Q3: What is multi-chain staking (PoS V3) in Orbs 3.0? Q4: How does Orbs function as a Layer-3 blockchain? Q5: What are Orbs Lambda and Orbs VM, and how do they work? Q6: How does Orbs enhance existing EVM and TON smart contracts? Q7: What are the limitations of existing smart contracts that Orbs addresses? Q8: How is the Orbs project related to the TON blockchain? [c2]: Write as detailed an article as possible about all [Q#]. Title: Orbs 3.0: Unlocking the Full Potential of Web 3.0 and Blockchain Technology Introduction The Orbs project has been making significant strides in 2022, with the launch of Orbs 3.0, which includes multi-chain staking, integration with the TON blockchain, and the introduction of Orbs Lambda and Orbs VM. In this article, we'll explore the key components of Orbs 3.0 and how it's revolutionizing the Web 3.0 and blockchain ecosystems. Q1 & Q2: The Orbs Project and Its Place in the Web 3.0 Ecosystem Orbs is a decentralized and public blockchain infrastructure that utilizes a Proof-of-Stake (PoS) consensus algorithm. The Orbs network is secured by permissionless validators called Guardians, who are elected using PoS. The Orbs PoS algorithm is implemented as EVM smart contracts running on the Ethereum mainnet, providing strong security guarantees. As a Layer 3 protocol, Orbs runs over existing L1/L2 blockchains like Ethereum and TON, enhancing the capabilities of their existing smart contracts, and playing a crucial role in the Web 3.0 ecosystem. Q3: Multi-chain Staking (PoS V3) in Orbs 3.0 Orbs 3.0 features multi-chain staking, allowing users to stake their tokens on multiple blockchain networks, such as Ethereum and Polygon. This development provides additional flexibility and security to token holders, as they can now participate in the Orbs ecosystem across multiple chains. Q4 & Q6: Orbs as a Layer-3 Blockchain and Enhancing EVM and TON Smart Contracts Orbs is a decentralized execution layer that operates between existing L1/L2 solutions and the application layer. By functioning as a Layer 3 blockchain, Orbs enhances the capabilities of existing EVM and TON smart contracts, enabling new possibilities for Web 3.0, DeFi, NFTs, and GameFi applications. Q5: Orbs Lambda and Orbs VM Orbs Lambda is a decentralized, serverless cloud function similar to AWS Lambda. It is fully transparent, decentralized, and event-driven, relying on independent network validators for guaranteed execution. Developers can write cloud functions in JavaScript or TypeScript, and trigger them through scheduled time intervals, on-chain events from multiple L1 blockchains, or HTTP requests. Orbs VM, on the other hand, is a decentralized virtual machine akin to AWS EC2. Implemented as Docker containers deployed to the Orbs Network, it is fully transparent and decentralized. Services can be implemented in any familiar programming language and are always-on. Both Orbs Lambda and Orbs VM enhance existing smart contracts without replacing them. Q7: Addressing Limitations of Existing Smart Contracts Orbs tackles several limitations of existing smart contracts, including the inability to access data outside the chain, trigger future actions, limited language support, and inability to access historic data. By addressing these limitations, Orbs enables developers to create more sophisticated and feature-rich decentralized applications, leveraging the full potential of blockchain technology. Q8: Orbs Project's Relation to the TON Blockchain Orbs has integrated with the TON blockchain, a decentralized platform that uses blockchain technology to provide secure and efficient interaction with online services. TON, or The Open Network, was initially launched by Telegram in 2018 as an open-source project to create an ecosystem for distributed applications (dApps), smart contracts, decentralized storage solutions, and other innovations. Although Telegram was forced to abandon its plans to issue Gram tokens due to a dispute with the SEC, the TON network has continued to develop as a separate entity. Orbs' integration with TON further expands its ecosystem and enhances the capabilities of TON-based smart contracts. Conclusion The Orbs project has made impressive progress in 2022, with the launch of Orbs 3.0 and its integration with the TON blockchain. Through multi-chain staking, the development of Orbs Lambda and Orbs VM, and the enhancement of existing EVM and TON smart contracts, Orbs is poised to unlock the full potential of Web 3.0 and blockchain technology. As the project continues to grow and develop in 2023, we can expect further innovations and breakthroughs that will shape the future of decentralized applications and services. まとめ GPT4の書いたテク ノロ ジー レポートを読んだ後に、情報参照サイトのコンテンツを読むと、かなりよく理解できます。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア 執筆: @higa 、レビュー: @higa ( Shodo で執筆されました )
電通国際情報サービス 、オープン イノベーション ラボの 比嘉康雄 です。 僕の所属する部署は、旬な技術に対して、レポートを継続して書いています。そのレポートをGPT4に書いてもらおうというのが今回の記事になります。はやりのプロンプトエンジニアリングですね。 この記事を書いている2023/03/24の時点では、GPT4は、Webページにアクセスして情報を取得することができません。GPT4が学習したデータは、2021年9月までのものらしいので、旬な技術のことはほとんど知りません。 それをどう解決するかが、苦労したポイントでした。もうすぐ、ChatGPTのBrowsing Pluginが出ると思うので、この問題は解決しますが、テク ノロ ジー レポートを書くという本筋は変わらないはずです。 レポートのテーマ キーワードのピックアップ 情報参照サイトのピックアップ 情報参照サイトの要約 プロンプトの実行 実行結果 まとめ 仲間募集 レポートのテーマ レポートのテーマは、 Orbs という ブロックチェーン です。 Orbsを選んだ深い理由はないのですが、2022年度以降に盛り上がっている ブロックチェーン で、僕がほとんど知らない ブロックチェーン を選んでみました。 キーワードのピックアップ 軽くググって、レポートに必要そうなキーワードをピックアップしました。そのキーワードは下記の通りです。実は、キーワードのピックアップが最も重要な作業になります。 GPT4に何を書いてほしいのかは、人が決める必要があります。それを決めないと、出来上がったレポートの良し悪しも判断できません。 Layer-3 Orbs 3.0 PoS V3 Multi-chain Staking Orbs Lambda Orbs VM EVM TON Orbs Governance 情報参照サイトのピックアップ orbs キーワード で、情報の参照サイトを探します。斜め読みして分かりやすいサイトを選びます。1キーワード、1サイトで良いでしょう。 サイトを斜め読みしているので、この段階で、Orbsに対する知識がそれなりに付いてきています。 情報参照サイトの要約 Edge(Bing)で、サイトにアクセスし、CMD/CTRL + Aでページのコンテンツを全選択してコピーします。プロンプトを出し、 summarize と入力し、CMD/CTRL Vでページのコンテンツを貼り付けます。 出力された要約をプロンプトの [inputs]: の下に下記のように貼り付けます。 [inputs]: This page is about the Orbs project and its progress in 2022. The article discusses the key components of Orbs 3.0, including its place in web3, multi-chain staking (PoS V3), and Orbs as a layer-3 (L3) blockchain. The project has made significant progress in each of these areas, including the launch of multi-chain staking on Ethereum and Polygon, the development of Orbs Lambda and Orbs VM as L3 execution services, and the introduction and launch of Orbs governance. The project has also integrated with the TON blockchain and has plans to continue building and developing in 2023. Edgeでなくても、Webページを要約できるなら、何を使っても構いません。 ページの要約と [inputs]: への貼付けを情報参照サイトの数分繰り返します。 プロンプトの実行 これで、プロンプトを実行する準備が整いました。下記のプロンプトを実行しましょう。 You are an experienced writer who is good at writing technology reports. [theme]:orbs [inputs]: ... [c1]:Generate some popular questions about [theme] and save [Q#]. [c2]:Write as detailed an article as possible about all [Q#]. 実行結果 実行結果は下記のようになりました。結構よくできたレポートではないでしょうか。 [c1]: Generate some popular questions about [theme] and save [Q#]. Q1: What is the Orbs project and its main objectives? Q2: How does Orbs fit into the Web 3.0 ecosystem? Q3: What is multi-chain staking (PoS V3) in Orbs 3.0? Q4: How does Orbs function as a Layer-3 blockchain? Q5: What are Orbs Lambda and Orbs VM, and how do they work? Q6: How does Orbs enhance existing EVM and TON smart contracts? Q7: What are the limitations of existing smart contracts that Orbs addresses? Q8: How is the Orbs project related to the TON blockchain? [c2]: Write as detailed an article as possible about all [Q#]. Title: Orbs 3.0: Unlocking the Full Potential of Web 3.0 and Blockchain Technology Introduction The Orbs project has been making significant strides in 2022, with the launch of Orbs 3.0, which includes multi-chain staking, integration with the TON blockchain, and the introduction of Orbs Lambda and Orbs VM. In this article, we'll explore the key components of Orbs 3.0 and how it's revolutionizing the Web 3.0 and blockchain ecosystems. Q1 & Q2: The Orbs Project and Its Place in the Web 3.0 Ecosystem Orbs is a decentralized and public blockchain infrastructure that utilizes a Proof-of-Stake (PoS) consensus algorithm. The Orbs network is secured by permissionless validators called Guardians, who are elected using PoS. The Orbs PoS algorithm is implemented as EVM smart contracts running on the Ethereum mainnet, providing strong security guarantees. As a Layer 3 protocol, Orbs runs over existing L1/L2 blockchains like Ethereum and TON, enhancing the capabilities of their existing smart contracts, and playing a crucial role in the Web 3.0 ecosystem. Q3: Multi-chain Staking (PoS V3) in Orbs 3.0 Orbs 3.0 features multi-chain staking, allowing users to stake their tokens on multiple blockchain networks, such as Ethereum and Polygon. This development provides additional flexibility and security to token holders, as they can now participate in the Orbs ecosystem across multiple chains. Q4 & Q6: Orbs as a Layer-3 Blockchain and Enhancing EVM and TON Smart Contracts Orbs is a decentralized execution layer that operates between existing L1/L2 solutions and the application layer. By functioning as a Layer 3 blockchain, Orbs enhances the capabilities of existing EVM and TON smart contracts, enabling new possibilities for Web 3.0, DeFi, NFTs, and GameFi applications. Q5: Orbs Lambda and Orbs VM Orbs Lambda is a decentralized, serverless cloud function similar to AWS Lambda. It is fully transparent, decentralized, and event-driven, relying on independent network validators for guaranteed execution. Developers can write cloud functions in JavaScript or TypeScript, and trigger them through scheduled time intervals, on-chain events from multiple L1 blockchains, or HTTP requests. Orbs VM, on the other hand, is a decentralized virtual machine akin to AWS EC2. Implemented as Docker containers deployed to the Orbs Network, it is fully transparent and decentralized. Services can be implemented in any familiar programming language and are always-on. Both Orbs Lambda and Orbs VM enhance existing smart contracts without replacing them. Q7: Addressing Limitations of Existing Smart Contracts Orbs tackles several limitations of existing smart contracts, including the inability to access data outside the chain, trigger future actions, limited language support, and inability to access historic data. By addressing these limitations, Orbs enables developers to create more sophisticated and feature-rich decentralized applications, leveraging the full potential of blockchain technology. Q8: Orbs Project's Relation to the TON Blockchain Orbs has integrated with the TON blockchain, a decentralized platform that uses blockchain technology to provide secure and efficient interaction with online services. TON, or The Open Network, was initially launched by Telegram in 2018 as an open-source project to create an ecosystem for distributed applications (dApps), smart contracts, decentralized storage solutions, and other innovations. Although Telegram was forced to abandon its plans to issue Gram tokens due to a dispute with the SEC, the TON network has continued to develop as a separate entity. Orbs' integration with TON further expands its ecosystem and enhances the capabilities of TON-based smart contracts. Conclusion The Orbs project has made impressive progress in 2022, with the launch of Orbs 3.0 and its integration with the TON blockchain. Through multi-chain staking, the development of Orbs Lambda and Orbs VM, and the enhancement of existing EVM and TON smart contracts, Orbs is poised to unlock the full potential of Web 3.0 and blockchain technology. As the project continues to grow and develop in 2023, we can expect further innovations and breakthroughs that will shape the future of decentralized applications and services. まとめ GPT4の書いたテク ノロ ジー レポートを読んだ後に、情報参照サイトのコンテンツを読むと、かなりよく理解できます。 仲間募集 私たちは同じグループで共に働いていただける仲間を募集しています。 現在、以下のような職種を募集しています。 ソリューションアーキテクト AIエンジニア 執筆: @higa 、レビュー: @higa ( Shodo で執筆されました )
こんにちは!グループ経営ソリューション( GMS )事業部5年目の川島です。現在は自社製品Ci*Xの導入やセールス支援をしています。 この記事では私のやっている仕事の内容や職場の雰囲気について、また、今学生で今後ISIDを目指す方向けに就活のお話も少しできればと思います。 本記事を通して私の仕事を知ってもらえたり、IT経験のない方でIT企業を目指す方に少しでも勇気を持ってもらえたりしたらうれしいです。 自己紹介 まずは簡単に、自己紹介を。 1. 年次 新卒で2019年にISIDに入社し、2023年1月で5年目になりました。 2. ISIDの入社のきめて 最初は大学の同級生に「ISIDの インターン が面白かった!」と言われ、その場の勢いでエントリーしました。 (採用面接でも何を思ったか正直に↑を伝えました。笑っていただけて安心した記憶が・・・) ・面接が楽しく、自分らしくいられたこと ・入社を悩んでいるときに会わせていただいた先輩社員が全員素敵だったこと を決め手に入社を決意しました。 3. 大学の専攻 大学、大学院と数学科に所属し、 素数 周りの研究をしていました。IT経験はありませんでした。 余談ですが:数学科の人ならわかると思いますが、 TeX をプログラミングだと思っていたレベルです。 4. ISID内の所属 グループ経営ソリューション事業部(会計のシステムを扱う事業部です)に所属しています。 配属から部署は変わっていません。 どんな環境でどんな仕事をしているの? QA形式で私の仕事を紹介します。 Q1:なんの仕事をしているのですか? 「Ci*X」という自社会計システムの導入やセールス支援の仕事をしています。 Q2:「導入」とは?「セールス支援」とは何ですか? 同じ事業部所属の後輩、安田さんの記事にわかりやすくまとめてあるので割愛します! 導入/セールスする製品は違いますが、やっていることはほぼ同じです。 安田さんの記事はこちら>新卒3年目社員の働き方紹介(GMS事業部) Q3:「Ci*X」とは何ですか? サイクロスと読みます。サイクロスシリーズは ISIDが作った 会計システムです。 シリーズとしては「Ci*X Expense」という経費精算システムを皮切りに、「Journalizer」(仕訳エンジン)、「Workflow」(汎用ワークフロー)、「Financials」(グループ統合会計システム)が続々とリリースされています。 Q4:「Ci*X」の魅力って何ですか? たくさんありますが、代表すると下記の2点です。 ・使いやすさ(UIにこだわっています) ・他システムとの連携のしやすさ(Journalizerを利用することで他システムへの柔軟な連携を実現します) ぜひ Ci*Xのホームページ も見ていただきたいです! Q5:プログラミングしていますか? プログラミングはしませんが、読むことはあります。 製品の仕様確認のために JAVA や SQL を読むこともあるので、知っておくことに損はありません。 (私は研修&配属後の勉強で知識を付けました) Q6:どんな人と仕事をしているのですか? 私の所属する導入グループは20名いて、年次や新卒or中途入社もバラバラです。 (ちょっと特徴的なのは、このグループには女性のほうが多い!ということですかね) Q7:1日の仕事の流れはどんな感じですか? 打合せと作業を交互に繰り返します。 入社してすぐは作業のほうが多かったですが、今はどの案件でもリーダー以上の役割が多いため、打合せ多めです。 Q8:在宅勤務は多いですか? 多いです。 今は OJT リーダーとして新人の教育担当をしており、1年目の後輩とは週1日程度一緒に出社してランチしています。 Q9:教育体制や先輩とのつながりはどんな感じですか? 部署によっても様々ですが、 OJT 制度はもちろん、私の部署では新任者向けのCi*X Expense/Journalizerの教育コンテンツも用意しています。 また、同部署だけでなく別部署の先輩とも同好会やメンターなどでつながることができるので、仕事やプライベートな相談もできますよ。 やりがいは? 私の仕事のやりがいは大きく2つあります。 ①お客様の反応をダイレクトに感じられること 導入後の感想など、お客様からダイレクトに反応をいただけることはとてもうれしいです。 お客様に直接やり取りできる導入の特権です。 ②自社製品なので今後よりよくするために社内で直接働きかけられること ISIDが作っている製品なので、お客様からの意見も元にしながら、製品をよりよくしていく働きかけをできることがうれしいです。 開発を行ったメンバが近くにいるので、実際の開発秘話なども聞けるのはとても楽しいし、愛着が湧きます。 大変なことは? 日々勉強が必要です。 自社の会計システム(私だとCi*X)を導入する上では ①製品知識 ②業務知識 の2つの知識を大前提にしています。 導入している製品を知り尽くし、お客様の業務背景を把握しきることで、課題に対して最も適切な解決策を提示できます。 大変な時もありますが、勉強したことを活用して、顧客課題に対してベストな解決策を提案できてお客様に喜んでいただけたときはこの上なく達成感を感じます。 システム「導入」にはどんな人が向いているのか? 丸4年働いて、私の考える導入に向いている人の特徴は下記4つだと思います。 ①チームで仕事をするのが好きな人 チームの大小はありますが、導入はチームプレイです。必ずお客様や社内のメンバで案件を進めるので、1人で何かをするだけではありません。人と一緒に何かをすることが好きな人は楽しいと思います。 ②人のために何かしたい!と思える人 導入はお客様企業の業務効率化を目指す仕事です。 お客様の業務をより効率的にいいものになるようにしたい!という思いをもって取り組んでいます。 ③人と話すことが好きな人 導入はとにかくコミュニケーションをとります。 ミーティング、メール、TEAMS・・・ツールはいろいろありますが、課題を「聞き出し」、解決策を「伝える」能力は必須です。 ④考えることが好きな人 時にはその場で解決できないような大きな課題に当たることがあります。 状況を整理し、コツコツ解決までたどり着けるように道筋を立てながら進める必要があります。 SIer はIT経験がなくても大丈夫なのか? IT経験なしで入社した私からQA形式で回答します。 Q1:IT経験がなくても大丈夫ですか? 世の中のどの記事を読んでも「大丈夫」と書いてあるものが多いので、 あえて私からは「"IT"や"課題解決の取り組み"に 嫌悪感がないのであれば 大丈夫」と伝えます。 私の経験からは、3日や1週間等ちょっと長めの(プログラミングとまではいかなくても)IT企業の課題解決系 インターン に行ってみることをお勧めします。 いろいろな資料を読んで頭を悩ませたり、 ヒアリ ングしたりすることが楽しい!と思えるのであればIT企業の仕事は楽しいと思います。 世の中にはたくさんのIT企業がありますので、IT経験の全くない方はぜひ1度 インターン に行ってみることをお勧めします! (もちろんISIDに来ていただけたらうれしいですが!) Q2:IT経験のある人と差を感じますか? 入社直後の研修の時はIT経験があったり、すでに資格を持っていたりする同期とは差を感じることがありました。 (そういう同期にプログラミングを教えてもらっていました) ですが、ISIDは研修制度がかなりしっかりしているので、研修が終わるころには実際の業務にあたってあまり引け目を感じないレベルまで知識はついたと思います。 Q3:IT経験がないと就活で不利そう。就活でどういうアピールをしていましたか? 実際に働いてみると、学生時代何をしていたか?なんてあまり気にならないので、個人的にあまり不利ではないと思います。 とはいえ、就活ではそうもいかないと思うので、私は就活で ①ITに興味があること ②自分に素質があること をアピールしていました。 ①はQ1で書いた通り インターン 経験を伝え、私はIT企業の仕事を楽しく感じます!とアピールしていました。 ②は、数学科でとにかく「論理的に考える」ことを実践していたので、それをアピールしていました。 Q4:大学の時の経験は今の仕事に役に立っていますか? 直接的ではないですが、以下の2つがとても役に立っています。 ①論理的思考力 私は 素数 を勉強していたので、直接的に 素数 が役に立っているのか?といわれるとそうではないのですが、研究活動を経て得た論理的に物事を考え、解決する力は今でも一番自分の武器だと自負しています。 ②教育経験 ずっと数学の先生を目指していたので、人に何か伝えることについては学生時代から塾のアルバイトなどを通して実践をしてきました。(教育実習も行きました) 今の仕事の中でも、講師役をやったり、人に何かを伝えたりする際にこの経験は強く活きています。 さいごに ISIDは新卒・中途関わらず一緒に働く仲間を募集しています 私の所属する部は、中途社員と新卒社員が半々、女性男性比率も半々、年齢も幅広いです。 いろいろな価値観を持つ方と、同じ目標をもって仕事をすることはとても面白いですよ! もしこの記事を読んでISIDや私の部署に少しでも興味を持っていただけたら、ぜひISIDの新卒採用サイトを覗いてみてください。 www.isid.co.jp (余談ですが:恥ずかしながら私も採用サイトの動画にコッソリ出させていただいております・・・!) 執筆: @kawashima.rei 、レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
こんにちは!グループ経営ソリューション( GMS )事業部5年目の川島です。現在は自社製品Ci*Xの導入やセールス支援をしています。 この記事では私のやっている仕事の内容や職場の雰囲気について、また、今学生で今後ISIDを目指す方向けに就活のお話も少しできればと思います。 本記事を通して私の仕事を知ってもらえたり、IT経験のない方でIT企業を目指す方に少しでも勇気を持ってもらえたりしたらうれしいです。 自己紹介 まずは簡単に、自己紹介を。 1. 年次 新卒で2019年にISIDに入社し、2023年1月で5年目になりました。 2. ISIDの入社のきめて 最初は大学の同級生に「ISIDの インターン が面白かった!」と言われ、その場の勢いでエントリーしました。 (採用面接でも何を思ったか正直に↑を伝えました。笑っていただけて安心した記憶が・・・) ・面接が楽しく、自分らしくいられたこと ・入社を悩んでいるときに会わせていただいた先輩社員が全員素敵だったこと を決め手に入社を決意しました。 3. 大学の専攻 大学、大学院と数学科に所属し、 素数 周りの研究をしていました。IT経験はありませんでした。 余談ですが:数学科の人ならわかると思いますが、 TeX をプログラミングだと思っていたレベルです。 4. ISID内の所属 グループ経営ソリューション事業部(会計のシステムを扱う事業部です)に所属しています。 配属から部署は変わっていません。 どんな環境でどんな仕事をしているの? QA形式で私の仕事を紹介します。 Q1:なんの仕事をしているのですか? 「Ci*X」という自社会計システムの導入やセールス支援の仕事をしています。 Q2:「導入」とは?「セールス支援」とは何ですか? 同じ事業部所属の後輩、安田さんの記事にわかりやすくまとめてあるので割愛します! 導入/セールスする製品は違いますが、やっていることはほぼ同じです。 安田さんの記事はこちら>新卒3年目社員の働き方紹介(GMS事業部) Q3:「Ci*X」とは何ですか? サイクロスと読みます。サイクロスシリーズは ISIDが作った 会計システムです。 シリーズとしては「Ci*X Expense」という経費精算システムを皮切りに、「Journalizer」(仕訳エンジン)、「Workflow」(汎用ワークフロー)、「Financials」(グループ統合会計システム)が続々とリリースされています。 Q4:「Ci*X」の魅力って何ですか? たくさんありますが、代表すると下記の2点です。 ・使いやすさ(UIにこだわっています) ・他システムとの連携のしやすさ(Journalizerを利用することで他システムへの柔軟な連携を実現します) ぜひ Ci*Xのホームページ も見ていただきたいです! Q5:プログラミングしていますか? プログラミングはしませんが、読むことはあります。 製品の仕様確認のために JAVA や SQL を読むこともあるので、知っておくことに損はありません。 (私は研修&配属後の勉強で知識を付けました) Q6:どんな人と仕事をしているのですか? 私の所属する導入グループは20名いて、年次や新卒or中途入社もバラバラです。 (ちょっと特徴的なのは、このグループには女性のほうが多い!ということですかね) Q7:1日の仕事の流れはどんな感じですか? 打合せと作業を交互に繰り返します。 入社してすぐは作業のほうが多かったですが、今はどの案件でもリーダー以上の役割が多いため、打合せ多めです。 Q8:在宅勤務は多いですか? 多いです。 今は OJT リーダーとして新人の教育担当をしており、1年目の後輩とは週1日程度一緒に出社してランチしています。 Q9:教育体制や先輩とのつながりはどんな感じですか? 部署によっても様々ですが、 OJT 制度はもちろん、私の部署では新任者向けのCi*X Expense/Journalizerの教育コンテンツも用意しています。 また、同部署だけでなく別部署の先輩とも同好会やメンターなどでつながることができるので、仕事やプライベートな相談もできますよ。 やりがいは? 私の仕事のやりがいは大きく2つあります。 ①お客様の反応をダイレクトに感じられること 導入後の感想など、お客様からダイレクトに反応をいただけることはとてもうれしいです。 お客様に直接やり取りできる導入の特権です。 ②自社製品なので今後よりよくするために社内で直接働きかけられること ISIDが作っている製品なので、お客様からの意見も元にしながら、製品をよりよくしていく働きかけをできることがうれしいです。 開発を行ったメンバが近くにいるので、実際の開発秘話なども聞けるのはとても楽しいし、愛着が湧きます。 大変なことは? 日々勉強が必要です。 自社の会計システム(私だとCi*X)を導入する上では ①製品知識 ②業務知識 の2つの知識を大前提にしています。 導入している製品を知り尽くし、お客様の業務背景を把握しきることで、課題に対して最も適切な解決策を提示できます。 大変な時もありますが、勉強したことを活用して、顧客課題に対してベストな解決策を提案できてお客様に喜んでいただけたときはこの上なく達成感を感じます。 システム「導入」にはどんな人が向いているのか? 丸4年働いて、私の考える導入に向いている人の特徴は下記4つだと思います。 ①チームで仕事をするのが好きな人 チームの大小はありますが、導入はチームプレイです。必ずお客様や社内のメンバで案件を進めるので、1人で何かをするだけではありません。人と一緒に何かをすることが好きな人は楽しいと思います。 ②人のために何かしたい!と思える人 導入はお客様企業の業務効率化を目指す仕事です。 お客様の業務をより効率的にいいものになるようにしたい!という思いをもって取り組んでいます。 ③人と話すことが好きな人 導入はとにかくコミュニケーションをとります。 ミーティング、メール、TEAMS・・・ツールはいろいろありますが、課題を「聞き出し」、解決策を「伝える」能力は必須です。 ④考えることが好きな人 時にはその場で解決できないような大きな課題に当たることがあります。 状況を整理し、コツコツ解決までたどり着けるように道筋を立てながら進める必要があります。 SIer はIT経験がなくても大丈夫なのか? IT経験なしで入社した私からQA形式で回答します。 Q1:IT経験がなくても大丈夫ですか? 世の中のどの記事を読んでも「大丈夫」と書いてあるものが多いので、 あえて私からは「"IT"や"課題解決の取り組み"に 嫌悪感がないのであれば 大丈夫」と伝えます。 私の経験からは、3日や1週間等ちょっと長めの(プログラミングとまではいかなくても)IT企業の課題解決系 インターン に行ってみることをお勧めします。 いろいろな資料を読んで頭を悩ませたり、 ヒアリ ングしたりすることが楽しい!と思えるのであればIT企業の仕事は楽しいと思います。 世の中にはたくさんのIT企業がありますので、IT経験の全くない方はぜひ1度 インターン に行ってみることをお勧めします! (もちろんISIDに来ていただけたらうれしいですが!) Q2:IT経験のある人と差を感じますか? 入社直後の研修の時はIT経験があったり、すでに資格を持っていたりする同期とは差を感じることがありました。 (そういう同期にプログラミングを教えてもらっていました) ですが、ISIDは研修制度がかなりしっかりしているので、研修が終わるころには実際の業務にあたってあまり引け目を感じないレベルまで知識はついたと思います。 Q3:IT経験がないと就活で不利そう。就活でどういうアピールをしていましたか? 実際に働いてみると、学生時代何をしていたか?なんてあまり気にならないので、個人的にあまり不利ではないと思います。 とはいえ、就活ではそうもいかないと思うので、私は就活で ①ITに興味があること ②自分に素質があること をアピールしていました。 ①はQ1で書いた通り インターン 経験を伝え、私はIT企業の仕事を楽しく感じます!とアピールしていました。 ②は、数学科でとにかく「論理的に考える」ことを実践していたので、それをアピールしていました。 Q4:大学の時の経験は今の仕事に役に立っていますか? 直接的ではないですが、以下の2つがとても役に立っています。 ①論理的思考力 私は 素数 を勉強していたので、直接的に 素数 が役に立っているのか?といわれるとそうではないのですが、研究活動を経て得た論理的に物事を考え、解決する力は今でも一番自分の武器だと自負しています。 ②教育経験 ずっと数学の先生を目指していたので、人に何か伝えることについては学生時代から塾のアルバイトなどを通して実践をしてきました。(教育実習も行きました) 今の仕事の中でも、講師役をやったり、人に何かを伝えたりする際にこの経験は強く活きています。 さいごに ISIDは新卒・中途関わらず一緒に働く仲間を募集しています 私の所属する部は、中途社員と新卒社員が半々、女性男性比率も半々、年齢も幅広いです。 いろいろな価値観を持つ方と、同じ目標をもって仕事をすることはとても面白いですよ! もしこの記事を読んでISIDや私の部署に少しでも興味を持っていただけたら、ぜひISIDの新卒採用サイトを覗いてみてください。 www.isid.co.jp (余談ですが:恥ずかしながら私も採用サイトの動画にコッソリ出させていただいております・・・!) 執筆: @kawashima.rei 、レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
こんにちは、ISID 金融ソリューション事業部の岡崎です。 今回は 前回のこちらの記事(UE5 PixelStreamingで、WebUI経由でUE Blueprintを操作する) の続きとして、 EpicGames社が提供する ゲームエンジン 、UnrealEngine5 のPlugin「PixelStreaming」を使用し、UE5でブラウザの JavaScript で反応可能なカスタムイベントの作成や、一定時間非アクティブであったユーザーの接続を自動的に切断する タイムアウト の作成などを行いました。 はじめに PixelStreamingを利用してブラウザの JavaScript を発火させるためには、UE上のBlueprintと JavaScript を紐づける必要があります。 前回のこちらの記事(UE5 PixelStreamingで、WebUI経由でUE Blueprintを操作する) では、ブラウザ上にボタンを設置し押下時にBlueprintを利用してUE画面上に文字をプリントする実装を行いました。 今回はUEを操作してプレイヤーと任意のオブジェクトが 接触 した際にBlueprintで 接触 を感知し、 JavaScript に通信を行いブラウザ上でイベントを発火させる方法について解説します。 検証環境/ツール Unreal Engine5.1 AWS EC2 Windows _Server-2022-English-Full-Base-2023.01.19 Chrome ver.110.0.5481.177 実装手順 1. プレイヤーが任意のオブジェクトに 接触 した際にイベントを発火させる処理 1-1. プレイヤーの他オブジェクトの 接触 を感知するBlueprintを作成 1-2. UEからブラウザに通信する処理を作成 1-3. JavaScript でUEからの通信を受け取る処理を作成 1-4. PixelStreamingを起動して接続テスト 2. 一定時間非アクティブであったユーザーがいた場合 タイムアウト する処理 2-1. タイムアウト 処理の設定 2-2. タイムアウト 処理の実行テスト 1. プレイヤーが任意のオブジェクトに 接触 した際にイベントを発火させる処理 1-1. プレイヤーの他オブジェクトの 接触 を感知するBlueprintを作成 今回の検証では「ThirdPersonTemplate」を使用してプロジェクトを作成します。 プロジェクト作成からPixelStreamingの プラグイン を有効にするまでの流れは、 前回のこちらの記事(UE5 PixelStreamingで、WebUI経由でUE Blueprintを操作する) を参考にしてください。 まずは、プレイヤーが何かに 接触 した際、UE上に文字列を表示するBlueprintを作成します。 使用するBlueprintは「ThirdPersonTemplate」でプロジェクトを作成した際に自動的に作られるプレイヤーアクターのBlueprintである「BP_ThirdPersonCharacter」を使用します。 All>Content>ThirdPerson>Blueprints内に「BP_ThirdPersonCharacter」を見つけることができます。 Blueprint内のEventGraphには既にプレイヤーの動きやカメラ操作についてなどの処理が記述されています。 そのファイル内の空いている箇所に「Event Hit」というイベントノードを追加し、PrintStringと繋ぎます。 これにより、UEプレイ中にプレイヤーアクターが何かに 接触 した際、UE画面上に「Hit」の文字がプリントされます。 1-2. UEからブラウザに通信する処理を作成 ここまではUE上で「Hit」の文字をプリントするだけでしたが、今回はこの 接触 した際のイベントをUEからブラウザに通信しなければいけないので、その処理を追加します。 PixelStreamingの プラグイン を追加した状態でBlueprintエディターの左側のComponentsタブのAddを押下し、PixelStreamingInputを検索し左側の コンポーネント 一覧に追加します。 次に追加されたPixelStreamingInputを右側のグラフに ドラッグ&ドロップ を行い、ノードを追加します。 接触 した際のイベントをUEからブラウザに通信するためには今追加したPixelStreamingInput内のSendPixelStreamingResponseというファンクションを使用します。 SendPixelStreamingResponse内にDescriptorのボックスがあるので、今回はわかりやすく「PlayerHitEvent」と記述します。 次にこのファンクションをプレイヤーが 接触 した際に発火するようにするため、先ほど作成した「Event Hit」のノードとつなげます。 以上でUE側の、ブラウザに通信するための処理は終わりなので JavaScript 側の処理に移っていきます。 1-3. JavaScript でUEからの通信を受け取る処理を作成 前回のブラウザ上にボタンを設置し押下時にBlueprintを利用してUE画面上に文字をプリントする実装の記事 と同様に、 player.html内部に JavaScript の処理を追加していきました。 まずはplayer.html内で呼ばれているapp.jsで提供されているaddResponseEventListener関数を使用して、イベントハンドル用の関数を登録します。 次にplayer.htmlの JavaScript に、UEからの通信を受信するたびに起動する イベントハンドラ 関数を記述します。 これにより、UEのBlueprintで設定したSendPixelStreamingResponseノードにより送信された文字列引数(Descriptorで指定した「PlayerHitEvent」の文字列)が渡されます。 // リスナー登録 addResponseEventListener("handle_responses", myHandleResponseFunction); //イベントハンドラ関数作成 function myHandleResponseFunction(data) { if (data === "PlayerHitEvent") { console.log("プレイヤーがオブジェクトに接触しました"); } else { console.log("その他のイベント"); } } この関数を作成することで、UE側からプレイヤーがオブジェクトに 接触 した際に発行されるBlueprintで、Descriptorで指定した「PlayerHitEvent」の文字列を JavaScript 側で受信できるようになります。 また、より複雑なデータをやり取りする必要がある場合は、Descriptor内の文字列を JSON 形式に記述し、 JavaScript 側で文字列をデコードし JSON として使用する方法も公式で推奨されています。 1-4. PixelStreamingを起動して接続テスト ここまでで、UE側のBlueprintの設定と JavaScript で イベントハンドラ ーとしての設定、ログを出力する処理の全ての実装が完了したので、実際にPixelStreamingを起動して接続テストを行います。 UEのプロジェクトをパッケージ化し、SignallingServerを立ち上げてアプリを起動します。 プレイヤーを操作し、ゲーム内のオブジェクトや壁などに 接触 すると、ブラウザの検証画面から JavaScript で出力したコンソールログを見ることができます。 今回はプレイヤーをジャンプさせ、着地したときに地面のオブジェクトとプレイヤーが 接触 したため、 JavaScript で「プレイヤーがオブジェクトと 接触 しました」というログを出力しています。 今回は検証のため 接触 とログ出力だけを行いましたが、Blueprintと JavaScript を変更することで、 例えばプレイヤーが獲得したアイテムを実際に購入できるECページに遷移させるといった従来の JavaScript で行える処理なども記述できます。 2. 一定時間非アクティブであったユーザーがいた場合 タイムアウト する処理 2-1. タイムアウト 処理の設定 UEを使用してマッチメイキングを行うアプリなどをPixelStreamingで配信したい際、UEのアプリケーションやサーバー負荷の観点から、非アクティブなユーザーの接続を切りたい場合などが出てくると思います。 そういった際に必要になってくるのが非アクティブなユーザーに対する タイムアウト 処理です。 PixelStreaming プラグイン では、app.jsの中で既に タイムアウト の関数が既に用意されています。 デフォルトでは タイムアウト をしない設定になっているので、 JavaScript を使用して設定を変えていきます。 player.htmlの中に下記3つの記述を JavaScript を追加していきます。 afk.enabled = true; AFKとはAway From Keybordの略で、ユーザーが一定期間操作を行っていない事を指します。 デフォルトではfalseになっているので、有効に変更します。 afk.warnTimeout = 60; ここでは タイムアウト までの時間を設定します。デフォルトでは120秒になっているので、今回は60秒に変更しました。 afk.closeTimeout = 20; ここでは「afk.warnTimeout」の期間が過ぎてから、ユーザーに対して接続切断の警告メッセージを表示する秒数を設定します。 画面上ではここで設定した秒数のカウントダウンが表示され、設定した時間を過ぎると接続が切断されます。 2-2. タイムアウト 処理の実行テスト 最後に実際に タイムアウト 処理が実行できるかテストをしてみます。 afk.warnTimeoutで設定した秒数を放置した後、下のような画面が出ていれば、正常に タイムアウト 処理の設定が変更できています。 また、AFKにはemitCommandやemitUIInteraction関数で設定したインタ ラク ションも挙動に反映されてしまうので注意が必要です。 タイムアウト により接続が切断されると、PixelStreaming開始時に表示される画面に変わります。 タイムアウト の処理の説明は以上になります。 所感 UEから JavaScript への通信や、逆に JavaScript からUEへの通信の仕方はとても簡略化してあり、使いやすいと感じました。 文字列でのやり取りだけでなく、Blueprintを工夫することで JSON 形式に変更することで、Webアプリケーションでできることの幅はとても広がりそうだと感じました。 特に ノンゲーム の領域では、別のURLへ遷移させたり、Webアプリケーションにつなげることでプロダクトの幅や品質を向上させることができると思います。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://docs.unrealengine.com/5.1/ja/customizing-the-player-web-page-in-unreal-engine/ 執筆: @okazaki.wataru 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
こんにちは、ISID 金融ソリューション事業部の岡崎です。 今回は 前回のこちらの記事(UE5 PixelStreamingで、WebUI経由でUE Blueprintを操作する) の続きとして、 EpicGames社が提供する ゲームエンジン 、UnrealEngine5 のPlugin「PixelStreaming」を使用し、UE5でブラウザの JavaScript で反応可能なカスタムイベントの作成や、一定時間非アクティブであったユーザーの接続を自動的に切断する タイムアウト の作成などを行いました。 はじめに PixelStreamingを利用してブラウザの JavaScript を発火させるためには、UE上のBlueprintと JavaScript を紐づける必要があります。 前回のこちらの記事(UE5 PixelStreamingで、WebUI経由でUE Blueprintを操作する) では、ブラウザ上にボタンを設置し押下時にBlueprintを利用してUE画面上に文字をプリントする実装を行いました。 今回はUEを操作してプレイヤーと任意のオブジェクトが 接触 した際にBlueprintで 接触 を感知し、 JavaScript に通信を行いブラウザ上でイベントを発火させる方法について解説します。 検証環境/ツール Unreal Engine5.1 AWS EC2 Windows _Server-2022-English-Full-Base-2023.01.19 Chrome ver.110.0.5481.177 実装手順 1. プレイヤーが任意のオブジェクトに 接触 した際にイベントを発火させる処理 1-1. プレイヤーの他オブジェクトの 接触 を感知するBlueprintを作成 1-2. UEからブラウザに通信する処理を作成 1-3. JavaScript でUEからの通信を受け取る処理を作成 1-4. PixelStreamingを起動して接続テスト 2. 一定時間非アクティブであったユーザーがいた場合 タイムアウト する処理 2-1. タイムアウト 処理の設定 2-2. タイムアウト 処理の実行テスト 1. プレイヤーが任意のオブジェクトに 接触 した際にイベントを発火させる処理 1-1. プレイヤーの他オブジェクトの 接触 を感知するBlueprintを作成 今回の検証では「ThirdPersonTemplate」を使用してプロジェクトを作成します。 プロジェクト作成からPixelStreamingの プラグイン を有効にするまでの流れは、 前回のこちらの記事(UE5 PixelStreamingで、WebUI経由でUE Blueprintを操作する) を参考にしてください。 まずは、プレイヤーが何かに 接触 した際、UE上に文字列を表示するBlueprintを作成します。 使用するBlueprintは「ThirdPersonTemplate」でプロジェクトを作成した際に自動的に作られるプレイヤーアクターのBlueprintである「BP_ThirdPersonCharacter」を使用します。 All>Content>ThirdPerson>Blueprints内に「BP_ThirdPersonCharacter」を見つけることができます。 Blueprint内のEventGraphには既にプレイヤーの動きやカメラ操作についてなどの処理が記述されています。 そのファイル内の空いている箇所に「Event Hit」というイベントノードを追加し、PrintStringと繋ぎます。 これにより、UEプレイ中にプレイヤーアクターが何かに 接触 した際、UE画面上に「Hit」の文字がプリントされます。 1-2. UEからブラウザに通信する処理を作成 ここまではUE上で「Hit」の文字をプリントするだけでしたが、今回はこの 接触 した際のイベントをUEからブラウザに通信しなければいけないので、その処理を追加します。 PixelStreamingの プラグイン を追加した状態でBlueprintエディターの左側のComponentsタブのAddを押下し、PixelStreamingInputを検索し左側の コンポーネント 一覧に追加します。 次に追加されたPixelStreamingInputを右側のグラフに ドラッグ&ドロップ を行い、ノードを追加します。 接触 した際のイベントをUEからブラウザに通信するためには今追加したPixelStreamingInput内のSendPixelStreamingResponseというファンクションを使用します。 SendPixelStreamingResponse内にDescriptorのボックスがあるので、今回はわかりやすく「PlayerHitEvent」と記述します。 次にこのファンクションをプレイヤーが 接触 した際に発火するようにするため、先ほど作成した「Event Hit」のノードとつなげます。 以上でUE側の、ブラウザに通信するための処理は終わりなので JavaScript 側の処理に移っていきます。 1-3. JavaScript でUEからの通信を受け取る処理を作成 前回のブラウザ上にボタンを設置し押下時にBlueprintを利用してUE画面上に文字をプリントする実装の記事 と同様に、 player.html内部に JavaScript の処理を追加していきました。 まずはplayer.html内で呼ばれているapp.jsで提供されているaddResponseEventListener関数を使用して、イベントハンドル用の関数を登録します。 次にplayer.htmlの JavaScript に、UEからの通信を受信するたびに起動する イベントハンドラ 関数を記述します。 これにより、UEのBlueprintで設定したSendPixelStreamingResponseノードにより送信された文字列引数(Descriptorで指定した「PlayerHitEvent」の文字列)が渡されます。 // リスナー登録 addResponseEventListener("handle_responses", myHandleResponseFunction); //イベントハンドラ関数作成 function myHandleResponseFunction(data) { if (data === "PlayerHitEvent") { console.log("プレイヤーがオブジェクトに接触しました"); } else { console.log("その他のイベント"); } } この関数を作成することで、UE側からプレイヤーがオブジェクトに 接触 した際に発行されるBlueprintで、Descriptorで指定した「PlayerHitEvent」の文字列を JavaScript 側で受信できるようになります。 また、より複雑なデータをやり取りする必要がある場合は、Descriptor内の文字列を JSON 形式に記述し、 JavaScript 側で文字列をデコードし JSON として使用する方法も公式で推奨されています。 1-4. PixelStreamingを起動して接続テスト ここまでで、UE側のBlueprintの設定と JavaScript で イベントハンドラ ーとしての設定、ログを出力する処理の全ての実装が完了したので、実際にPixelStreamingを起動して接続テストを行います。 UEのプロジェクトをパッケージ化し、SignallingServerを立ち上げてアプリを起動します。 プレイヤーを操作し、ゲーム内のオブジェクトや壁などに 接触 すると、ブラウザの検証画面から JavaScript で出力したコンソールログを見ることができます。 今回はプレイヤーをジャンプさせ、着地したときに地面のオブジェクトとプレイヤーが 接触 したため、 JavaScript で「プレイヤーがオブジェクトと 接触 しました」というログを出力しています。 今回は検証のため 接触 とログ出力だけを行いましたが、Blueprintと JavaScript を変更することで、 例えばプレイヤーが獲得したアイテムを実際に購入できるECページに遷移させるといった従来の JavaScript で行える処理なども記述できます。 2. 一定時間非アクティブであったユーザーがいた場合 タイムアウト する処理 2-1. タイムアウト 処理の設定 UEを使用してマッチメイキングを行うアプリなどをPixelStreamingで配信したい際、UEのアプリケーションやサーバー負荷の観点から、非アクティブなユーザーの接続を切りたい場合などが出てくると思います。 そういった際に必要になってくるのが非アクティブなユーザーに対する タイムアウト 処理です。 PixelStreaming プラグイン では、app.jsの中で既に タイムアウト の関数が既に用意されています。 デフォルトでは タイムアウト をしない設定になっているので、 JavaScript を使用して設定を変えていきます。 player.htmlの中に下記3つの記述を JavaScript を追加していきます。 afk.enabled = true; AFKとはAway From Keybordの略で、ユーザーが一定期間操作を行っていない事を指します。 デフォルトではfalseになっているので、有効に変更します。 afk.warnTimeout = 60; ここでは タイムアウト までの時間を設定します。デフォルトでは120秒になっているので、今回は60秒に変更しました。 afk.closeTimeout = 20; ここでは「afk.warnTimeout」の期間が過ぎてから、ユーザーに対して接続切断の警告メッセージを表示する秒数を設定します。 画面上ではここで設定した秒数のカウントダウンが表示され、設定した時間を過ぎると接続が切断されます。 2-2. タイムアウト 処理の実行テスト 最後に実際に タイムアウト 処理が実行できるかテストをしてみます。 afk.warnTimeoutで設定した秒数を放置した後、下のような画面が出ていれば、正常に タイムアウト 処理の設定が変更できています。 また、AFKにはemitCommandやemitUIInteraction関数で設定したインタ ラク ションも挙動に反映されてしまうので注意が必要です。 タイムアウト により接続が切断されると、PixelStreaming開始時に表示される画面に変わります。 タイムアウト の処理の説明は以上になります。 所感 UEから JavaScript への通信や、逆に JavaScript からUEへの通信の仕方はとても簡略化してあり、使いやすいと感じました。 文字列でのやり取りだけでなく、Blueprintを工夫することで JSON 形式に変更することで、Webアプリケーションでできることの幅はとても広がりそうだと感じました。 特に ノンゲーム の領域では、別のURLへ遷移させたり、Webアプリケーションにつなげることでプロダクトの幅や品質を向上させることができると思います。 現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております(カテゴリー「3DCG」の記事は こちら )。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ぜひお気軽にご連絡ください! 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ(Web3/メタバース/AI) 参考 https://docs.unrealengine.com/5.1/ja/customizing-the-player-web-page-in-unreal-engine/ 執筆: @okazaki.wataru 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
金融ソリューション事業部 石沢です。ISIDは現在全社で1on1の実施を奨励しており、私自身も部署のメンバーに対して1on1を実施しています。本記事ではISIDにおける1on1の状況と、昨年参加したエール社提供の1on1改善ワークショップ【聴くトレ】に参加した経験と感想をご紹介します。 ISIDにおける1on1の状況 ISIDでは現在1on1を人事施策として奨励しています。実施は強制ではないですが、社として様々な支援を受けることが可能で、実施のためのガイダンスやツール( TeamUp )を希望者は全員利用できるようになっています。ISIDの1on1の取組みは以下サイトでも紹介されていますので、興味があれば参照してください。 わたしと1on1 現在は社の人事施策として推進されている1on1ですが、わたし自身はそれより数年前から書籍やネット記事などを参考に自分のプロジェクトに実験的に適用していました。その時に参考にしていた情報は書籍「 HIGH OUTPUT MANAGEMENT 」(当時は旧版「 インテル 経営の秘密」)など、あとは海外で働く日本人エンジニアの記ネット記事などです。あとは、 見よう見まね ですね。 ISIDでは現場でいろいろ勝手に工夫する文化があるので、わたしと同じようにいろいろな現場で1on1の取組みは進んでいたようです。すぐに社内でも「1on1をやってみたらよかった」「所属部員とのコミュニケーションに1on1を活用するのが良い」という声がすぐに聞こえるようになっていました。社の規定で定められた上司部下の面談はあるのですが、それを拡張してみんな工夫していったという感じです。 そんな中で、2021年には人事部主導で1on1を支援するツールのトライアルや全社展開なども行われ、1on1に関する基本的な ガイドライン なども公開されました。現在では多くの社員が1on1をやっている状況になっています。利用は任意ですが、支援ツールの TeamUp はリマインドや記録機能などが便利で、わたしは愛用しています。わたし自身は隔週の頻度でメンバーとの1on1を日常的に実施しています。 【聴くトレ】参加経緯 日常的に1on1をやってはいるのですが、すこし不安はありました。 自分が1on1を効果的にやれているのかわからない 改善すべき点があるのかどうかもわからない ちょうどそのころ読んでいた「 ヤフーの1on1 」という本で、ヤフーさんでは教育施策として「1on1チェック」というアセスメントや研修をやっているという事が紹介されていました。 うらやま…… 実際のメンバーとの1on1は、内容的にも第 三者 に見てもらったり、話した内容を開示するわけにもいかないし、振返ってみると 練習する場に飢えていたんだ と思います。 素振りがしたいんや…… そう思っていたところに、人事部から エール社さんの 【聴くトレ】 のトライアルをやるので参加者を公募するという話が飛び込んできたので、さっそく飛びついて参加してみたのです。 【聴くトレ】とは 具体的な内容は公式サイトを見ていただくとして、ざっくりこのような内容でした。 キック オフミ ーティングで取組みの説明とコンセプトの話を聞く 自習用動画が配布されるので、【聴くトレ】が重視する注意点、コツを学ぶ サポーターと呼ばれる社外メンターと練習や相談をする 実際に1on1をやる 1on1を評価してもらい、よりよくするためのアド バイス を貰う アド バイス して貰ったことが実務に旨く適用できたかのふりかえりを一緒にやる これは私の問題意識にピッタリでした。 【聴くトレ】の感想 個人的には自分の1on1について振り返る良い機会だったと思います。また社外の方と1on1をすることも新鮮で受講して良かった。ずっと受講し続けたいというわけではありませんが、定期的にこういったサービスで自分のフォームを点検できるのは大変良いと思います。今回はトライアルということでしたが、人事部でも今後継続する方向性で検討しているということで、期待です。 その他の細かい感想はこちら コストパフォーマンスは未確認です(人事部さんありがとう!) サポーターとのマッチング問題はありそうですが、わたしを担当していただいた方は非常に面白かったです。ただ、実際の1on1でも相性の問題はあるはずなので苦手なタイプの人とマッチングしてしまっても、それも修行だと思えば楽しめそう セッションはVoice Onlyのオンライン実施形式(専用のサポートサイト経由での実施となります)。表情が見えないのはちょっと残念でしたが、場所を選ばずに実施できるというメリットもありそう セッションは緊張します(特に初回)。どんなことを言われるのだろうかという不安もありましたが、至らない点もやさしくフィードバックしてもらえて杞憂でしたが…… 普段つかわない筋肉を使ったような疲れはありました(それがやりたかったので問題はないのですが) 詳しくはナイショですが、自分自身の1on1についてはひとつ重要な課題を発見できて良かったです。 客観的に観察してもらえるというのは、やはりよいですね 。うまく解消できているかはわかりませんが、よりそったアド バイス をしていただき、練習させてもらったので改善したと信じたい むすびにかえて 本記事ではISIDの1on1の取組みと、管理職として1on1力をレベルアップしようとしてみたわたしの経験についてご紹介しましたが、いかがでしたか。1on1がうまくいかないというお悩みをお持ちの管理職な方はエールさんのサービスをチェックすると良いと思いますが、むしろそういったサポートも受けられるISIDへの転職もご検討してはいかがでしょうか? 私たちは一緒に働いてくれる仲間を募集しています! ISID 募集職種一覧 執筆: Ishizawa Kento (@kent) 、レビュー: @kou.kinyo ( Shodo で執筆されました )
金融ソリューション事業部 石沢です。ISIDは現在全社で1on1の実施を奨励しており、私自身も部署のメンバーに対して1on1を実施しています。本記事ではISIDにおける1on1の状況と、昨年参加したエール社提供の1on1改善ワークショップ【聴くトレ】に参加した経験と感想をご紹介します。 ISIDにおける1on1の状況 ISIDでは現在1on1を人事施策として奨励しています。実施は強制ではないですが、社として様々な支援を受けることが可能で、実施のためのガイダンスやツール( TeamUp )を希望者は全員利用できるようになっています。ISIDの1on1の取組みは以下サイトでも紹介されていますので、興味があれば参照してください。 わたしと1on1 現在は社の人事施策として推進されている1on1ですが、わたし自身はそれより数年前から書籍やネット記事などを参考に自分のプロジェクトに実験的に適用していました。その時に参考にしていた情報は書籍「 HIGH OUTPUT MANAGEMENT 」(当時は旧版「 インテル 経営の秘密」)など、あとは海外で働く日本人エンジニアの記ネット記事などです。あとは、 見よう見まね ですね。 ISIDでは現場でいろいろ勝手に工夫する文化があるので、わたしと同じようにいろいろな現場で1on1の取組みは進んでいたようです。すぐに社内でも「1on1をやってみたらよかった」「所属部員とのコミュニケーションに1on1を活用するのが良い」という声がすぐに聞こえるようになっていました。社の規定で定められた上司部下の面談はあるのですが、それを拡張してみんな工夫していったという感じです。 そんな中で、2021年には人事部主導で1on1を支援するツールのトライアルや全社展開なども行われ、1on1に関する基本的な ガイドライン なども公開されました。現在では多くの社員が1on1をやっている状況になっています。利用は任意ですが、支援ツールの TeamUp はリマインドや記録機能などが便利で、わたしは愛用しています。わたし自身は隔週の頻度でメンバーとの1on1を日常的に実施しています。 【聴くトレ】参加経緯 日常的に1on1をやってはいるのですが、すこし不安はありました。 自分が1on1を効果的にやれているのかわからない 改善すべき点があるのかどうかもわからない ちょうどそのころ読んでいた「 ヤフーの1on1 」という本で、ヤフーさんでは教育施策として「1on1チェック」というアセスメントや研修をやっているという事が紹介されていました。 うらやま…… 実際のメンバーとの1on1は、内容的にも第 三者 に見てもらったり、話した内容を開示するわけにもいかないし、振返ってみると 練習する場に飢えていたんだ と思います。 素振りがしたいんや…… そう思っていたところに、人事部から エール社さんの 【聴くトレ】 のトライアルをやるので参加者を公募するという話が飛び込んできたので、さっそく飛びついて参加してみたのです。 【聴くトレ】とは 具体的な内容は公式サイトを見ていただくとして、ざっくりこのような内容でした。 キック オフミ ーティングで取組みの説明とコンセプトの話を聞く 自習用動画が配布されるので、【聴くトレ】が重視する注意点、コツを学ぶ サポーターと呼ばれる社外メンターと練習や相談をする 実際に1on1をやる 1on1を評価してもらい、よりよくするためのアド バイス を貰う アド バイス して貰ったことが実務に旨く適用できたかのふりかえりを一緒にやる これは私の問題意識にピッタリでした。 【聴くトレ】の感想 個人的には自分の1on1について振り返る良い機会だったと思います。また社外の方と1on1をすることも新鮮で受講して良かった。ずっと受講し続けたいというわけではありませんが、定期的にこういったサービスで自分のフォームを点検できるのは大変良いと思います。今回はトライアルということでしたが、人事部でも今後継続する方向性で検討しているということで、期待です。 その他の細かい感想はこちら コストパフォーマンスは未確認です(人事部さんありがとう!) サポーターとのマッチング問題はありそうですが、わたしを担当していただいた方は非常に面白かったです。ただ、実際の1on1でも相性の問題はあるはずなので苦手なタイプの人とマッチングしてしまっても、それも修行だと思えば楽しめそう セッションはVoice Onlyのオンライン実施形式(専用のサポートサイト経由での実施となります)。表情が見えないのはちょっと残念でしたが、場所を選ばずに実施できるというメリットもありそう セッションは緊張します(特に初回)。どんなことを言われるのだろうかという不安もありましたが、至らない点もやさしくフィードバックしてもらえて杞憂でしたが…… 普段つかわない筋肉を使ったような疲れはありました(それがやりたかったので問題はないのですが) 詳しくはナイショですが、自分自身の1on1についてはひとつ重要な課題を発見できて良かったです。 客観的に観察してもらえるというのは、やはりよいですね 。うまく解消できているかはわかりませんが、よりそったアド バイス をしていただき、練習させてもらったので改善したと信じたい むすびにかえて 本記事ではISIDの1on1の取組みと、管理職として1on1力をレベルアップしようとしてみたわたしの経験についてご紹介しましたが、いかがでしたか。1on1がうまくいかないというお悩みをお持ちの管理職な方はエールさんのサービスをチェックすると良いと思いますが、むしろそういったサポートも受けられるISIDへの転職もご検討してはいかがでしょうか? 私たちは一緒に働いてくれる仲間を募集しています! ISID 募集職種一覧 執筆: Ishizawa Kento (@kent) 、レビュー: @kou.kinyo ( Shodo で執筆されました )
こんにちは! 電通国際情報サービス 人事部 人材・組織開発グループの川谷です。 今回は、 UdemyBusiness (以下UB)を活用し、 個々に沿った内定者向け教育コンテンツを提供した内容を共有します。 本記事をこんな方におすすめします 就職活動を控えた大学生の方 Udemy,UBの受講を検討されている方 自社でUBを導入済み / 検討中の研修担当者 本記事のあらすじ ISIDでの内定者向け教育コンテンツにおいて、従来の全体一律ト レーニン グ型から「全体のベースアップ+個々人に沿うスキルト レーニン グ」のハイブリッド型に提供をシフトした。 内定者が各自必要とするインプットを得られるようにUBを導入した。そして、入社後の教育にもUBを利用することで、働きながら学ぶ姿勢を支援できる仕組みとした。 Udemy・Udemy Businessとは Udemyは、プログラミングやビジネス、 自己啓発 など様々な分野のコンテンツを提供する、オンライン学習プラットフォームです。各分野の専門家が、合計200,000を超えるコンテンツを提供しています。 UBは、そんなUdemyの講座群から、一般レビューをもとに厳選された講座を受け放題で受講できる法人向けのサービスです。 ISIDにおけるこれまでのUB活用 ISIDでは、2022年上旬にUBを全社導入しました。 公募制で展開しており、23年2月21日現在、約500名程度が受講しています。 コンテンツの質・受講自由度の高さから、受講人数は期を重ねるごとに増加傾向にあり、効果的な受講支援や階層別研修の補助利用など活用の幅を広げております。(UB受講者用Teamsの作成、新人研修の補助としての導入など) これまでのISIDにおける内定者教育について ISIDでは内定者に対して、入社後の早期活躍を支援すべく「心・技・体」の観点で学習機会を提供してきました。 (記載の教育施策は一例です) これまでの教育施策は、主にIT理解度( 基本情報技術者試験 (FE)の取得状況など)に応じて個別に内容を変更していました。しかし、その施策内容の検討/選定が難しく課題となっていました。 そのため、内定者自身が、各自のレベルに合った講座を自由に選べる点が、UB内定者提供に踏み切った理由の一つでした。 そんな内定者教育に、UBをどう活用しているのか? UBのコンテンツ数の多さ、そしてそれらを自由に選んで受講できるメリットを活かし、下記の通り必須受講と任意受講を組み合わせて運用しています。 必須受講 IT理解度が低い人向けに、IT基礎知識を学ぶ講座を人事部で選定し、受講を指示( 基本情報技術者試験 に関するコンテンツなど) 任意受講 一部講座を推薦(ビジネスマナーやセルフマネジメント、論理思考力など) その他、自身が学習したい分野、不足していると思う分野に関する講座 上記の通りISIDではUBを、内定者個々の学習意欲に合わせた教育コンテンツ提供にも活用しています。 どう管理しているの?受講生の実施までチェックしているの? 前章で紹介した推奨コンテンツは、UBの"ラーニングパス機能"を用いて、各内定者の受講状況をチェックできるようにしています。ラーニングパスとは、UB上の講座や社内のコンテンツ、外部リンクなどを組み合わせて作成する"音楽のプレイリスト"のようなものであり、受講状況もチェック可能です。 この機能を活用し、学んでほしい内容を詰め込んだ内定者教育専用のラーニングパスを作成して、展開および受講状況をチェックしています。 人事部としての所感 結論としては、UBは内定者教育にもフィットしている実感があります。 理由は、これまで課題と感じていた「内定者それぞれに合わせた教育コンテンツ提供」が、前述の通りUBが有する豊富なコンテンツ量・受講者の管理が容易なラーニングパスによって対処できていると感じるためです。 内定者によるフィードバックはこれからですが、自身の興味に合うコンテンツをいつでも自由に受講できるUBは、受講者側にも大きなメリットなのではないかと個人的に思っています。 今後の展望 今後の展望としては、以下のようにUBを活用したいと考えています。 (入社後の教育)入社後の教育にもUBを活用し、学び続ける姿勢をサポートしたい。 (今後の内定者研修)今年度と同様に内定者期間にUBを提供。内定者からのフィードバックを受け、ラーニングパスの中身をブラッシュアップする。 おわりに 以上が、弊社の内定者教育にUBを活用した事例の紹介です。 UBを内定者に提供することは、業務の傍ら学び続ける習慣のキッカケになると確信しております。 もちろん内定者教育をeラーニングだけに留めようという思いはありません。ただUBが簡単に活用できるからこそ、より一層狙いを持って、eラーニングもしくは対面型研修を選択するべきだと私個人としては考えています。 本記事が誰かのお役に立てば幸いです。 執筆: @kawatani.tomoro 、レビュー: @nakamura.toshihiro ( Shodo で執筆されました )
こんにちは! 電通国際情報サービス 人事部 人材・組織開発グループの川谷です。 今回は、 UdemyBusiness (以下UB)を活用し、 個々に沿った内定者向け教育コンテンツを提供した内容を共有します。 本記事をこんな方におすすめします 就職活動を控えた大学生の方 Udemy,UBの受講を検討されている方 自社でUBを導入済み / 検討中の研修担当者 本記事のあらすじ ISIDでの内定者向け教育コンテンツにおいて、従来の全体一律ト レーニン グ型から「全体のベースアップ+個々人に沿うスキルト レーニン グ」のハイブリッド型に提供をシフトした。 内定者が各自必要とするインプットを得られるようにUBを導入した。そして、入社後の教育にもUBを利用することで、働きながら学ぶ姿勢を支援できる仕組みとした。 Udemy・Udemy Businessとは Udemyは、プログラミングやビジネス、 自己啓発 など様々な分野のコンテンツを提供する、オンライン学習プラットフォームです。各分野の専門家が、合計200,000を超えるコンテンツを提供しています。 UBは、そんなUdemyの講座群から、一般レビューをもとに厳選された講座を受け放題で受講できる法人向けのサービスです。 ISIDにおけるこれまでのUB活用 ISIDでは、2022年上旬にUBを全社導入しました。 公募制で展開しており、23年2月21日現在、約500名程度が受講しています。 コンテンツの質・受講自由度の高さから、受講人数は期を重ねるごとに増加傾向にあり、効果的な受講支援や階層別研修の補助利用など活用の幅を広げております。(UB受講者用Teamsの作成、新人研修の補助としての導入など) これまでのISIDにおける内定者教育について ISIDでは内定者に対して、入社後の早期活躍を支援すべく「心・技・体」の観点で学習機会を提供してきました。 (記載の教育施策は一例です) これまでの教育施策は、主にIT理解度( 基本情報技術者試験 (FE)の取得状況など)に応じて個別に内容を変更していました。しかし、その施策内容の検討/選定が難しく課題となっていました。 そのため、内定者自身が、各自のレベルに合った講座を自由に選べる点が、UB内定者提供に踏み切った理由の一つでした。 そんな内定者教育に、UBをどう活用しているのか? UBのコンテンツ数の多さ、そしてそれらを自由に選んで受講できるメリットを活かし、下記の通り必須受講と任意受講を組み合わせて運用しています。 必須受講 IT理解度が低い人向けに、IT基礎知識を学ぶ講座を人事部で選定し、受講を指示( 基本情報技術者試験 に関するコンテンツなど) 任意受講 一部講座を推薦(ビジネスマナーやセルフマネジメント、論理思考力など) その他、自身が学習したい分野、不足していると思う分野に関する講座 上記の通りISIDではUBを、内定者個々の学習意欲に合わせた教育コンテンツ提供にも活用しています。 どう管理しているの?受講生の実施までチェックしているの? 前章で紹介した推奨コンテンツは、UBの"ラーニングパス機能"を用いて、各内定者の受講状況をチェックできるようにしています。ラーニングパスとは、UB上の講座や社内のコンテンツ、外部リンクなどを組み合わせて作成する"音楽のプレイリスト"のようなものであり、受講状況もチェック可能です。 この機能を活用し、学んでほしい内容を詰め込んだ内定者教育専用のラーニングパスを作成して、展開および受講状況をチェックしています。 人事部としての所感 結論としては、UBは内定者教育にもフィットしている実感があります。 理由は、これまで課題と感じていた「内定者それぞれに合わせた教育コンテンツ提供」が、前述の通りUBが有する豊富なコンテンツ量・受講者の管理が容易なラーニングパスによって対処できていると感じるためです。 内定者によるフィードバックはこれからですが、自身の興味に合うコンテンツをいつでも自由に受講できるUBは、受講者側にも大きなメリットなのではないかと個人的に思っています。 今後の展望 今後の展望としては、以下のようにUBを活用したいと考えています。 (入社後の教育)入社後の教育にもUBを活用し、学び続ける姿勢をサポートしたい。 (今後の内定者研修)今年度と同様に内定者期間にUBを提供。内定者からのフィードバックを受け、ラーニングパスの中身をブラッシュアップする。 おわりに 以上が、弊社の内定者教育にUBを活用した事例の紹介です。 UBを内定者に提供することは、業務の傍ら学び続ける習慣のキッカケになると確信しております。 もちろん内定者教育をeラーニングだけに留めようという思いはありません。ただUBが簡単に活用できるからこそ、より一層狙いを持って、eラーニングもしくは対面型研修を選択するべきだと私個人としては考えています。 本記事が誰かのお役に立てば幸いです。 執筆: @kawatani.tomoro 、レビュー: @nakamura.toshihiro ( Shodo で執筆されました )
スマートなタイトルが思いつきませんでした。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 インターネットにWebアプリケーションを公開すると、さまざまなHTTPリク エス トを受けることになります。中にはアプリケーションを利用する目的ではない、 情報収集や 脆弱性 を突いて攻撃を狙うリク エス トも多数含まれています。攻撃への根本対策はWebアプリケーションレベルで行うべきですが、対策漏れや ゼロデイ攻撃 に対してはWAFの利用も効果的です。 AWS WAFには AWSマネージドルール が多数用意されており、簡単に導入し多層防御を実現できます。この記事では AWS WAFによるブロックをより厳しくする方法として、WAFのルールがブロックしたリク エス トの IPアドレス を、悪意のあるリク エス トを送信してくる可能性がある IPアドレス とみなして、一定期間 IPアドレス レベルでブロックし続ける仕組みをご紹介します。一度WAFのWeb ACL でブロックに成功しても、攻撃者はパターンを変えて何度も攻撃を試みる場合があり、IPレベルで一定期間ブロックすることで他のWAFルールが対応していないパターンで攻撃が成立するリスクを減らすことを期待しています。 インフラは AWS CDK で実装します。 どんな Web アプリが適しているか アーキテクチャ 1.サブスクリプションフィルターでWAFのログからLambdaを起動 2.DynamoDBのTTLで一定期間後にブロックを解除 3.CDKデプロイ用にSSM Parameter Storeを利用 4.WAF IPセットの楽観ロックで同時更新に対応 サンプルコード 事前準備 (CDK) IPアドレスリストの取得 (CDK) WAFの作成 (CDK) WAFのログを出力 (CDK) DynamoDBテーブルの作成 (CDK) ブロック対象を追加するLambda関数 (CDK) サブスクリプションフィルター (CDK) EventBridgeでIPアドレスリストを同期 (Lambda) パッケージのインポートとSDKクライアントの作成 (Lambda) ハンドラー (Lambda) ログの抽出 (Lambda) IPアドレスをDynamoDBテーブルに追加 (Lambda) DynamoDBからIPアドレスリストを取得 (Lambda) DynamoDBのIPアドレスリストを同期する関数 どんな Web アプリが適しているか 不特定多数の利用者を想定した一般公開のアプリには、これから紹介する仕組みは向かないと思います。WAFの誤検知によってリク エス トをブロックしてしまった場合の影響が大きいからです。社内用アプリなど、利用ユーザーは特定できるがインターネットからアクセスできるアプリがこの仕組みに適していると思います。 アーキテクチャ 1. サブスクリプション フィルターでWAFのログからLambdaを起動 AWS WAFで IPアドレス レンジレベルでリク エス トを許可・ブロックするためには、 IPセット を定義します。WAFのブロック結果からこのIPセットを動的に変更すれば良いので、WAFのブロックログをCloudWatch Logsに出力し、 サブスクリプションフィルター で特定のログをトリガーにLambda関数を起動します。 2.DynamoDBの TTL で一定期間後にブロックを解除 IPアドレス とユーザーの関連は変わり得るので、一定期間経過後は IPアドレス レベルのブロックを自動で解除し、ブロック対象の IPアドレス リストの長さが増え続けないようにします。誤動作で特定の IPアドレス がブロックされてしまった場合の影響を小さくします。 DynamoDBの TTL を利用します。ブロック対象となる IPアドレス をDynamoDBテーブルのレコードとして保持し、 TTL を設定します。EventBridgeで定期的にLambda関数を起動し、DynamoDBテーブルのアイテムのうち、有効期限内の IPアドレス のみでWAFのIPセットを更新します。 3.CDKデプロイ用にSSM Parameter Storeを利用 今回はCDKでインフラを構築する前提なので、IPセットもCDKコードに記載することになります。ただしIPセットの中身は、CDKデプロイ時に先祖返りしないように SSM Parameter Storeパラメータから取得 するようにします。Lambda関数でDynamoDBテーブルから有効な IPアドレス を取得した後は、このパラメータも更新するようにします。 最終的な アーキテクチャ は次の図の通りになります。ブロック対象の IPアドレス 情報はWAFのIPセット、DynamoDBテーブル、Parameter Storeパラメータと3箇所に分散していますが、DynamoDBテーブルのデータを正とし、IPセットとParameter StoreパラメータのデータはDynamoDBテーブルから結果整合的に更新されるようになっています。 4.WAF IPセットの楽観ロックで同時更新に対応 同じIPセットを更新する箇所が複数あり、また複数のWAFブロックログが同時に出力された場合はブロック対象 IPアドレス の追加が同時に実行される可能性があります。この複数の更新に対する 排他制御 の仕組みが、IPセットの API にあります。 WAF IPセットを取得する GetIPSet API のレスポンスで LockToken を取得し、IPセットを更新する UpdateIPSet API を呼びだす時のリク エス トに付加することで、楽観ロックを容易に実現できます。 WAF IPセットを更新するUpdateIPSet API は、個別の IPアドレス レンジを追加、削除するのではなく、更新時に IPアドレス レンジの全体を置き換えるような API 仕様になっているため、このような楽観ロックの仕組みが用意されているのだと思います。 サンプルコード 以上で説明した仕組みを実現するためのサンプルコードをご紹介します。 事前準備 ブロック対象の IPアドレス リストを保持するParameter Storeパラメータを手動で作成しておきます。ここでは MALICIOUS_IP_LIST というパラメータ名とします。空文字では登録できないので初期値は半角スペース 1 つとし、Lambda関数で動的に更新していきます。 1.1.1.1/32;2.2.2.2/32 のように、複数のIP CIDRを セミ コロンで区切るフォーマットを想定します。 (CDK) IPアドレス リストの取得 Parameter Storeパラメータより IPアドレス リストを取得し、文字列型の配列に変換します。それを利用してWAFのIPセットを定義します。 const ipListParamName = "MALICIOUS_IP_LIST" ; const maliciousIpSet = ssm.StringParameter.valueFromLookup ( this , ipListParamName ) .split ( ";" ) .map (( ip ) => ip.trim ()) .filter (( ip ) => ip ); const wafIpSetName = "MaliciousIpSet" ; const wafIpSet = new wafv2.CfnIPSet ( this , "MaliciousIpSet" , { name: wafIpSetName , ipAddressVersion: "IPV4" , scope: "REGIONAL" , // CloudFront用のWAFの場合は "CLOUDFRONT" addresses: maliciousIpSet , } ); またCDKデプロイする際には、 コンテキスト をリセットして常にParameter Storeから最新のパラメータを取得するように、 cdk deploy の前に cdk context --clear を実行するようにします。 cdk context --clear cdk deploy MyStack --require-approval never (CDK) WAFの作成 WAFを作成します。最初に評価されるルールに IpConstraintStatement を作成し、先ほど作成した IPセットをブロックします。他のルールは AWSManagedRulesCommonRuleSet など、自由に追加します。 WAFのALBやCloudFrontなどへの関連付けは省略します。 const wafWebAcl = new wafv2.CfnWebACL ( this , "WafV2WebAcl" , { defaultAction: { allow: {} } , scope: "REGIONAL" , // CloudFront用のWAFの場合は "CLOUDFRONT" visibilityConfig: { cloudWatchMetricsEnabled: true , sampledRequestsEnabled: true , metricName: "WafV2WebAcl" , } , rules: [ { name: "IpConstraintStatement" , priority: 10 , statement: { ipSetReferenceStatement: { arn: wafIpSet.attrArn , } , } , action: { block: {} } , visibilityConfig: { cloudWatchMetricsEnabled: true , sampledRequestsEnabled: true , metricName: "IpConstraintStatement" , } , } , { name: "AWSManagedRulesCommonRuleSet" , priority: 20 , statement: { managedRuleGroupStatement: { vendorName: "AWS" , name: "AWSManagedRulesCommonRuleSet" , } , } , overrideAction: { none: {} } , visibilityConfig: { cloudWatchMetricsEnabled: true , sampledRequestsEnabled: true , metricName: "AWSManagedRulesCommonRuleSet" , } , } , // 他のルールは省略 ] , } ); (CDK) WAFのログを出力 WAFのBLOCKログとCOUNTログをCloudWatch Logsに出力します。 const wafLogGroup = new logs.LogGroup ( this , "WafLogGroup" , { logGroupName: "waf-logs" , } ); const logConfig = new wafv2.CfnLoggingConfiguration ( this , "WafV2LoggingConfiguration" , { logDestinationConfigs: [ `arn:aws:logs: ${ region } : ${ accountId } :log-group: ${ wafLogGroup.logGroupName } ` ] , resourceArn: wafWebAcl.attrArn , loggingFilter: { DefaultBehavior: "DROP" , Filters: [ { Behavior: "KEEP" , Conditions: [{ ActionCondition: { Action: "BLOCK" } } , { ActionCondition: { Action: "COUNT" } }] , Requirement: "MEETS_ANY" , } , ] , } , } ); logConfig.addDependsOn ( wafWebAcl ); (CDK) DynamoDBテーブルの作成 ブロック対象 IPアドレス 用のDynamoDBテーブルを作成します。 パーティション キーはIP CIDRで、 ip_range というキー名にします。 TTL は expires という名前の属性にしています。 const ipTable = new dynamodb.Table ( this , "MaliciousIpTable" , { tableName: "malicious-ip" , partitionKey: { name: "ip_range" , type : dynamodb.AttributeType.STRING } , billingMode: dynamodb.BillingMode.PAY_PER_REQUEST , timeToLiveAttribute: "expires" , } ); (CDK) ブロック対象を追加するLambda関数 ブロック対象の IPアドレス を追加するLambda関数を作成します。関数内で利用するための 環境変数 をいくつか設定し、必要な API コールをするための権限を渡します。 const autoBlockMaliciousIpFunction = new lambdaNodejs.NodejsFunction ( this , "AutoBlockMaliciousIpFunction" , { entry: "functions/auto-block-malicious-ip.ts" , runtime: Runtime.NODEJS_18_X , timeout: Duration.minutes ( 3 ), environment: { TABLE_NAME: ipTable.tableName , // DynamoDBテーブル名 IP_LIST_PARAMETER_NAME: ipListParamName , // Parameter Storeパラメータ名 IP_SET_NAME: wafIpSetName , // IPセット名 IP_SET_ID: wafIpSet.attrId , // IPセットID IP_SET_SCOPE: "REGIONAL" , // IPセットスコープ。CloudFront用のWAFの場合は "CLOUDFRONT" } , } ); // DynamoDBテーブルの参照・更新権限 ipTable.grantReadWriteData ( autoBlockMaliciousIpFunction ); // IPセットの参照・更新権限 autoBlockMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ wafIpSet.attrArn ] , actions: [ "wafv2:GetIpSet" , "wafv2:UpdateIPSet" ] , effect: iam.Effect.ALLOW , } ) ); // パラメータの更新権限 autoBlockMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ `arn:aws:ssm: ${ region } : ${ accountId } :parameter/ ${ ipListParamName } ` ] , actions: [ "ssm:PutParameter" ] , effect: iam.Effect.ALLOW , } ) ); (CDK) サブスクリプション フィルター ブロックログに対して サブスクリプション フィルターでLambda関数を起動させます。ただし IpConstraintStatement ルール自体でブロックされた場合はLambda関数を起動しないようにします。他にも地理的一致条件や、他の IPアドレス 条件でブロックされた場合、対象の IPアドレス はどちらにせよブロックされるので、Lambda関数の起動条件から除外しても良いでしょう。さらに会社の IPアドレス の場合はブロック対象外とするなど、フィルターパターンは要件に応じていろいろとカスタマイズできるでしょう。 const filter = wafLogGroup.addSubscriptionFilter ( "WafLogFilter" , { destination: new destinations.LambdaDestination ( autoBlockMaliciousIpFunction ), filterPattern: logs.FilterPattern.literal ( '{ $.action = "BLOCK" && $.terminatingRuleId != "IpConstraintStatement" }' ), } ); // https://github.com/aws/aws-cdk/issues/23177 へのワークアラウンド // これがないと初回デプロイで失敗する ( filter.node.defaultChild as logs.CfnSubscriptionFilter ) .addDependency ( filter.node.findChild ( "CanInvokeLambda" ) as CfnPermission ); (CDK) EventBridgeで IPアドレス リストを同期 定期的にEventBridgeで起動し、DynamoDBの IPアドレス リストをParameter StoreパラメータとIPセットに同期するLambda関数を作成します。EventBridgeルールは、ここでは1時間に1度起動するようにしています。 const autoUpdateMaliciousIpFunction = new lambdaNodejs.NodejsFunction ( this , "AutoUpdateMaliciousIpFunction" , { entry: "functions/auto-update-malicious-ip.ts" , runtime: Runtime.NODEJS_18_X , timeout: Duration.minutes ( 1 ), environment: { TABLE_NAME: ipTable.tableName , IP_LIST_PARAMETER_NAME: ipListParamName , IP_SET_NAME: wafIpSetName , IP_SET_ID: wafIpSet.attrId , IP_SET_SCOPE: "REGIONAL" , } , } ); // DynamoDBテーブルの参照権限のみ ipTable.grantReadData ( autoUpdateMaliciousIpFunction ); autoUpdateMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ wafIpSet.attrArn ] , actions: [ "wafv2:GetIpSet" , "wafv2:UpdateIPSet" ] , effect: iam.Effect.ALLOW , } ) ); autoUpdateMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ `arn:aws:ssm: ${ region } : ${ accountId } :parameter/ ${ ipListParamName } ` ] , actions: [ "ssm:PutParameter" ] , effect: iam.Effect.ALLOW , } ) ); new events.Rule ( this , "AutoUpdateWafIpListEventRule" , { schedule: events.Schedule.cron ( { minute: "0" } ), targets: [ new eventTargets.LambdaFunction ( autoUpdateMaliciousIpFunction ) ] , } ); (Lambda) パッケージのインポートと SDK クライアントの作成 ここからはブロック対象の IPアドレス を追加するLambda関数のコードを紹介します。まず必要なパッケージのインポートと、使用する各サービスの SDK クライアントを作成する部分です。 // functions/auto-block-malicious-ip.ts import * as zlib from "zlib"; import { DynamoDBClient, ScanCommand, ScanCommandInput, AttributeValue, PutItemCommand, } from "@aws-sdk/client-dynamodb"; import { SSMClient, PutParameterCommand } from "@aws-sdk/client-ssm"; import { WAFV2Client, UpdateIPSetCommand, GetIPSetCommand } from "@aws-sdk/client-wafv2"; import { Handler, CloudWatchLogsEvent, CloudWatchLogsLogEvent, CloudWatchLogsDecodedData } from "aws-lambda"; const dynamodbClient = new DynamoDBClient({ region: process.env.AWS_REGION }); const ssmClient = new SSMClient({ region: process.env.AWS_REGION }); const wafClient = new WAFV2Client({ region: process.env.AWS_REGION }); (Lambda) ハンドラー サブスクリプション フィルターを受けて起動する関数なので、引数のイベントは CloudWatchLogsEvent 型になります。全体の処理の流れはこの通りです。 1. ログイベントを抽出 2. IPアドレスをDynamoDBテーブルに追加 3. WAF IPセットのロックトークンを取得(楽観ロック開始) 4. DynamoDBからIPアドレスリストを取得 5. Parameter Storeパラメータを更新 6. WAF IPセットを更新。失敗したら3からやり直す export const handler: Handler = async ( input: CloudWatchLogsEvent ) => { if ( ! process .env.IP_SET_ID || ! process .env.IP_SET_NAME || ! process .env.TABLE_NAME || ! process .env.IP_LIST_PARAMETER_NAME || ! process .env.IP_SET_SCOPE || ! [ "REGIONAL" , "CLOUDFRONT" ] .includes ( process .env.IP_SET_SCOPE ) ) { return; } // 1. ログイベントを抽出(関数の中身は後述) const logEvents = await extractAwsLogEvents ( input ); // 各イベントに対してループ処理 for ( const event of logEvents ) { type LogMessageType = { httpRequest?: { clientIp?: string ; } ; } ; const message = JSON .parse ( event.message ) as LogMessageType ; if ( ! message.httpRequest || ! message.httpRequest.clientIp ) continue; // 2. IPアドレスをDynamoDBテーブルに追加(関数の中身は後述) await addDynamodbItem ( { tableName: process .env.TABLE_NAME , ipAddress: message.httpRequest.clientIp } ); let retryCount = 10 ; while ( retryCount > 0 ) { // 3. WAF IPセットのロックトークンを取得(楽観ロック開始) const { LockToken: lockToken } = await wafClient.send ( new GetIPSetCommand ( { Name: process .env.IP_SET_NAME , Scope: process .env.IP_SET_SCOPE , Id: process .env.IP_SET_ID , } ) ); // 4. DynamoDBからIPアドレスリストを取得(関数の中身は後述) const ipRanges: string [] = await getAllIpsFromDynamodb ( { tableName: process .env.TABLE_NAME } ); const ipRangeParam = ipRanges.length > 0 ? ipRanges.join ( ";" ) : " " ; // 5. Parameter Storeパラメータを更新 await ssmClient.send ( new PutParameterCommand ( { Name: process .env.IP_LIST_PARAMETER_NAME , Value: ipRangeParam , Overwrite: true } ) ); try { // 6. WAF IPセットを更新 // lockToken発行後に更新されているとエラーがThrowされる await wafClient.send ( new UpdateIPSetCommand ( { Name: process .env.IP_SET_NAME , Id: process .env.IP_SET_ID , Scope: process .env.IP_SET_SCOPE , Addresses: ipRanges , LockToken: lockToken , } ) ); retryCount = 0 ; } catch { // 楽観ロックで失敗したら一定回数リトライ retryCount -= 1 ; } } } } ; (Lambda) ログの抽出 ハンドラーの引数からログイベントの中身を抽出する関数は こちら を参考にして実装します。 const extractAwsLogEvents = async ( input: CloudWatchLogsEvent ) : Promise < CloudWatchLogsLogEvent [] > => { const payload = Buffer . from( input.awslogs.data , "base64" ); const result = await new Promise < string >(( resolve , reject ) => { zlib.gunzip ( payload , ( e , result ) => { return e ? reject ( e ) : resolve ( result.toString ( "ascii" )); } ); } ); return ( JSON .parse ( result ) as CloudWatchLogsDecodedData ) .logEvents ; } ; (Lambda) IPアドレス をDynamoDBテーブルに追加 ログに記録された IPアドレス をDynamoDBテーブルにアイテムとして追加する部分は次のようになります。 TTL のフォーマットは Unix エポック時間形式の秒単位 であり、現在時刻より1時間後としています。 const addDynamodbItem = async ( props: { tableName: string ; ipAddress: string } ) : Promise < void > => { // 1時間ブロックする const expires = Math .floor (new Date () .getTime () / 1000 ) + 3600 ; const command = new PutItemCommand ( { TableName: props.tableName , Item: { ip_range: { S: ` ${ props.ipAddress } /32` } , expires: { N: ` ${ expires } ` } , } , } ); await dynamodbClient.send ( command ); } ; (Lambda) DynamoDBから IPアドレス リストを取得 Parameter StoreパラメータとIPセットを更新するために、DynamoDBテーブルをスキャンし全 IPアドレス を取得する部分です。DynamoDBの TTL で有効期限が切れてもすぐにアイテムが削除される保証はなく、最大で48時間テーブルに残るため、スキャン後に フィルター で TTL が有効なアイテムをフィルタリングします。また読み取りは 強力な整合性 とし、直前で追加した IPアドレス が確実に結果に含まれるようにします。(結果整合性よりも多くのキャパシティユニットを消費することにご留意ください) const getAllIpsFromDynamodb = async ( props: { tableName: string } ) : Promise < string [] > => { let startKey: Record < string , AttributeValue > | undefined = undefined ; let shouldScanNext = true ; const ipRanges: string [] = [] ; const currentTime = Math .floor (new Date () .getTime () / 1000 ); while ( shouldScanNext ) { const params: ScanCommandInput = { TableName: props.tableName , ConsistentRead: true , ExpressionAttributeNames: { "#e" : "expires" } , ExpressionAttributeValues: { ":1" : { N: ` ${ currentTime } ` } } , FilterExpression: "#e >= :1" , ExclusiveStartKey: startKey , } ; const { Items , LastEvaluatedKey } = await dynamodbClient.send (new ScanCommand ( params )); Items?.forEach (( item ) => { if ( item [ "ip_range" ] .S ) ipRanges.push ( item [ "ip_range" ] .S ); } ); if ( LastEvaluatedKey ) { startKey = LastEvaluatedKey ; } else { shouldScanNext = false ; } } return ipRanges ; } ; これで、ブロック対象の IPアドレス をIPセットに追加するLambda関数を作成できました。 (Lambda) DynamoDBの IPアドレス リストを同期する関数 EventBridgeを受けて定期的に起動し、DynamoDBの IPアドレス リストをParameter StoreパラメータとIPセットに同期するLambda関数は、先ほどのLambda関数の 3 ~ 6 の処理を利用すれば作成できます。 3. WAF IPセットのロックトークンを取得(楽観ロック開始) 4. DynamoDBからIPアドレスリストを取得 5. Parameter Storeパラメータを更新 6. WAF IPセットを更新。失敗したら3からやり直す 以上で、 AWS WAFでブロックしたリク エス トの IPアドレス を、一定期間 IPアドレス レベルでブロックし続ける仕組みを実現できました。人手を介さずに完全に自動で動き、ブロック対象の IPアドレス は一定期間後に自動消去されるので増え続けることもなく、CDKのデプロイに対してデータが上書きされることもありません。 お読みいただいてありがとうございました! 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 セキュリティエンジニア(セキュリティ設計) 執筆: @kou.kinyo 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
スマートなタイトルが思いつきませんでした。X(クロス) イノベーション 本部 ソフトウェアデザインセンター セキュリティグループの耿です。 インターネットにWebアプリケーションを公開すると、さまざまなHTTPリク エス トを受けることになります。中にはアプリケーションを利用する目的ではない、 情報収集や 脆弱性 を突いて攻撃を狙うリク エス トも多数含まれています。攻撃への根本対策はWebアプリケーションレベルで行うべきですが、対策漏れや ゼロデイ攻撃 に対してはWAFの利用も効果的です。 AWS WAFには AWSマネージドルール が多数用意されており、簡単に導入し多層防御を実現できます。この記事では AWS WAFによるブロックをより厳しくする方法として、WAFのルールがブロックしたリク エス トの IPアドレス を、悪意のあるリク エス トを送信してくる可能性がある IPアドレス とみなして、一定期間 IPアドレス レベルでブロックし続ける仕組みをご紹介します。一度WAFのWeb ACL でブロックに成功しても、攻撃者はパターンを変えて何度も攻撃を試みる場合があり、IPレベルで一定期間ブロックすることで他のWAFルールが対応していないパターンで攻撃が成立するリスクを減らすことを期待しています。 インフラは AWS CDK で実装します。 どんな Web アプリが適しているか アーキテクチャ 1.サブスクリプションフィルターでWAFのログからLambdaを起動 2.DynamoDBのTTLで一定期間後にブロックを解除 3.CDKデプロイ用にSSM Parameter Storeを利用 4.WAF IPセットの楽観ロックで同時更新に対応 サンプルコード 事前準備 (CDK) IPアドレスリストの取得 (CDK) WAFの作成 (CDK) WAFのログを出力 (CDK) DynamoDBテーブルの作成 (CDK) ブロック対象を追加するLambda関数 (CDK) サブスクリプションフィルター (CDK) EventBridgeでIPアドレスリストを同期 (Lambda) パッケージのインポートとSDKクライアントの作成 (Lambda) ハンドラー (Lambda) ログの抽出 (Lambda) IPアドレスをDynamoDBテーブルに追加 (Lambda) DynamoDBからIPアドレスリストを取得 (Lambda) DynamoDBのIPアドレスリストを同期する関数 どんな Web アプリが適しているか 不特定多数の利用者を想定した一般公開のアプリには、これから紹介する仕組みは向かないと思います。WAFの誤検知によってリク エス トをブロックしてしまった場合の影響が大きいからです。社内用アプリなど、利用ユーザーは特定できるがインターネットからアクセスできるアプリがこの仕組みに適していると思います。 アーキテクチャ 1. サブスクリプション フィルターでWAFのログからLambdaを起動 AWS WAFで IPアドレス レンジレベルでリク エス トを許可・ブロックするためには、 IPセット を定義します。WAFのブロック結果からこのIPセットを動的に変更すれば良いので、WAFのブロックログをCloudWatch Logsに出力し、 サブスクリプションフィルター で特定のログをトリガーにLambda関数を起動します。 2.DynamoDBの TTL で一定期間後にブロックを解除 IPアドレス とユーザーの関連は変わり得るので、一定期間経過後は IPアドレス レベルのブロックを自動で解除し、ブロック対象の IPアドレス リストの長さが増え続けないようにします。誤動作で特定の IPアドレス がブロックされてしまった場合の影響を小さくします。 DynamoDBの TTL を利用します。ブロック対象となる IPアドレス をDynamoDBテーブルのレコードとして保持し、 TTL を設定します。EventBridgeで定期的にLambda関数を起動し、DynamoDBテーブルのアイテムのうち、有効期限内の IPアドレス のみでWAFのIPセットを更新します。 3.CDKデプロイ用にSSM Parameter Storeを利用 今回はCDKでインフラを構築する前提なので、IPセットもCDKコードに記載することになります。ただしIPセットの中身は、CDKデプロイ時に先祖返りしないように SSM Parameter Storeパラメータから取得 するようにします。Lambda関数でDynamoDBテーブルから有効な IPアドレス を取得した後は、このパラメータも更新するようにします。 最終的な アーキテクチャ は次の図の通りになります。ブロック対象の IPアドレス 情報はWAFのIPセット、DynamoDBテーブル、Parameter Storeパラメータと3箇所に分散していますが、DynamoDBテーブルのデータを正とし、IPセットとParameter StoreパラメータのデータはDynamoDBテーブルから結果整合的に更新されるようになっています。 4.WAF IPセットの楽観ロックで同時更新に対応 同じIPセットを更新する箇所が複数あり、また複数のWAFブロックログが同時に出力された場合はブロック対象 IPアドレス の追加が同時に実行される可能性があります。この複数の更新に対する 排他制御 の仕組みが、IPセットの API にあります。 WAF IPセットを取得する GetIPSet API のレスポンスで LockToken を取得し、IPセットを更新する UpdateIPSet API を呼びだす時のリク エス トに付加することで、楽観ロックを容易に実現できます。 WAF IPセットを更新するUpdateIPSet API は、個別の IPアドレス レンジを追加、削除するのではなく、更新時に IPアドレス レンジの全体を置き換えるような API 仕様になっているため、このような楽観ロックの仕組みが用意されているのだと思います。 サンプルコード 以上で説明した仕組みを実現するためのサンプルコードをご紹介します。 事前準備 ブロック対象の IPアドレス リストを保持するParameter Storeパラメータを手動で作成しておきます。ここでは MALICIOUS_IP_LIST というパラメータ名とします。空文字では登録できないので初期値は半角スペース 1 つとし、Lambda関数で動的に更新していきます。 1.1.1.1/32;2.2.2.2/32 のように、複数のIP CIDRを セミ コロンで区切るフォーマットを想定します。 (CDK) IPアドレス リストの取得 Parameter Storeパラメータより IPアドレス リストを取得し、文字列型の配列に変換します。それを利用してWAFのIPセットを定義します。 const ipListParamName = "MALICIOUS_IP_LIST" ; const maliciousIpSet = ssm.StringParameter.valueFromLookup ( this , ipListParamName ) .split ( ";" ) .map (( ip ) => ip.trim ()) .filter (( ip ) => ip ); const wafIpSetName = "MaliciousIpSet" ; const wafIpSet = new wafv2.CfnIPSet ( this , "MaliciousIpSet" , { name: wafIpSetName , ipAddressVersion: "IPV4" , scope: "REGIONAL" , // CloudFront用のWAFの場合は "CLOUDFRONT" addresses: maliciousIpSet , } ); またCDKデプロイする際には、 コンテキスト をリセットして常にParameter Storeから最新のパラメータを取得するように、 cdk deploy の前に cdk context --clear を実行するようにします。 cdk context --clear cdk deploy MyStack --require-approval never (CDK) WAFの作成 WAFを作成します。最初に評価されるルールに IpConstraintStatement を作成し、先ほど作成した IPセットをブロックします。他のルールは AWSManagedRulesCommonRuleSet など、自由に追加します。 WAFのALBやCloudFrontなどへの関連付けは省略します。 const wafWebAcl = new wafv2.CfnWebACL ( this , "WafV2WebAcl" , { defaultAction: { allow: {} } , scope: "REGIONAL" , // CloudFront用のWAFの場合は "CLOUDFRONT" visibilityConfig: { cloudWatchMetricsEnabled: true , sampledRequestsEnabled: true , metricName: "WafV2WebAcl" , } , rules: [ { name: "IpConstraintStatement" , priority: 10 , statement: { ipSetReferenceStatement: { arn: wafIpSet.attrArn , } , } , action: { block: {} } , visibilityConfig: { cloudWatchMetricsEnabled: true , sampledRequestsEnabled: true , metricName: "IpConstraintStatement" , } , } , { name: "AWSManagedRulesCommonRuleSet" , priority: 20 , statement: { managedRuleGroupStatement: { vendorName: "AWS" , name: "AWSManagedRulesCommonRuleSet" , } , } , overrideAction: { none: {} } , visibilityConfig: { cloudWatchMetricsEnabled: true , sampledRequestsEnabled: true , metricName: "AWSManagedRulesCommonRuleSet" , } , } , // 他のルールは省略 ] , } ); (CDK) WAFのログを出力 WAFのBLOCKログとCOUNTログをCloudWatch Logsに出力します。 const wafLogGroup = new logs.LogGroup ( this , "WafLogGroup" , { logGroupName: "waf-logs" , } ); const logConfig = new wafv2.CfnLoggingConfiguration ( this , "WafV2LoggingConfiguration" , { logDestinationConfigs: [ `arn:aws:logs: ${ region } : ${ accountId } :log-group: ${ wafLogGroup.logGroupName } ` ] , resourceArn: wafWebAcl.attrArn , loggingFilter: { DefaultBehavior: "DROP" , Filters: [ { Behavior: "KEEP" , Conditions: [{ ActionCondition: { Action: "BLOCK" } } , { ActionCondition: { Action: "COUNT" } }] , Requirement: "MEETS_ANY" , } , ] , } , } ); logConfig.addDependsOn ( wafWebAcl ); (CDK) DynamoDBテーブルの作成 ブロック対象 IPアドレス 用のDynamoDBテーブルを作成します。 パーティション キーはIP CIDRで、 ip_range というキー名にします。 TTL は expires という名前の属性にしています。 const ipTable = new dynamodb.Table ( this , "MaliciousIpTable" , { tableName: "malicious-ip" , partitionKey: { name: "ip_range" , type : dynamodb.AttributeType.STRING } , billingMode: dynamodb.BillingMode.PAY_PER_REQUEST , timeToLiveAttribute: "expires" , } ); (CDK) ブロック対象を追加するLambda関数 ブロック対象の IPアドレス を追加するLambda関数を作成します。関数内で利用するための 環境変数 をいくつか設定し、必要な API コールをするための権限を渡します。 const autoBlockMaliciousIpFunction = new lambdaNodejs.NodejsFunction ( this , "AutoBlockMaliciousIpFunction" , { entry: "functions/auto-block-malicious-ip.ts" , runtime: Runtime.NODEJS_18_X , timeout: Duration.minutes ( 3 ), environment: { TABLE_NAME: ipTable.tableName , // DynamoDBテーブル名 IP_LIST_PARAMETER_NAME: ipListParamName , // Parameter Storeパラメータ名 IP_SET_NAME: wafIpSetName , // IPセット名 IP_SET_ID: wafIpSet.attrId , // IPセットID IP_SET_SCOPE: "REGIONAL" , // IPセットスコープ。CloudFront用のWAFの場合は "CLOUDFRONT" } , } ); // DynamoDBテーブルの参照・更新権限 ipTable.grantReadWriteData ( autoBlockMaliciousIpFunction ); // IPセットの参照・更新権限 autoBlockMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ wafIpSet.attrArn ] , actions: [ "wafv2:GetIpSet" , "wafv2:UpdateIPSet" ] , effect: iam.Effect.ALLOW , } ) ); // パラメータの更新権限 autoBlockMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ `arn:aws:ssm: ${ region } : ${ accountId } :parameter/ ${ ipListParamName } ` ] , actions: [ "ssm:PutParameter" ] , effect: iam.Effect.ALLOW , } ) ); (CDK) サブスクリプション フィルター ブロックログに対して サブスクリプション フィルターでLambda関数を起動させます。ただし IpConstraintStatement ルール自体でブロックされた場合はLambda関数を起動しないようにします。他にも地理的一致条件や、他の IPアドレス 条件でブロックされた場合、対象の IPアドレス はどちらにせよブロックされるので、Lambda関数の起動条件から除外しても良いでしょう。さらに会社の IPアドレス の場合はブロック対象外とするなど、フィルターパターンは要件に応じていろいろとカスタマイズできるでしょう。 const filter = wafLogGroup.addSubscriptionFilter ( "WafLogFilter" , { destination: new destinations.LambdaDestination ( autoBlockMaliciousIpFunction ), filterPattern: logs.FilterPattern.literal ( '{ $.action = "BLOCK" && $.terminatingRuleId != "IpConstraintStatement" }' ), } ); // https://github.com/aws/aws-cdk/issues/23177 へのワークアラウンド // これがないと初回デプロイで失敗する ( filter.node.defaultChild as logs.CfnSubscriptionFilter ) .addDependency ( filter.node.findChild ( "CanInvokeLambda" ) as CfnPermission ); (CDK) EventBridgeで IPアドレス リストを同期 定期的にEventBridgeで起動し、DynamoDBの IPアドレス リストをParameter StoreパラメータとIPセットに同期するLambda関数を作成します。EventBridgeルールは、ここでは1時間に1度起動するようにしています。 const autoUpdateMaliciousIpFunction = new lambdaNodejs.NodejsFunction ( this , "AutoUpdateMaliciousIpFunction" , { entry: "functions/auto-update-malicious-ip.ts" , runtime: Runtime.NODEJS_18_X , timeout: Duration.minutes ( 1 ), environment: { TABLE_NAME: ipTable.tableName , IP_LIST_PARAMETER_NAME: ipListParamName , IP_SET_NAME: wafIpSetName , IP_SET_ID: wafIpSet.attrId , IP_SET_SCOPE: "REGIONAL" , } , } ); // DynamoDBテーブルの参照権限のみ ipTable.grantReadData ( autoUpdateMaliciousIpFunction ); autoUpdateMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ wafIpSet.attrArn ] , actions: [ "wafv2:GetIpSet" , "wafv2:UpdateIPSet" ] , effect: iam.Effect.ALLOW , } ) ); autoUpdateMaliciousIpFunction.addToRolePolicy ( new iam.PolicyStatement ( { resources: [ `arn:aws:ssm: ${ region } : ${ accountId } :parameter/ ${ ipListParamName } ` ] , actions: [ "ssm:PutParameter" ] , effect: iam.Effect.ALLOW , } ) ); new events.Rule ( this , "AutoUpdateWafIpListEventRule" , { schedule: events.Schedule.cron ( { minute: "0" } ), targets: [ new eventTargets.LambdaFunction ( autoUpdateMaliciousIpFunction ) ] , } ); (Lambda) パッケージのインポートと SDK クライアントの作成 ここからはブロック対象の IPアドレス を追加するLambda関数のコードを紹介します。まず必要なパッケージのインポートと、使用する各サービスの SDK クライアントを作成する部分です。 // functions/auto-block-malicious-ip.ts import * as zlib from "zlib"; import { DynamoDBClient, ScanCommand, ScanCommandInput, AttributeValue, PutItemCommand, } from "@aws-sdk/client-dynamodb"; import { SSMClient, PutParameterCommand } from "@aws-sdk/client-ssm"; import { WAFV2Client, UpdateIPSetCommand, GetIPSetCommand } from "@aws-sdk/client-wafv2"; import { Handler, CloudWatchLogsEvent, CloudWatchLogsLogEvent, CloudWatchLogsDecodedData } from "aws-lambda"; const dynamodbClient = new DynamoDBClient({ region: process.env.AWS_REGION }); const ssmClient = new SSMClient({ region: process.env.AWS_REGION }); const wafClient = new WAFV2Client({ region: process.env.AWS_REGION }); (Lambda) ハンドラー サブスクリプション フィルターを受けて起動する関数なので、引数のイベントは CloudWatchLogsEvent 型になります。全体の処理の流れはこの通りです。 1. ログイベントを抽出 2. IPアドレスをDynamoDBテーブルに追加 3. WAF IPセットのロックトークンを取得(楽観ロック開始) 4. DynamoDBからIPアドレスリストを取得 5. Parameter Storeパラメータを更新 6. WAF IPセットを更新。失敗したら3からやり直す export const handler: Handler = async ( input: CloudWatchLogsEvent ) => { if ( ! process .env.IP_SET_ID || ! process .env.IP_SET_NAME || ! process .env.TABLE_NAME || ! process .env.IP_LIST_PARAMETER_NAME || ! process .env.IP_SET_SCOPE || ! [ "REGIONAL" , "CLOUDFRONT" ] .includes ( process .env.IP_SET_SCOPE ) ) { return; } // 1. ログイベントを抽出(関数の中身は後述) const logEvents = await extractAwsLogEvents ( input ); // 各イベントに対してループ処理 for ( const event of logEvents ) { type LogMessageType = { httpRequest?: { clientIp?: string ; } ; } ; const message = JSON .parse ( event.message ) as LogMessageType ; if ( ! message.httpRequest || ! message.httpRequest.clientIp ) continue; // 2. IPアドレスをDynamoDBテーブルに追加(関数の中身は後述) await addDynamodbItem ( { tableName: process .env.TABLE_NAME , ipAddress: message.httpRequest.clientIp } ); let retryCount = 10 ; while ( retryCount > 0 ) { // 3. WAF IPセットのロックトークンを取得(楽観ロック開始) const { LockToken: lockToken } = await wafClient.send ( new GetIPSetCommand ( { Name: process .env.IP_SET_NAME , Scope: process .env.IP_SET_SCOPE , Id: process .env.IP_SET_ID , } ) ); // 4. DynamoDBからIPアドレスリストを取得(関数の中身は後述) const ipRanges: string [] = await getAllIpsFromDynamodb ( { tableName: process .env.TABLE_NAME } ); const ipRangeParam = ipRanges.length > 0 ? ipRanges.join ( ";" ) : " " ; // 5. Parameter Storeパラメータを更新 await ssmClient.send ( new PutParameterCommand ( { Name: process .env.IP_LIST_PARAMETER_NAME , Value: ipRangeParam , Overwrite: true } ) ); try { // 6. WAF IPセットを更新 // lockToken発行後に更新されているとエラーがThrowされる await wafClient.send ( new UpdateIPSetCommand ( { Name: process .env.IP_SET_NAME , Id: process .env.IP_SET_ID , Scope: process .env.IP_SET_SCOPE , Addresses: ipRanges , LockToken: lockToken , } ) ); retryCount = 0 ; } catch { // 楽観ロックで失敗したら一定回数リトライ retryCount -= 1 ; } } } } ; (Lambda) ログの抽出 ハンドラーの引数からログイベントの中身を抽出する関数は こちら を参考にして実装します。 const extractAwsLogEvents = async ( input: CloudWatchLogsEvent ) : Promise < CloudWatchLogsLogEvent [] > => { const payload = Buffer . from( input.awslogs.data , "base64" ); const result = await new Promise < string >(( resolve , reject ) => { zlib.gunzip ( payload , ( e , result ) => { return e ? reject ( e ) : resolve ( result.toString ( "ascii" )); } ); } ); return ( JSON .parse ( result ) as CloudWatchLogsDecodedData ) .logEvents ; } ; (Lambda) IPアドレス をDynamoDBテーブルに追加 ログに記録された IPアドレス をDynamoDBテーブルにアイテムとして追加する部分は次のようになります。 TTL のフォーマットは Unix エポック時間形式の秒単位 であり、現在時刻より1時間後としています。 const addDynamodbItem = async ( props: { tableName: string ; ipAddress: string } ) : Promise < void > => { // 1時間ブロックする const expires = Math .floor (new Date () .getTime () / 1000 ) + 3600 ; const command = new PutItemCommand ( { TableName: props.tableName , Item: { ip_range: { S: ` ${ props.ipAddress } /32` } , expires: { N: ` ${ expires } ` } , } , } ); await dynamodbClient.send ( command ); } ; (Lambda) DynamoDBから IPアドレス リストを取得 Parameter StoreパラメータとIPセットを更新するために、DynamoDBテーブルをスキャンし全 IPアドレス を取得する部分です。DynamoDBの TTL で有効期限が切れてもすぐにアイテムが削除される保証はなく、最大で48時間テーブルに残るため、スキャン後に フィルター で TTL が有効なアイテムをフィルタリングします。また読み取りは 強力な整合性 とし、直前で追加した IPアドレス が確実に結果に含まれるようにします。(結果整合性よりも多くのキャパシティユニットを消費することにご留意ください) const getAllIpsFromDynamodb = async ( props: { tableName: string } ) : Promise < string [] > => { let startKey: Record < string , AttributeValue > | undefined = undefined ; let shouldScanNext = true ; const ipRanges: string [] = [] ; const currentTime = Math .floor (new Date () .getTime () / 1000 ); while ( shouldScanNext ) { const params: ScanCommandInput = { TableName: props.tableName , ConsistentRead: true , ExpressionAttributeNames: { "#e" : "expires" } , ExpressionAttributeValues: { ":1" : { N: ` ${ currentTime } ` } } , FilterExpression: "#e >= :1" , ExclusiveStartKey: startKey , } ; const { Items , LastEvaluatedKey } = await dynamodbClient.send (new ScanCommand ( params )); Items?.forEach (( item ) => { if ( item [ "ip_range" ] .S ) ipRanges.push ( item [ "ip_range" ] .S ); } ); if ( LastEvaluatedKey ) { startKey = LastEvaluatedKey ; } else { shouldScanNext = false ; } } return ipRanges ; } ; これで、ブロック対象の IPアドレス をIPセットに追加するLambda関数を作成できました。 (Lambda) DynamoDBの IPアドレス リストを同期する関数 EventBridgeを受けて定期的に起動し、DynamoDBの IPアドレス リストをParameter StoreパラメータとIPセットに同期するLambda関数は、先ほどのLambda関数の 3 ~ 6 の処理を利用すれば作成できます。 3. WAF IPセットのロックトークンを取得(楽観ロック開始) 4. DynamoDBからIPアドレスリストを取得 5. Parameter Storeパラメータを更新 6. WAF IPセットを更新。失敗したら3からやり直す 以上で、 AWS WAFでブロックしたリク エス トの IPアドレス を、一定期間 IPアドレス レベルでブロックし続ける仕組みを実現できました。人手を介さずに完全に自動で動き、ブロック対象の IPアドレス は一定期間後に自動消去されるので増え続けることもなく、CDKのデプロイに対してデータが上書きされることもありません。 お読みいただいてありがとうございました! 私たちは同じチームで働いてくれる仲間を大募集しています!たくさんのご応募をお待ちしています。 セキュリティエンジニア(セキュリティ設計) 執筆: @kou.kinyo 、レビュー: 寺山 輝 (@terayama.akira) ( Shodo で執筆されました )
こんにちは、金融ソリューション事業部の若本です。 先日ChatGPT(gpt-3.5-turbo)の API が公開されるとともに、Open AIのサービスが使いやすくなりました。 今回は、ChatGPTから返ってきたレスポンスを読み上げる簡易アプリケーションの作成を行います。 使用するもの 処理概要 1. APIの作成 1.1 Open-AI API(ChatGPT)を介して、質問の答えを取得 1.2 Espnetを用いて、合成音声を生成 2. APIを呼び出すアプリの作成 おわりに 使用するもの OpenAI API 事前に API キーを発行しておく必要があります。また、1000 トーク ン(約700文字程度)単位で費用が発生します。ユーザー登録後、一定まで無料で使えるフリークレジットもあるので、お気軽にお試しいただけます。 ESPNet 有志の方が作成されている音声処理ツールキットです。 音声認識 から 音声合成 まで、幅広い音声タスク/手法が実装されています。 今回は、 音声合成 を使用します。使用する 音声合成 モデルは、 つくよみちゃんコーパスを用いた学習済みモデル です。 fastAPI (任意) python で構築できる API としてfastAPIを使用します。今回は、会話と 音声合成 の処理を API 化します。 Streamlit (任意) python で簡単にアプリを作成できる フレームワーク です。 Docker(任意) 筆者は上記fastAPIとStreamlitの環境を、別々のDocker containerとして構築しました。 処理概要 下記の処理を実装します。 API を用いてAIの推論結果を返せるようにする 質問(Text)を投げ、ChatGPTの返答(Text)を受け取る 文章(Text)を投げ、 音声合成 の結果(Audio)を受け取る 上記の API に処理を依頼する簡易アプリ 今回は 音声合成 の課題である『 音声合成 に時間がかかる問題』に対処します。 音声合成 は、そのモデルの仕組み上、基本的に ストリーミングのようにデータを受け取ることができません 。 そのため、ChatGPTのレスポンスをそのまま 音声合成 してしまうと、数十秒~数分間待たされる可能性があります。 それでは全く使い物にならないため、今回は python の マルチスレッド処理を用いて 音声合成 を並行処理で行います 。 別の 音声合成 の課題として「長文の 音声合成 が安定しない」というのもあり、上記のアプローチはその面でも効果的です。 ただし、最終的な合成の品質をなるべく落とさないために どの区切りで 音声合成 を実行するか を判定する必要があります。 1. API の作成 まず、処理の要となる API を作成します。 今回は保守性向上のため API としているだけですので、必ずしも API にする必要はありません。 fastAPI/Streamlitの基本的な使用方法、およびESPNetの環境構築については割愛します。 1.1 Open-AI API (ChatGPT)を介して、質問の答えを取得 API のRouterにChatGPTを呼び出す関数を記述します。 OpenAIのブログ を参考に、requestライブラリで実施しました。執筆時点(2023/03/07)の情報ですので、適宜request_bodyは変更ください。 他にも OpenAIライブラリで実装する方法 もあります。 from fastapi import APIRouter import requests import json TEMPLATE_PATH = "./data/template.txt" SYSTEM_TEMPLATE_PATH = "./data/system_template.txt" API_KEY = 'YOUR_OPEN_API_KEY' router = APIRouter() template = load_template(TEMPLATE_PATH) system_prompt = load_template(SYSTEM_TEMPLATE_PATH) def load_template(path:str): with open(path, 'r') as f: template = f.read() return template def edit_prompt(template:str, text:str, token="{query}"): prompt = template.replace(token, text) return prompt def prompt_response(prompt:str, system_prompt:str): headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + API_KEY, } data = { "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": prompt}, {"role": "system", "content": system_prompt}], "max_tokens": 200, "temperature": 1, "top_p": 1, } response = requests.post('https://api.openai.com/v1/chat/completions', headers=headers, data=json.dumps(data)) return response.json() def foreprocessing(res): text = res['choices'][0]['message']['content'] return text @router.get("/chat") def get_chat_response(text:str): prompt = edit_prompt(template, text) res = prompt_response(prompt, system_prompt) res_text = foreprocessing(res) return res_text また、この時ChatGPTにクエリを送るためのテンプレートを用意しておきます。 template.txt {query} system_template.txt 文章に対して簡潔に回答してください。 あなたはアシスタントで、ユーザーと会話をしています。 template.txtの内容は "role":"user" に、system_template.txtの内容は "role":"system" にそれぞれ入力として設定されます。今回は "role":"user" にユーザーの入力を、 "role":"system" にChatGPTの振る舞いの指定を実施しました。 {query}は ユーザーの入力で置き換えられます。 今回は 音声合成 時に、あまり長くなく、かつ口語的に返してもらうために振る舞いを指定しておきます。 1.2 Espnetを用いて、合成音声を生成 下記のコードだけで 音声合成 が実行できます。非常にお手軽です。 今回使用しているモデルは VITS (Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech) です。 model_tag で使用するmodelを指定することにより、モデルを自動でダウンロードできます。 代わりの引数として、 model_file に自分で作成したモデルのpathを指定することも可能です(動作確認済み)。 音声合成 モデルを最初から学習するためのレシピもEspnetには整備されていますので、興味のある方はぜひ。 from fastapi import APIRouter from fastapi.responses import StreamingResponse from espnet2.bin.tts_inference import Text2Speech import torch import soundfile as sf import uuid import os router = APIRouter() fs, lang = 44100, "Japanese" text2speech = Text2Speech.from_pretrained( model_tag="kan-bayashi/tsukuyomi_full_band_vits_prosody", device="cpu", # or "cuda" speed_control_alpha=1.0, noise_scale=0.333, noise_scale_dur=0.333, ) def TTS_streamer(text: str): with torch.no_grad(): wav = text2speech(text)["wav"] filename = str(uuid.uuid4()) sf.write(f"{filename}.wav", wav.view(-1).cpu().numpy(), text2speech.fs) with open(f"{filename}.wav", mode="rb") as wav_file: yield from wav_file os.remove(f"{filename}.wav") @router.get("/tts") async def tts_streamer(text: str): return StreamingResponse(TTS_streamer(text), media_type="audio/wav") 上記で API サーバーを起動します。FastAPIにはSwaggerUIも用意されているため、画面上で確認することができます。 API として作成した機能の動作が確認できた後、次のステップに進みます。 2. API を呼び出すアプリの作成 次に、ユーザーの入力と API 処理を繋ぐ簡易アプリを実装します。 ここでは、下記のような処理としました。 ChatGPTのレスポンスを、句読点や改行で区切る ThreadPoolExecutorを用いて API へのリク エス トを並行処理 Threadの実行を順番に待機(+実行時間のスケジュール設定) 音声の再生に必要な秒数分、以降の実行を遅らせる もし音声が遅く届いた場合、遅れた時間だけ以降の実行を遅らせる Chat-GPTの文章を区切り、並行処理で 音声合成 を実行していきます。 Chat-GPTのレスポンスは1文が長い、かつ改行が出現する可能性があるため、区切り文字は句読点と改行としました。 今回は単一 API を同じリソース上で呼び出しているため、並行処理にするメリットは薄いかもしれませんが、実用上は必要な処理になります。 import streamlit as st import requests import base64 import time import re import datetime from concurrent.futures import ThreadPoolExecutor # 音声合成の最小文字数。小さすぎると安定しない場合があります。 SPLIT_THRESHOLD = 4 # 息継ぎの秒数(s) TIME_BUFFER = 0.1 # 待機中の実行スパン(s) SLEEP_ITER = 0.2 # APIのそれぞれのURL CHATBOT_ENDPOINT = 'http://chatbot-backend:8000/chat' TTS_ENDPOINT = 'http://chatbot-backend:8000/tts' def split_text(text:str): text_list = re.split('[\n、。]+', text) text_list_ = [] for text in text_list: if text == '': continue if len(text) < SPLIT_THRESHOLD: try: text_list_[-1] = text_list_[-1] + '。' + text except IndexError: text_list_.append(text) else: text_list_.append(text) if len(text_list[0]) < SPLIT_THRESHOLD and len(text_list_) > 1: text_list_[1] = text_list[0] + '。' + text_list_[1] text_list_ = text_list_[1:] return text_list_ def get_tts_sound(text:str, url=TTS_ENDPOINT): params = {'text': text} response = requests.get(url, params=params) return response.content, datetime.datetime.now() def sound_player(response_content:str): # 参考:https://qiita.com/kunishou/items/a0a1a26449293634b7a0 audio_placeholder = st.empty() audio_str = "data:audio/ogg;base64,%s"%(base64.b64encode(response_content).decode()) audio_html = """ <audio autoplay=True> <source src="%s" type="audio/ogg" autoplay=True> Your browser does not support the audio element. </audio> """ %audio_str audio_placeholder.empty() time.sleep(0.5) audio_placeholder.markdown(audio_html, unsafe_allow_html=True) def get_chat_response(text:str, url=CHATBOT_ENDPOINT): params = {'text': text} response = requests.get(url, params=params) return response.text if __name__ == "__main__": st.set_page_config(layout="wide") query = st.text_input('質問を入力してください') button = st.button('実行') if button: # チャットボットの返信を取得 response_text = get_chat_response(query) st.write(f'回答:{response_text}') # 返信を分割 split_response = split_text(response_text) executor = ThreadPoolExecutor(max_workers=2) futures = [] # 並行処理として音声合成へ for sq_text in split_response: future = executor.submit(get_tts_sound, sq_text) futures.append(future) block_time_list = [datetime.timedelta() for i in range(len(futures))] current_time = datetime.datetime.now() # 結果をwaitし、再生可能時間になり次第再生する res_index = 0 gap_time = datetime.timedelta() while res_index < len(futures): future = futures[res_index] if future.done(): if res_index==0: base_time = datetime.datetime.now() if datetime.datetime.now() > base_time + block_time_list[res_index]: for i in range(len(block_time_list)): if i > res_index: # 音声長を計算。音声は32bitの16000Hz、base64エンコードの結果は1文字6bitの情報であるため、下記の計算で算出できます block_time_list[i] += datetime.timedelta(seconds=(len(future.result()[0])*6/32/16000)+gap_time.total_seconds()+TIME_BUFFER) st.write(f' 実行完了:{split_response[res_index]}') st.write(f' 実行時間:{(future.result()[1] - current_time).total_seconds():.3f}s') st.write(f' 音声の長さ:{len(future.result()[0])*6/32/16000:.3f}s') sound_player(future.result()[0]) res_index += 1 gap_time = datetime.timedelta() elif res_index!=0: gap_time += datetime.timedelta(seconds=SLEEP_ITER) time.sleep(SLEEP_ITER) executor.shutdown() 早速結果を見てみましょう。 リアルタイムとはいきませんでしたが、概ね許容範囲です。 なお、こちらは筆者の GPU 非搭載ノートPCで実行しています。今回は簡易的な検証でしたので、モデルの圧縮、コードの簡素化、高スペックサーバーの使用など、高速化できる余地は多々あります。 並行実行することにより、繋ぎ目となる文章の箇所に違和感が残る懸念もありましたが、あまり違和感は感じられませんでした。 返ってくる結果が遅い場合には、 フィラー (「えーと」「その」など)を入れる判定を噛ませることで自然になりそうです。 おわりに ChatGPTとESPNetを用いて、会話の返答を音声で出力する簡易アプリケーションを作成しました。 OpenAIは、ChatGPTの他にもWhisperという 音声認識 モデルを API として公開していますので、連結することで音声だけで会話することも可能です。 映画アイアンマンのJ.A.R.V.I.S.に憧れてAIの勉強を始めたので、夢見た未来がだいぶ近づいてきたなと感じます。このようなAIが親しみやすい形で生活に浸透していくのか、今後も注視していきます。 (追記:記事公開前にGPT-4がローンチされました。AI領域の進歩の加速を感じる昨今です。) それでは最後に。 執筆: @wakamoto.ryosuke 、レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
こんにちは、金融ソリューション事業部の若本です。 先日ChatGPT(gpt-3.5-turbo)の API が公開されるとともに、Open AIのサービスが使いやすくなりました。 今回は、ChatGPTから返ってきたレスポンスを読み上げる簡易アプリケーションの作成を行います。 使用するもの 処理概要 1. APIの作成 1.1 Open-AI API(ChatGPT)を介して、質問の答えを取得 1.2 Espnetを用いて、合成音声を生成 2. APIを呼び出すアプリの作成 おわりに 使用するもの OpenAI API 事前に API キーを発行しておく必要があります。また、1000 トーク ン(約700文字程度)単位で費用が発生します。ユーザー登録後、一定まで無料で使えるフリークレジットもあるので、お気軽にお試しいただけます。 ESPNet 有志の方が作成されている音声処理ツールキットです。 音声認識 から 音声合成 まで、幅広い音声タスク/手法が実装されています。 今回は、 音声合成 を使用します。使用する 音声合成 モデルは、 つくよみちゃんコーパスを用いた学習済みモデル です。 fastAPI (任意) python で構築できる API としてfastAPIを使用します。今回は、会話と 音声合成 の処理を API 化します。 Streamlit (任意) python で簡単にアプリを作成できる フレームワーク です。 Docker(任意) 筆者は上記fastAPIとStreamlitの環境を、別々のDocker containerとして構築しました。 処理概要 下記の処理を実装します。 API を用いてAIの推論結果を返せるようにする 質問(Text)を投げ、ChatGPTの返答(Text)を受け取る 文章(Text)を投げ、 音声合成 の結果(Audio)を受け取る 上記の API に処理を依頼する簡易アプリ 今回は 音声合成 の課題である『 音声合成 に時間がかかる問題』に対処します。 音声合成 は、そのモデルの仕組み上、基本的に ストリーミングのようにデータを受け取ることができません 。 そのため、ChatGPTのレスポンスをそのまま 音声合成 してしまうと、数十秒~数分間待たされる可能性があります。 それでは全く使い物にならないため、今回は python の マルチスレッド処理を用いて 音声合成 を並行処理で行います 。 別の 音声合成 の課題として「長文の 音声合成 が安定しない」というのもあり、上記のアプローチはその面でも効果的です。 ただし、最終的な合成の品質をなるべく落とさないために どの区切りで 音声合成 を実行するか を判定する必要があります。 1. API の作成 まず、処理の要となる API を作成します。 今回は保守性向上のため API としているだけですので、必ずしも API にする必要はありません。 fastAPI/Streamlitの基本的な使用方法、およびESPNetの環境構築については割愛します。 1.1 Open-AI API (ChatGPT)を介して、質問の答えを取得 API のRouterにChatGPTを呼び出す関数を記述します。 OpenAIのブログ を参考に、requestライブラリで実施しました。執筆時点(2023/03/07)の情報ですので、適宜request_bodyは変更ください。 他にも OpenAIライブラリで実装する方法 もあります。 from fastapi import APIRouter import requests import json TEMPLATE_PATH = "./data/template.txt" SYSTEM_TEMPLATE_PATH = "./data/system_template.txt" API_KEY = 'YOUR_OPEN_API_KEY' router = APIRouter() template = load_template(TEMPLATE_PATH) system_prompt = load_template(SYSTEM_TEMPLATE_PATH) def load_template(path:str): with open(path, 'r') as f: template = f.read() return template def edit_prompt(template:str, text:str, token="{query}"): prompt = template.replace(token, text) return prompt def prompt_response(prompt:str, system_prompt:str): headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + API_KEY, } data = { "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": prompt}, {"role": "system", "content": system_prompt}], "max_tokens": 200, "temperature": 1, "top_p": 1, } response = requests.post('https://api.openai.com/v1/chat/completions', headers=headers, data=json.dumps(data)) return response.json() def foreprocessing(res): text = res['choices'][0]['message']['content'] return text @router.get("/chat") def get_chat_response(text:str): prompt = edit_prompt(template, text) res = prompt_response(prompt, system_prompt) res_text = foreprocessing(res) return res_text また、この時ChatGPTにクエリを送るためのテンプレートを用意しておきます。 template.txt {query} system_template.txt 文章に対して簡潔に回答してください。 あなたはアシスタントで、ユーザーと会話をしています。 template.txtの内容は "role":"user" に、system_template.txtの内容は "role":"system" にそれぞれ入力として設定されます。今回は "role":"user" にユーザーの入力を、 "role":"system" にChatGPTの振る舞いの指定を実施しました。 {query}は ユーザーの入力で置き換えられます。 今回は 音声合成 時に、あまり長くなく、かつ口語的に返してもらうために振る舞いを指定しておきます。 1.2 Espnetを用いて、合成音声を生成 下記のコードだけで 音声合成 が実行できます。非常にお手軽です。 今回使用しているモデルは VITS (Conditional Variational Autoencoder with Adversarial Learning for End-to-End Text-to-Speech) です。 model_tag で使用するmodelを指定することにより、モデルを自動でダウンロードできます。 代わりの引数として、 model_file に自分で作成したモデルのpathを指定することも可能です(動作確認済み)。 音声合成 モデルを最初から学習するためのレシピもEspnetには整備されていますので、興味のある方はぜひ。 from fastapi import APIRouter from fastapi.responses import StreamingResponse from espnet2.bin.tts_inference import Text2Speech import torch import soundfile as sf import uuid import os router = APIRouter() fs, lang = 44100, "Japanese" text2speech = Text2Speech.from_pretrained( model_tag="kan-bayashi/tsukuyomi_full_band_vits_prosody", device="cpu", # or "cuda" speed_control_alpha=1.0, noise_scale=0.333, noise_scale_dur=0.333, ) def TTS_streamer(text: str): with torch.no_grad(): wav = text2speech(text)["wav"] filename = str(uuid.uuid4()) sf.write(f"{filename}.wav", wav.view(-1).cpu().numpy(), text2speech.fs) with open(f"{filename}.wav", mode="rb") as wav_file: yield from wav_file os.remove(f"{filename}.wav") @router.get("/tts") async def tts_streamer(text: str): return StreamingResponse(TTS_streamer(text), media_type="audio/wav") 上記で API サーバーを起動します。FastAPIにはSwaggerUIも用意されているため、画面上で確認することができます。 API として作成した機能の動作が確認できた後、次のステップに進みます。 2. API を呼び出すアプリの作成 次に、ユーザーの入力と API 処理を繋ぐ簡易アプリを実装します。 ここでは、下記のような処理としました。 ChatGPTのレスポンスを、句読点や改行で区切る ThreadPoolExecutorを用いて API へのリク エス トを並行処理 Threadの実行を順番に待機(+実行時間のスケジュール設定) 音声の再生に必要な秒数分、以降の実行を遅らせる もし音声が遅く届いた場合、遅れた時間だけ以降の実行を遅らせる Chat-GPTの文章を区切り、並行処理で 音声合成 を実行していきます。 Chat-GPTのレスポンスは1文が長い、かつ改行が出現する可能性があるため、区切り文字は句読点と改行としました。 今回は単一 API を同じリソース上で呼び出しているため、並行処理にするメリットは薄いかもしれませんが、実用上は必要な処理になります。 import streamlit as st import requests import base64 import time import re import datetime from concurrent.futures import ThreadPoolExecutor # 音声合成の最小文字数。小さすぎると安定しない場合があります。 SPLIT_THRESHOLD = 4 # 息継ぎの秒数(s) TIME_BUFFER = 0.1 # 待機中の実行スパン(s) SLEEP_ITER = 0.2 # APIのそれぞれのURL CHATBOT_ENDPOINT = 'http://chatbot-backend:8000/chat' TTS_ENDPOINT = 'http://chatbot-backend:8000/tts' def split_text(text:str): text_list = re.split('[\n、。]+', text) text_list_ = [] for text in text_list: if text == '': continue if len(text) < SPLIT_THRESHOLD: try: text_list_[-1] = text_list_[-1] + '。' + text except IndexError: text_list_.append(text) else: text_list_.append(text) if len(text_list[0]) < SPLIT_THRESHOLD and len(text_list_) > 1: text_list_[1] = text_list[0] + '。' + text_list_[1] text_list_ = text_list_[1:] return text_list_ def get_tts_sound(text:str, url=TTS_ENDPOINT): params = {'text': text} response = requests.get(url, params=params) return response.content, datetime.datetime.now() def sound_player(response_content:str): # 参考:https://qiita.com/kunishou/items/a0a1a26449293634b7a0 audio_placeholder = st.empty() audio_str = "data:audio/ogg;base64,%s"%(base64.b64encode(response_content).decode()) audio_html = """ <audio autoplay=True> <source src="%s" type="audio/ogg" autoplay=True> Your browser does not support the audio element. </audio> """ %audio_str audio_placeholder.empty() time.sleep(0.5) audio_placeholder.markdown(audio_html, unsafe_allow_html=True) def get_chat_response(text:str, url=CHATBOT_ENDPOINT): params = {'text': text} response = requests.get(url, params=params) return response.text if __name__ == "__main__": st.set_page_config(layout="wide") query = st.text_input('質問を入力してください') button = st.button('実行') if button: # チャットボットの返信を取得 response_text = get_chat_response(query) st.write(f'回答:{response_text}') # 返信を分割 split_response = split_text(response_text) executor = ThreadPoolExecutor(max_workers=2) futures = [] # 並行処理として音声合成へ for sq_text in split_response: future = executor.submit(get_tts_sound, sq_text) futures.append(future) block_time_list = [datetime.timedelta() for i in range(len(futures))] current_time = datetime.datetime.now() # 結果をwaitし、再生可能時間になり次第再生する res_index = 0 gap_time = datetime.timedelta() while res_index < len(futures): future = futures[res_index] if future.done(): if res_index==0: base_time = datetime.datetime.now() if datetime.datetime.now() > base_time + block_time_list[res_index]: for i in range(len(block_time_list)): if i > res_index: # 音声長を計算。音声は32bitの16000Hz、base64エンコードの結果は1文字6bitの情報であるため、下記の計算で算出できます block_time_list[i] += datetime.timedelta(seconds=(len(future.result()[0])*6/32/16000)+gap_time.total_seconds()+TIME_BUFFER) st.write(f' 実行完了:{split_response[res_index]}') st.write(f' 実行時間:{(future.result()[1] - current_time).total_seconds():.3f}s') st.write(f' 音声の長さ:{len(future.result()[0])*6/32/16000:.3f}s') sound_player(future.result()[0]) res_index += 1 gap_time = datetime.timedelta() elif res_index!=0: gap_time += datetime.timedelta(seconds=SLEEP_ITER) time.sleep(SLEEP_ITER) executor.shutdown() 早速結果を見てみましょう。 リアルタイムとはいきませんでしたが、概ね許容範囲です。 なお、こちらは筆者の GPU 非搭載ノートPCで実行しています。今回は簡易的な検証でしたので、モデルの圧縮、コードの簡素化、高スペックサーバーの使用など、高速化できる余地は多々あります。 並行実行することにより、繋ぎ目となる文章の箇所に違和感が残る懸念もありましたが、あまり違和感は感じられませんでした。 返ってくる結果が遅い場合には、 フィラー (「えーと」「その」など)を入れる判定を噛ませることで自然になりそうです。 おわりに ChatGPTとESPNetを用いて、会話の返答を音声で出力する簡易アプリケーションを作成しました。 OpenAIは、ChatGPTの他にもWhisperという 音声認識 モデルを API として公開していますので、連結することで音声だけで会話することも可能です。 映画アイアンマンのJ.A.R.V.I.S.に憧れてAIの勉強を始めたので、夢見た未来がだいぶ近づいてきたなと感じます。このようなAIが親しみやすい形で生活に浸透していくのか、今後も注視していきます。 (追記:記事公開前にGPT-4がローンチされました。AI領域の進歩の加速を感じる昨今です。) それでは最後に。 執筆: @wakamoto.ryosuke 、レビュー: Ishizawa Kento (@kent) ( Shodo で執筆されました )
こんにちは!金融ソリューション事業部の山下です。 本記事では、 Unreal Engine のPluginである OnlineSubsystem を利用して、インターネット経由で同時接続するオンライン マルチプレイ 機能を C++ で実装する手順を紹介します。 前提知識 ネットワークモデル ゲームサーバー/ゲームクライアント UEにおけるゲームサーバー方式 ゲームセッション オンラインサービス OnlineSubsystem 実施手順 実施環境/ツール 1. UEプロジェクト作成と各種設定 2. Session Interfaceの作成 3. CreateSession()の実装 4. JoinGameSession()の実装 5. 端末2台を用いた接続確認 所感 参考 前提知識 ネットワークモデル オンラインゲームにおけるネットワークモデルには複数の選択肢があります。 Unreal Engine では、基本的に「Client-Server」モデルが採用されております。 Peer-To-Peerモデル:プレイヤー同士でゲーム情報を相互通信する方式。プレイヤー数の増加に伴い通信が増大してしまう。また、ゲーム内に「唯一の正しいステート」が存在しない為、緻密な判定やプレイ精度が求められるゲームには不向きです。 Client-Serverモデル:「唯一の正しいステート」を持つゲームサーバーに対して、ゲームクライアントが接続する方式。ゲームクライアントから送られた情報は、ゲームサーバー経由で各ゲームクライアントにBroadCastされます。 ゲームサーバー/ゲームクライアント よく混同されますが、 Webサービス におけるWebサーバーとWebクライアントとは異なります。 ゲームサーバー:Client-Serverモデルにおける、「唯一の正しいステート」を持つサーバーです。 ゲームクライアント:Client-Serverモデルにおける、ゲームを実行するクライアントです。 UEにおけるゲームサーバー方式 Unrealn Engine では、以下2種類のゲームサーバー方式が利用可能です。 ListenServer:ゲームサーバー上でグラフィックの レンダリング をします。特定のプレイヤーがゲームサーバーを兼ねることにより、運営リソースを節約できます。一方で、ユーザーの端末スペックに依存してしまうこと、また多人数のゲームには不向きである点が欠点です。 DedicatedServer:ゲームサーバー上でグラフィックの レンダリング を行わいません。運営側でサーバーを用意する必要があり、ユーザー数の増加に伴いインフラコストもかかりますが、多人数のゲームにも対応可能です。 ゲームセッション よく混同されますが、ゲームセッションと Webサービス のセッションは異なります。 ゲームセッションは、具体的にはゲームサーバー上で動作するゲーム インスタンス を指します。 複数のプレイヤーが同一のゲームセッションに接続することで、「同じゲーム空間の共有 = マルチプレイ 」が可能になります。 オンラインサービス 一般的なオンライン マルチプレイ ゲームでは、UserやSession、AchievementやFriendなどの機能が必要になります。 そこでサービスプラットフォーム(Steam、 Xbox live 、 Facebook など)では、このような機能がオンラインサービスとして提供されております。 サービスプラットフォームを利用せずに自前で構築することももちろん可能です。例えば AWS では GameLift などのサービスも提供しており、DedicatedServerの ホスティング に加えてオンラインサービスの提供もされています。 AWS GameLift の利用方法については、 孫さんの記事 をぜひご覧ください。 OnlineSubsystem Unreal Engine が提供するPluginです。 各オンラインサービスプラットフォームにアクセスする為の共通モジュールおよびインターフェースが提供されています。 Steam、 Xbox live 、 Facebook 、EOSなど マルチプラットフォーム のゲームが、基本的にはコンフィギューレーションを調整するだけで1コードベースで マルチプラットフォーム の実装が可能です。 インターフェースには、Session、Friends, Achievementsなどが提供されています。 本記事では、基本的なCreateSession()とFindSessions()、JoinSession()を用います。 詳細は以下をご覧ください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/Interfaces/IOnlineSession/ 実施手順 今回は、検証を簡易にするためListenServer方式で検証を行います。 また、セッション管理を行うオンラインサービスについては、開発用IDが無償提供されているSteamネットワークを使用します。 UEプロジェクト作成と各種設定 Session Interfaceの作成 CreateSession()の実装 JoinGameSession()の実装 端末2台を用いた接続確認 実施環境/ツール OS: Windows 11 pro GPU : NVIDIA GeForce RTX 3070Ti Laptop DCC: Adobe Substance 3D Sampler 3.4.1 Game Engine: Unreal Engine 5.1.0 1. UEプロジェクト作成と各種設定 Unreal Engine のNew Project > Third Personテンプレートを選択します。Project Defaultsで、 C++ を選択します。 今回、プロジェクト名は「OnlineMultiplaySteam」としました。 Edit >Pluginを開きます。 「Online Subsystem Steam」を選択します。 Restartが求められるので再起動します。 次に、 Visual Studio エディタに移ります。 SolusionExplorerで以下のファイルを開きます。 Games > OnlineMultiplaySteam > Source > OnlineMultiplaySteam > OnlineMultiplaySteam.Build.cs PublicDependencyModuleに"OnlineSubsystemSteam", "OnlineSubsystem"を追加します。 11行目を以下に書き換え、Buildします。 PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "OnlineSubsystemSteam", "OnlineSubsystem" }); 次に、DefaultEngine.iniを修正します。 SolusionExplorerで以下のファイルを開きます。 Games > OnlineMultiplaySteam > Config > DefaultEngine.ini こちらのUEドキュメント を参考に、以下を追記します。 [/Script/Engine.GameEngine] +NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") [OnlineSubsystem] DefaultPlatformService=Steam [OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 [/Script/OnlineSubsystemSteam.SteamNetDriver] NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection" 今回使用するSteamDevAppIdは480となっております。これはSteamから開発用に提供されているサンプルゲーム(SpaceWar)のIDです。本番開発で用いる場合は、自身でAppIdを取得する必要がありますのでご注意ください。 最後にプロジェクトファイルを生成します。 Editorを閉じて、File Explorer で「Saved」「Intermidiate」「Binaries」ファイルを削除します。その後、「Generate Visual Studio project files」でプロジェクトファイルを生成します。 これでプロジェクト設定は完了です。 2. Session Interfaceの作成 ThirdPersonTemplateのCharacterクラスを修正します。 Unreal Engine 独自の プレフィックス (クラス名にF,A、型名にF,Uなど)については、 公式のコーディング規約 をご参照ください。 OnlineMultiplaySteamCharacter.hを編集します。 includeに以下を追加します。 #include "Interfaces/OnlineSessionInterface.h" 記載する行について、"....generated.h"が一番最下部になる点にはご注意ください。 class内に、以下を追加します。 public: IOnlineSessionPtr OnlineSessionInterface; SessionInterfaceの変数が宣言できました。 IOnlineSession インターフェイス 仕様は、以下を参照してください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/Interfaces/IOnlineSession/ OnlineMultiplaySteamCharacter.cppを編集します。 includeに以下を追加します。 #include "OnlineSubsystem.h" コンスト ラク タの最下部に、以下を追記します。 IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); if (OnlineSubsystem)    {     OnlineSessionInterface = OnlineSubsystem->GetSessionInterface();     if (GEngine) { GEngine->AddOnScreenDebugMessage( 15.f, Color::Blue, String::Printf(TEXT("Found subsystem %s"), *OnlineSubsystem->GetSubsystemName().ToString()) ); } } OnlineSubsystemを使って、事前に指定したSteamと接続するためのInterfaceを取得しています。 本処理はCharacterクラスに追記しているため、キャ ラク ターがレベルにSpawnするタイミングでSessionInterfaceが作成されることになります。 IOnlineSubsystemの インターフェイス 仕様は、以下をご確認ください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/IOnlineSubsystem/ ビルドしてゲームを立ち上げます。 以下のように左側に青字のメッセージが表示されることを確認します。 SessionInterfaceが無事作成され、Steamネットワークに接続ができました。 3. CreateSession()の実装 作成したSessionInterfaceを使って、ゲームセッションの作成を行います。 事前に、セッション作成後の移動先レベルを作っておきます。レベルはDafaultのままで、名称はLobbyとします。 OnlineMultiplaySteamCharacter.hを編集します。 BluePrintでイベント(今回はKey1押下イベント)を受け取ってセッションを実行する為に、 protectedセクションを作成して以下BlueprintCallable関数を追加します。 protected: UFUNCTION(BlueprintCallable) void CreateGameSession(); OnlineSessionInterfaceでCreateGameSesion()を実行後、コールバック関数であるOnCreateSessionComplete()を実行します。 同じくprotectedセクションに、セッション作成後に呼びだすコールバック関数も追加します。 void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); privateセクションを追加して、コールバック関数をBindするためのDelegete変数も追加します。 private: FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; 続いて、.cppファイルにDelegete変数をコールバック関数とbindした上で、OnlineSessionInterfaceの Delegate ハンドラーに登録していきます。 OnlineMultiplaySteamCharacter.cppに以下を追加します。 コンスト ラク タの冒頭に、以下のように Delegate 変数にコールバック関数をBindします。 AOnlineMultiplaySteamCharacter::AOnlineMultiplaySteamCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)) CreateGameSession ()を追加します。 少し長いコードになってきたので、折りたたみ表示にします。詳細はコメントをご確認ください。 CreateGameSession() void AOnlineMultiplaySteamCharacter::CreateGameSession() { // called when pressed 1 key if (!OnlineSessionInterface.IsValid()) { return; } // check existing session auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { OnlineSessionInterface->DestroySession(NAME_GameSession); } // Add Delegete variable to OnlineSessionInterface OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate); // Create Session Settings TSharedPtr <FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings()); SessionSettings->bIsLANMatch = false; SessionSettings->NumPublicConnections = 4; SessionSettings->bAllowJoinInProgress = true; SessionSettings->bAllowJoinViaPresence = true; SessionSettings->bShouldAdvertise = true; SessionSettings->bUsesPresence = true; SessionSettings->bUseLobbiesIfAvailable = true; SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); //Create Session const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings); } SessionSettingsを用意してSessionInterface->CreateSession関数を呼んでいます。 また、コンスト ラク タでも触れた Delegate 変数をハンドラーに登録しています。 SessionSettingのパラメーターは こちら を参照してください。 セッション作成後に呼びだすコールバック関数を実装します。 OnCreateSessionComplete() void AOnlineMultiplaySteamCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Blue, FString::Printf(TEXT("Successsfully Created session: %s"), *SessionName.ToString()) ); } UWorld* World = GetWorld(); if (World) { World->ServerTravel(FString("/Game/ThirdPerson/Maps/Lobby?Listen")); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to create session!")) ); } } セッション作成完了後にシンプルなログメッセージを表示しています。 またLobbyレベルへの移動は、World->ServerTravel()関数を用いました。 オプションパラメーターでListenサーバーを指定しています。 イベントを受け取るために、EditorでThirdPersonCharacterのBPを開いて以下ノードを追加します。 これで、Key1を押下すると、BluePrint Callableで実装したCreate Game Session関数が実行されます。 コンパイル 後、ゲームを立ち上げます。 Key1を押下します。 成功ログが画面に表示されれば、セッション作成は完了です。 これで、ListenServer方式のゲームサーバー側の処理は完了しました。 注意:オンラインシステムへの接続を試す際、ゲームをEditor Viewportで実行しても正しく接続されません。Standsloneモード or パッケージ化した上で実行してください。 4. JoinGameSession()の実装 BluePrintでイベント(こちらはKey2押下イベント)を受け取ってセッションに参加する処理を実装します。 OnlineMultiplaySteamCharacter.hを編集します。 protectedセクションを作成して以下BlueprintCallable関数を追加します。 protected: UFUNCTION(BlueprintCallable) void JoinGameSession(); このJoinGameSesion()の中では、「セッションの検索」と「セッションへの参加」の2つの処理を行います。 その為、今回はそれぞれのコールバック関数を2つ用意します。 void OnFindSessionsComplete(bool bWasSuccessful); void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); privateセクションに、Delegete変数も2つ追加します。 また、検索条件を格納する為のSharedPointerも追加します。 FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; // Search Setting TSharedPtr<FOnlineSessionSearch> SessionSearch; 続いてOnlineMultiplaySteamCharacter.cppを編集します。 includeに以下を追加します。 #include "OnlineSessionSettings.h" 3.と同様にコンスト ラク タにて、 Delegate 変数にコールバック関数をbindします。 FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)) 以下関数を実装します。 void JoinGameSession(); void OnFindSessionsComplete(); void OnJoinSessionComplete(); こちらもそれぞれ折りたたみ表示にします。詳細はコメントをご確認ください。 まずJoinGameSession()では、検索条件であるSessionSearchを設定してFindSessionsを実行します。 SessionSearchのパラメーターは こちら をご覧ください。 JoinGameSession() void AOnlineMultiplaySteamCharacter::JoinGameSession() { // called when pressing 2 key if (!OnlineSessionInterface.IsValid()) { return; } if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Blue, FString::Printf(TEXT("pressed 2 and Executed function: JoinGameSession()")) ); } OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate); SessionSearch = MakeShareable(new FOnlineSessionSearch()); SessionSearch->MaxSearchResults = 10000; SessionSearch->bIsLanQuery = false; SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef()); } OnFindSessionsComplete()は、FindSessions()実行後のコールバック関数です。 検索結果のSession情報を用いて、セッションに参加します。 今回利用しているSteamAppIDは世界中の人が使っている為、SessionSettingで定義したMatchTypeにフィルタリングをしています。取得したセッション情報を引数に、JoinSession()を実行します。 OnFindSessionsComplete() void AOnlineMultiplaySteamCharacter::OnFindSessionsComplete(bool bWasSuccessful) { if (!OnlineSessionInterface.IsValid()) { return; } if (bWasSuccessful) { GEngine->AddOnScreenDebugMessage(-1,15.f,FColor::Cyan, FString::Printf(TEXT("FindSession Complete! SearchResults.Num() = %d"), SessionSearch->SearchResults.Num())); for (auto Result : SessionSearch->SearchResults) { FString Id = Result.GetSessionIdStr(); FString User = Result.Session.OwningUserName; FString MatchType; Result.Session.SessionSettings.Get(FName("MatchType"), MatchType); if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Successsfully Find Session! Id: %s , OwningUser: %s"), *Id, *User) ); } if (MatchType == FString("FreeForAll")) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Joining Match Type: %s"), *MatchType) ); } OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result); } } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to join session!")) ); } } } OnJoinSessionComplete()では、セッションへの参加後、セッション情報から IPアドレス を取得してレベルに移動します。 レベルの移動には、PlayerController->ClientTravel()を用います。 OnJoinSessionComplete() void AOnlineMultiplaySteamCharacter::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!OnlineSessionInterface.IsValid()) { return; } FString Address; if (OnlineSessionInterface->GetResolvedConnectString(NAME_GameSession, Address)) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString::Printf(TEXT("Connect string: %s"), *Address) ); } APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); if (PlayerController) { PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); } } } 最後に、EditorでThirdPersonCharacterのBPを開いて以下ノードを追加します。 5. 端末2台を用いた接続確認 マルチプレイ の検証のために、 Windows マシンを2つ用意します。 また、Steam IDも2つ用意します(Steam IDが同一だとセッションに入れない為)。 各Steamアカウントの設定で、Download Regionを同一にする必要がある点にもご注意ください。 1台目で、Key1を押下します。 2台目で、Key2を押下します。 操作をしてみて、動きが正しく同期されていることを確認します。 無事、オンライン経由で マルチプレイ を実現しました。 所感 今回は、OnlineSubSystemを用いて、インターネット経由のオンライン マルチプレイ を実装しました。 基本的には Unreal Engine で提供されているOnlineSubsystem Pluginを用いることで実現可能なため、単純な ユースケース であれば比較的実装はしやすい印象でした。 以前紹介したPixelStreaming と組み合わせることで、今回用意したような高スペックなマシン不要で、オンライン マルチプレイ を実現できます。 これらについては今後も検証していきたいと思います。 以前の記事 でも紹介したように、現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ご連絡ください。 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ 参考 https://docs.unrealengine.com/5.1/en-US/online-subsystem-steam-interface-in-unreal-engine/ 執筆: @yamashita.yuki 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
こんにちは!金融ソリューション事業部の山下です。 本記事では、 Unreal Engine のPluginである OnlineSubsystem を利用して、インターネット経由で同時接続するオンライン マルチプレイ 機能を C++ で実装する手順を紹介します。 前提知識 ネットワークモデル ゲームサーバー/ゲームクライアント UEにおけるゲームサーバー方式 ゲームセッション オンラインサービス OnlineSubsystem 実施手順 実施環境/ツール 1. UEプロジェクト作成と各種設定 2. Session Interfaceの作成 3. CreateSession()の実装 4. JoinGameSession()の実装 5. 端末2台を用いた接続確認 所感 参考 前提知識 ネットワークモデル オンラインゲームにおけるネットワークモデルには複数の選択肢があります。 Unreal Engine では、基本的に「Client-Server」モデルが採用されております。 Peer-To-Peerモデル:プレイヤー同士でゲーム情報を相互通信する方式。プレイヤー数の増加に伴い通信が増大してしまう。また、ゲーム内に「唯一の正しいステート」が存在しない為、緻密な判定やプレイ精度が求められるゲームには不向きです。 Client-Serverモデル:「唯一の正しいステート」を持つゲームサーバーに対して、ゲームクライアントが接続する方式。ゲームクライアントから送られた情報は、ゲームサーバー経由で各ゲームクライアントにBroadCastされます。 ゲームサーバー/ゲームクライアント よく混同されますが、 Webサービス におけるWebサーバーとWebクライアントとは異なります。 ゲームサーバー:Client-Serverモデルにおける、「唯一の正しいステート」を持つサーバーです。 ゲームクライアント:Client-Serverモデルにおける、ゲームを実行するクライアントです。 UEにおけるゲームサーバー方式 Unrealn Engine では、以下2種類のゲームサーバー方式が利用可能です。 ListenServer:ゲームサーバー上でグラフィックの レンダリング をします。特定のプレイヤーがゲームサーバーを兼ねることにより、運営リソースを節約できます。一方で、ユーザーの端末スペックに依存してしまうこと、また多人数のゲームには不向きである点が欠点です。 DedicatedServer:ゲームサーバー上でグラフィックの レンダリング を行わいません。運営側でサーバーを用意する必要があり、ユーザー数の増加に伴いインフラコストもかかりますが、多人数のゲームにも対応可能です。 ゲームセッション よく混同されますが、ゲームセッションと Webサービス のセッションは異なります。 ゲームセッションは、具体的にはゲームサーバー上で動作するゲーム インスタンス を指します。 複数のプレイヤーが同一のゲームセッションに接続することで、「同じゲーム空間の共有 = マルチプレイ 」が可能になります。 オンラインサービス 一般的なオンライン マルチプレイ ゲームでは、UserやSession、AchievementやFriendなどの機能が必要になります。 そこでサービスプラットフォーム(Steam、 Xbox live 、 Facebook など)では、このような機能がオンラインサービスとして提供されております。 サービスプラットフォームを利用せずに自前で構築することももちろん可能です。例えば AWS では GameLift などのサービスも提供しており、DedicatedServerの ホスティング に加えてオンラインサービスの提供もされています。 AWS GameLift の利用方法については、 孫さんの記事 をぜひご覧ください。 OnlineSubsystem Unreal Engine が提供するPluginです。 各オンラインサービスプラットフォームにアクセスする為の共通モジュールおよびインターフェースが提供されています。 Steam、 Xbox live 、 Facebook 、EOSなど マルチプラットフォーム のゲームが、基本的にはコンフィギューレーションを調整するだけで1コードベースで マルチプラットフォーム の実装が可能です。 インターフェースには、Session、Friends, Achievementsなどが提供されています。 本記事では、基本的なCreateSession()とFindSessions()、JoinSession()を用います。 詳細は以下をご覧ください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/Interfaces/IOnlineSession/ 実施手順 今回は、検証を簡易にするためListenServer方式で検証を行います。 また、セッション管理を行うオンラインサービスについては、開発用IDが無償提供されているSteamネットワークを使用します。 UEプロジェクト作成と各種設定 Session Interfaceの作成 CreateSession()の実装 JoinGameSession()の実装 端末2台を用いた接続確認 実施環境/ツール OS: Windows 11 pro GPU : NVIDIA GeForce RTX 3070Ti Laptop DCC: Adobe Substance 3D Sampler 3.4.1 Game Engine: Unreal Engine 5.1.0 1. UEプロジェクト作成と各種設定 Unreal Engine のNew Project > Third Personテンプレートを選択します。Project Defaultsで、 C++ を選択します。 今回、プロジェクト名は「OnlineMultiplaySteam」としました。 Edit >Pluginを開きます。 「Online Subsystem Steam」を選択します。 Restartが求められるので再起動します。 次に、 Visual Studio エディタに移ります。 SolusionExplorerで以下のファイルを開きます。 Games > OnlineMultiplaySteam > Source > OnlineMultiplaySteam > OnlineMultiplaySteam.Build.cs PublicDependencyModuleに"OnlineSubsystemSteam", "OnlineSubsystem"を追加します。 11行目を以下に書き換え、Buildします。 PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "OnlineSubsystemSteam", "OnlineSubsystem" }); 次に、DefaultEngine.iniを修正します。 SolusionExplorerで以下のファイルを開きます。 Games > OnlineMultiplaySteam > Config > DefaultEngine.ini こちらのUEドキュメント を参考に、以下を追記します。 [/Script/Engine.GameEngine] +NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver") [OnlineSubsystem] DefaultPlatformService=Steam [OnlineSubsystemSteam] bEnabled=true SteamDevAppId=480 [/Script/OnlineSubsystemSteam.SteamNetDriver] NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection" 今回使用するSteamDevAppIdは480となっております。これはSteamから開発用に提供されているサンプルゲーム(SpaceWar)のIDです。本番開発で用いる場合は、自身でAppIdを取得する必要がありますのでご注意ください。 最後にプロジェクトファイルを生成します。 Editorを閉じて、File Explorer で「Saved」「Intermidiate」「Binaries」ファイルを削除します。その後、「Generate Visual Studio project files」でプロジェクトファイルを生成します。 これでプロジェクト設定は完了です。 2. Session Interfaceの作成 ThirdPersonTemplateのCharacterクラスを修正します。 Unreal Engine 独自の プレフィックス (クラス名にF,A、型名にF,Uなど)については、 公式のコーディング規約 をご参照ください。 OnlineMultiplaySteamCharacter.hを編集します。 includeに以下を追加します。 #include "Interfaces/OnlineSessionInterface.h" 記載する行について、"....generated.h"が一番最下部になる点にはご注意ください。 class内に、以下を追加します。 public: IOnlineSessionPtr OnlineSessionInterface; SessionInterfaceの変数が宣言できました。 IOnlineSession インターフェイス 仕様は、以下を参照してください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/Interfaces/IOnlineSession/ OnlineMultiplaySteamCharacter.cppを編集します。 includeに以下を追加します。 #include "OnlineSubsystem.h" コンスト ラク タの最下部に、以下を追記します。 IOnlineSubsystem* OnlineSubsystem = IOnlineSubsystem::Get(); if (OnlineSubsystem)    {     OnlineSessionInterface = OnlineSubsystem->GetSessionInterface();     if (GEngine) { GEngine->AddOnScreenDebugMessage( 15.f, Color::Blue, String::Printf(TEXT("Found subsystem %s"), *OnlineSubsystem->GetSubsystemName().ToString()) ); } } OnlineSubsystemを使って、事前に指定したSteamと接続するためのInterfaceを取得しています。 本処理はCharacterクラスに追記しているため、キャ ラク ターがレベルにSpawnするタイミングでSessionInterfaceが作成されることになります。 IOnlineSubsystemの インターフェイス 仕様は、以下をご確認ください。 https://docs.unrealengine.com/5.1/en-US/API/Plugins/OnlineSubsystem/IOnlineSubsystem/ ビルドしてゲームを立ち上げます。 以下のように左側に青字のメッセージが表示されることを確認します。 SessionInterfaceが無事作成され、Steamネットワークに接続ができました。 3. CreateSession()の実装 作成したSessionInterfaceを使って、ゲームセッションの作成を行います。 事前に、セッション作成後の移動先レベルを作っておきます。レベルはDafaultのままで、名称はLobbyとします。 OnlineMultiplaySteamCharacter.hを編集します。 BluePrintでイベント(今回はKey1押下イベント)を受け取ってセッションを実行する為に、 protectedセクションを作成して以下BlueprintCallable関数を追加します。 protected: UFUNCTION(BlueprintCallable) void CreateGameSession(); OnlineSessionInterfaceでCreateGameSesion()を実行後、コールバック関数であるOnCreateSessionComplete()を実行します。 同じくprotectedセクションに、セッション作成後に呼びだすコールバック関数も追加します。 void OnCreateSessionComplete(FName SessionName, bool bWasSuccessful); privateセクションを追加して、コールバック関数をBindするためのDelegete変数も追加します。 private: FOnCreateSessionCompleteDelegate CreateSessionCompleteDelegate; 続いて、.cppファイルにDelegete変数をコールバック関数とbindした上で、OnlineSessionInterfaceの Delegate ハンドラーに登録していきます。 OnlineMultiplaySteamCharacter.cppに以下を追加します。 コンスト ラク タの冒頭に、以下のように Delegate 変数にコールバック関数をBindします。 AOnlineMultiplaySteamCharacter::AOnlineMultiplaySteamCharacter(): CreateSessionCompleteDelegate(FOnCreateSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnCreateSessionComplete)) CreateGameSession ()を追加します。 少し長いコードになってきたので、折りたたみ表示にします。詳細はコメントをご確認ください。 CreateGameSession() void AOnlineMultiplaySteamCharacter::CreateGameSession() { // called when pressed 1 key if (!OnlineSessionInterface.IsValid()) { return; } // check existing session auto ExistingSession = OnlineSessionInterface->GetNamedSession(NAME_GameSession); if (ExistingSession != nullptr) { OnlineSessionInterface->DestroySession(NAME_GameSession); } // Add Delegete variable to OnlineSessionInterface OnlineSessionInterface->AddOnCreateSessionCompleteDelegate_Handle(CreateSessionCompleteDelegate); // Create Session Settings TSharedPtr <FOnlineSessionSettings> SessionSettings = MakeShareable(new FOnlineSessionSettings()); SessionSettings->bIsLANMatch = false; SessionSettings->NumPublicConnections = 4; SessionSettings->bAllowJoinInProgress = true; SessionSettings->bAllowJoinViaPresence = true; SessionSettings->bShouldAdvertise = true; SessionSettings->bUsesPresence = true; SessionSettings->bUseLobbiesIfAvailable = true; SessionSettings->Set(FName("MatchType"), FString("FreeForAll"), EOnlineDataAdvertisementType::ViaOnlineServiceAndPing); //Create Session const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->CreateSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, *SessionSettings); } SessionSettingsを用意してSessionInterface->CreateSession関数を呼んでいます。 また、コンスト ラク タでも触れた Delegate 変数をハンドラーに登録しています。 SessionSettingのパラメーターは こちら を参照してください。 セッション作成後に呼びだすコールバック関数を実装します。 OnCreateSessionComplete() void AOnlineMultiplaySteamCharacter::OnCreateSessionComplete(FName SessionName, bool bWasSuccessful) { if (bWasSuccessful) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Blue, FString::Printf(TEXT("Successsfully Created session: %s"), *SessionName.ToString()) ); } UWorld* World = GetWorld(); if (World) { World->ServerTravel(FString("/Game/ThirdPerson/Maps/Lobby?Listen")); } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to create session!")) ); } } セッション作成完了後にシンプルなログメッセージを表示しています。 またLobbyレベルへの移動は、World->ServerTravel()関数を用いました。 オプションパラメーターでListenサーバーを指定しています。 イベントを受け取るために、EditorでThirdPersonCharacterのBPを開いて以下ノードを追加します。 これで、Key1を押下すると、BluePrint Callableで実装したCreate Game Session関数が実行されます。 コンパイル 後、ゲームを立ち上げます。 Key1を押下します。 成功ログが画面に表示されれば、セッション作成は完了です。 これで、ListenServer方式のゲームサーバー側の処理は完了しました。 注意:オンラインシステムへの接続を試す際、ゲームをEditor Viewportで実行しても正しく接続されません。Standsloneモード or パッケージ化した上で実行してください。 4. JoinGameSession()の実装 BluePrintでイベント(こちらはKey2押下イベント)を受け取ってセッションに参加する処理を実装します。 OnlineMultiplaySteamCharacter.hを編集します。 protectedセクションを作成して以下BlueprintCallable関数を追加します。 protected: UFUNCTION(BlueprintCallable) void JoinGameSession(); このJoinGameSesion()の中では、「セッションの検索」と「セッションへの参加」の2つの処理を行います。 その為、今回はそれぞれのコールバック関数を2つ用意します。 void OnFindSessionsComplete(bool bWasSuccessful); void OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result); privateセクションに、Delegete変数も2つ追加します。 また、検索条件を格納する為のSharedPointerも追加します。 FOnFindSessionsCompleteDelegate FindSessionsCompleteDelegate; FOnJoinSessionCompleteDelegate JoinSessionCompleteDelegate; // Search Setting TSharedPtr<FOnlineSessionSearch> SessionSearch; 続いてOnlineMultiplaySteamCharacter.cppを編集します。 includeに以下を追加します。 #include "OnlineSessionSettings.h" 3.と同様にコンスト ラク タにて、 Delegate 変数にコールバック関数をbindします。 FindSessionsCompleteDelegate(FOnFindSessionsCompleteDelegate::CreateUObject(this, &ThisClass::OnFindSessionsComplete)), JoinSessionCompleteDelegate(FOnJoinSessionCompleteDelegate::CreateUObject(this, &ThisClass::OnJoinSessionComplete)) 以下関数を実装します。 void JoinGameSession(); void OnFindSessionsComplete(); void OnJoinSessionComplete(); こちらもそれぞれ折りたたみ表示にします。詳細はコメントをご確認ください。 まずJoinGameSession()では、検索条件であるSessionSearchを設定してFindSessionsを実行します。 SessionSearchのパラメーターは こちら をご覧ください。 JoinGameSession() void AOnlineMultiplaySteamCharacter::JoinGameSession() { // called when pressing 2 key if (!OnlineSessionInterface.IsValid()) { return; } if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Blue, FString::Printf(TEXT("pressed 2 and Executed function: JoinGameSession()")) ); } OnlineSessionInterface->AddOnFindSessionsCompleteDelegate_Handle(FindSessionsCompleteDelegate); SessionSearch = MakeShareable(new FOnlineSessionSearch()); SessionSearch->MaxSearchResults = 10000; SessionSearch->bIsLanQuery = false; SessionSearch->QuerySettings.Set(SEARCH_PRESENCE, true, EOnlineComparisonOp::Equals); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->FindSessions(*LocalPlayer->GetPreferredUniqueNetId(), SessionSearch.ToSharedRef()); } OnFindSessionsComplete()は、FindSessions()実行後のコールバック関数です。 検索結果のSession情報を用いて、セッションに参加します。 今回利用しているSteamAppIDは世界中の人が使っている為、SessionSettingで定義したMatchTypeにフィルタリングをしています。取得したセッション情報を引数に、JoinSession()を実行します。 OnFindSessionsComplete() void AOnlineMultiplaySteamCharacter::OnFindSessionsComplete(bool bWasSuccessful) { if (!OnlineSessionInterface.IsValid()) { return; } if (bWasSuccessful) { GEngine->AddOnScreenDebugMessage(-1,15.f,FColor::Cyan, FString::Printf(TEXT("FindSession Complete! SearchResults.Num() = %d"), SessionSearch->SearchResults.Num())); for (auto Result : SessionSearch->SearchResults) { FString Id = Result.GetSessionIdStr(); FString User = Result.Session.OwningUserName; FString MatchType; Result.Session.SessionSettings.Get(FName("MatchType"), MatchType); if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Successsfully Find Session! Id: %s , OwningUser: %s"), *Id, *User) ); } if (MatchType == FString("FreeForAll")) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Cyan, FString::Printf(TEXT("Joining Match Type: %s"), *MatchType) ); } OnlineSessionInterface->AddOnJoinSessionCompleteDelegate_Handle(JoinSessionCompleteDelegate); const ULocalPlayer* LocalPlayer = GetWorld()->GetFirstLocalPlayerFromController(); OnlineSessionInterface->JoinSession(*LocalPlayer->GetPreferredUniqueNetId(), NAME_GameSession, Result); } } } else { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Red, FString(TEXT("Failed to join session!")) ); } } } OnJoinSessionComplete()では、セッションへの参加後、セッション情報から IPアドレス を取得してレベルに移動します。 レベルの移動には、PlayerController->ClientTravel()を用います。 OnJoinSessionComplete() void AOnlineMultiplaySteamCharacter::OnJoinSessionComplete(FName SessionName, EOnJoinSessionCompleteResult::Type Result) { if (!OnlineSessionInterface.IsValid()) { return; } FString Address; if (OnlineSessionInterface->GetResolvedConnectString(NAME_GameSession, Address)) { if (GEngine) { GEngine->AddOnScreenDebugMessage( -1, 15.f, FColor::Yellow, FString::Printf(TEXT("Connect string: %s"), *Address) ); } APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController(); if (PlayerController) { PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute); } } } 最後に、EditorでThirdPersonCharacterのBPを開いて以下ノードを追加します。 5. 端末2台を用いた接続確認 マルチプレイ の検証のために、 Windows マシンを2つ用意します。 また、Steam IDも2つ用意します(Steam IDが同一だとセッションに入れない為)。 各Steamアカウントの設定で、Download Regionを同一にする必要がある点にもご注意ください。 1台目で、Key1を押下します。 2台目で、Key2を押下します。 操作をしてみて、動きが正しく同期されていることを確認します。 無事、オンライン経由で マルチプレイ を実現しました。 所感 今回は、OnlineSubSystemを用いて、インターネット経由のオンライン マルチプレイ を実装しました。 基本的には Unreal Engine で提供されているOnlineSubsystem Pluginを用いることで実現可能なため、単純な ユースケース であれば比較的実装はしやすい印象でした。 以前紹介したPixelStreaming と組み合わせることで、今回用意したような高スペックなマシン不要で、オンライン マルチプレイ を実現できます。 これらについては今後も検証していきたいと思います。 以前の記事 でも紹介したように、現在ISIDは web3領域のグループ横断組織 を立ち上げ、Web3および メタバース 領域のR&Dを行っております。 もし本領域にご興味のある方や、一緒にチャレンジしていきたい方は、ご連絡ください。 私たちと同じチームで働いてくれる仲間を、是非お待ちしております! ISID採用ページ 参考 https://docs.unrealengine.com/5.1/en-US/online-subsystem-steam-interface-in-unreal-engine/ 執筆: @yamashita.yuki 、レビュー: @wakamoto.ryosuke ( Shodo で執筆されました )
みなさんこんにちは! 金融ソリューション事業部リースソリューション部の寺山です。本年から所属部署が変わりました。 今回は業務中に発見した Terraform の Tips を紹介する短めの記事となります。 先に結論 Terraform CloudのRemoteモードについて Remoteモード時のローカルplan時間の課題 解決方法と私がハマったポイント 注意点 先に結論 タイトルを先に回収しますと、ローカル plan 時間が大幅に改善された .terraformignore は以下です。 # ignore all ** # include terraform !code/terraform/ # ignore .terraform code/terraform/**/.terraform/ 以降のセクションでは背景等を含めて詳細を解説します。 Terraform CloudのRemoteモードについて 弊社の金融ソリューション事業部は M5 という Java 製マイクロサービス フレームワーク を製品開発し、お客様のシステムや自社サービス/パッケージの開発に活用しております。私は、この M5 をホストするためのインフラ構成の検討や、その実装方法(主に IaC の開発)に従事しています。 IaC 化ツールとして HashiCorp 社の Terraform 、および、同社の SaaS である Terraform Cloud を利用しています。 Terraform Cloud の Workspace の設定には Execution Mode というものがあります。この設定で Remote を選択すると、 terraform plan や terraform apply といった Terraform の実行は Terraform Cloud でホストされる 仮想マシン 内で実行されます。 Remote モードには以下のようなメリットがあります。 GitHub 等の構成管理サービスと組み合わせて、インフラスト ラク チャに対する CI/CD パイプラインを容易に構築可能 インフラスト ラク チャの操作権限は Terraform Cloud に設定すればよいため、 AWS の IAM ユーザアクセスキーのような認証情報を作業者全員に配布することは不要となる インフラスト ラク チャに影響を及ぼす terraform apply も Terraform Cloud 上で実行されるため、操作証跡が集約される Terraform および Provider Plugin のバージョンの作業者間における差異を抑制できる 専用の UI が提供されており、特に plan や apply コマンドの結果の視認性が優れている また、Remote モードではローカルで実行した terraform plan も Terraform Cloud 上で実行されます。タイトルの「ローカル plan」とはこのオペレーションのことを指しています。 Remoteモード時のローカルplan時間の課題 前述のようにメリットが多い Terraform Cloud の Remote モードですが、実は課題に感じている点がありました。 ローカルで plan 時は、ローカルの ファイルシステム にある リポジトリ ルート ディレクト リを起点として全ファイルが Terraform Cloud に送信されます。送信された ソースコード を基に環境が構築され、 plan が実行される仕組みです。 このローカル plan を実行には、毎回 10 分ほど要していました。 $ time terraform plan # (省略) terraform plan 94.39s user 8.39s system 18% cpu 9:21.40 total plan は頻繁に実行するコマンドであるため、毎回 10 分待たされるのは生産性に影響が出てしまいます。 Remote モードではなく Local に変更するか、Terraform Cloud の利用を停止するかを検討していました。 解決方法と私がハマったポイント 本件の解決方法は冒頭のセクションに記載したとおり .terraformignore ファイルを利用することです。 .terraformignore は .gitignore に類似した仕組みで、当該ファイルに記述したファイルや ディレクト リを Terraform Cloud に送信する対象外とします。 このファイルの仕様は Excluding Files from Upload with .terraformignore で説明されています。 .gitignore に類似した仕組みではあるものの リポジトリ のルート ディレクト リ以外では機能しない デフォルトで .git と .terraform ディレクト リが除外される 実は .terraformignore のことは以前から認識しており利用していました。当該 リポジトリ は monorepo として運用しており、Terraform の IaC 化コードの他に node_modules のような容量が大きい ディレクト リも存在していました。 Terraform の ディレクト リ以外を .terraformignore に記述することでローカル plan 時間が改善されることを期待していたのですが、前述した plan 時間は .terraformignore 配置後のものでした。 この頃に指定していた内容は以下です。 # ignore all ** # include terraform !code/terraform/ 1 行目の記述で リポジトリ 内の全ファイルを除外した上で、Terraform の IaC 化コードが配置された ディレクト リのみは Terraform Cloud に送信されるように ! を付与しています。 前述したように monorepo 運用している背景から、今後も ディレクト リが増えたとしても、このファイルをメンテナンスする必要がないよう ホワイトリスト 形式にしたいという意図です。 TF_IGNORE 環境変数 に trace を指定し、除外されたファイルを表示して確認すると以下のような出力となります。 $ terraform plan #(省略) Skipping excluded path: .git Skipping excluded path: .git/COMMIT_EDITMSG Skipping excluded path: .git/FETCH_HEAD #(省略) Skipping excluded path: code/shell/aws/sts/set-sts-profile.sh Skipping excluded path: code/terraform Skipping excluded path: doc #(省略) Skipping excluded path: doc/node_modules Skipping excluded path: doc/node_modules/.bin Skipping excluded path: doc/node_modules/.bin/acorn #(省略) 一見、記述の意図したとおり除外できているように見えます。 しかしながら、デフォルトで除外されている .terraform は Terraform Cloud に送信されてしまっていたため、ローカル plan 時間が長くなっていたとわかりました。 .terraform ディレクト リには terraform init 時に Provider Plugin がダウンロードされます。この プラグイン が数百 MB の容量のため、送信に時間を要します。 $ du -h .terraform 369M .terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0/darwin_amd64 369M .terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0 369M .terraform/providers/registry.terraform.io/hashicorp/aws 14M .terraform/providers/registry.terraform.io/hashicorp/random/3.3.2/darwin_amd64 14M .terraform/providers/registry.terraform.io/hashicorp/random/3.3.2 13M .terraform/providers/registry.terraform.io/hashicorp/random/3.4.3/darwin_amd64 13M .terraform/providers/registry.terraform.io/hashicorp/random/3.4.3 28M .terraform/providers/registry.terraform.io/hashicorp/random 397M .terraform/providers/registry.terraform.io/hashicorp 397M .terraform/providers/registry.terraform.io 397M .terraform/providers 4.0K .terraform/modules 397M .terraform したがって、私のように ホワイトリスト 風な記述をする場合は、 .terraform ディレクト リを明示的に指定する必要があります。 # ignore all ** # include terraform !code/terraform/ # ignore .terraform code/terraform/**/.terraform/ ##←これ .terraformignore に追加後はローカル plan 時間が 1 分ほどとなり、大幅に短縮されました! $ time terraform plan # (省略) Skipping excluded path: code/shell/aws/sts/set-sts-profile.sh Skipping excluded path: code/terraform Skipping excluded path: code/terraform/aws/excluded/.terraform/ Skipping excluded path: code/terraform/aws/excluded/.terraform/environment Skipping excluded path: code/terraform/aws/excluded/.terraform/providers Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0 Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0/darwin_amd64 Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0/darwin_amd64/terraform-provider-aws_v4.52.0_x5 # (省略) terraform plan 6.07s user 0.58s system 11% cpu 58.623 total 1 分ほどであれば生産性への影響は許容範囲であると評価しており、Terraform Cloud の Remote モードの利用を継続することにネガティブな要素はなくなりました。 なお、 ブラックリスト 形式で除外したいパスを丁寧に記述すれば、Terraform のドキュメントに記載のとおりデフォルトで .git と .terraform ディレクト リは除外されます。 同じような境遇の方のご参考になれば幸いです! 最後までお読みいただきありがとうとございました。 注意点 ローカル plan の所要時間は、Terraform の IaC 化コードのファイルサイズやネットワーク帯域・通信品質に依存するため、同じ効果が得られるとはお約束できないこと、ご留意ください。 私たちは一緒に働いてくれる仲間を募集しています! 金融機関のようなお客様においても クラウド ファーストやマイクロサービスのようなモダンな技術の採用が進みつつあります。ご自身のインフラ/ クラウド スキルを エンタープライズ 領域でも活用したい・挑戦したいという方がいらっしゃいましたら、ぜひご応募ください! 金融システム インフラ・アーキテクト 金融システム インフラエンジニア 執筆: 寺山 輝 (@terayama.akira) 、レビュー: @mizuno.kazuhiro ( Shodo で執筆されました )
みなさんこんにちは! 金融ソリューション事業部リースソリューション部の寺山です。本年から所属部署が変わりました。 今回は業務中に発見した Terraform の Tips を紹介する短めの記事となります。 先に結論 Terraform CloudのRemoteモードについて Remoteモード時のローカルplan時間の課題 解決方法と私がハマったポイント 注意点 先に結論 タイトルを先に回収しますと、ローカル plan 時間が大幅に改善された .terraformignore は以下です。 # ignore all ** # include terraform !code/terraform/ # ignore .terraform code/terraform/**/.terraform/ 以降のセクションでは背景等を含めて詳細を解説します。 Terraform CloudのRemoteモードについて 弊社の金融ソリューション事業部は M5 という Java 製マイクロサービス フレームワーク を製品開発し、お客様のシステムや自社サービス/パッケージの開発に活用しております。私は、この M5 をホストするためのインフラ構成の検討や、その実装方法(主に IaC の開発)に従事しています。 IaC 化ツールとして HashiCorp 社の Terraform 、および、同社の SaaS である Terraform Cloud を利用しています。 Terraform Cloud の Workspace の設定には Execution Mode というものがあります。この設定で Remote を選択すると、 terraform plan や terraform apply といった Terraform の実行は Terraform Cloud でホストされる 仮想マシン 内で実行されます。 Remote モードには以下のようなメリットがあります。 GitHub 等の構成管理サービスと組み合わせて、インフラスト ラク チャに対する CI/CD パイプラインを容易に構築可能 インフラスト ラク チャの操作権限は Terraform Cloud に設定すればよいため、 AWS の IAM ユーザアクセスキーのような認証情報を作業者全員に配布することは不要となる インフラスト ラク チャに影響を及ぼす terraform apply も Terraform Cloud 上で実行されるため、操作証跡が集約される Terraform および Provider Plugin のバージョンの作業者間における差異を抑制できる 専用の UI が提供されており、特に plan や apply コマンドの結果の視認性が優れている また、Remote モードではローカルで実行した terraform plan も Terraform Cloud 上で実行されます。タイトルの「ローカル plan」とはこのオペレーションのことを指しています。 Remoteモード時のローカルplan時間の課題 前述のようにメリットが多い Terraform Cloud の Remote モードですが、実は課題に感じている点がありました。 ローカルで plan 時は、ローカルの ファイルシステム にある リポジトリ ルート ディレクト リを起点として全ファイルが Terraform Cloud に送信されます。送信された ソースコード を基に環境が構築され、 plan が実行される仕組みです。 このローカル plan を実行には、毎回 10 分ほど要していました。 $ time terraform plan # (省略) terraform plan 94.39s user 8.39s system 18% cpu 9:21.40 total plan は頻繁に実行するコマンドであるため、毎回 10 分待たされるのは生産性に影響が出てしまいます。 Remote モードではなく Local に変更するか、Terraform Cloud の利用を停止するかを検討していました。 解決方法と私がハマったポイント 本件の解決方法は冒頭のセクションに記載したとおり .terraformignore ファイルを利用することです。 .terraformignore は .gitignore に類似した仕組みで、当該ファイルに記述したファイルや ディレクト リを Terraform Cloud に送信する対象外とします。 このファイルの仕様は Excluding Files from Upload with .terraformignore で説明されています。 .gitignore に類似した仕組みではあるものの リポジトリ のルート ディレクト リ以外では機能しない デフォルトで .git と .terraform ディレクト リが除外される 実は .terraformignore のことは以前から認識しており利用していました。当該 リポジトリ は monorepo として運用しており、Terraform の IaC 化コードの他に node_modules のような容量が大きい ディレクト リも存在していました。 Terraform の ディレクト リ以外を .terraformignore に記述することでローカル plan 時間が改善されることを期待していたのですが、前述した plan 時間は .terraformignore 配置後のものでした。 この頃に指定していた内容は以下です。 # ignore all ** # include terraform !code/terraform/ 1 行目の記述で リポジトリ 内の全ファイルを除外した上で、Terraform の IaC 化コードが配置された ディレクト リのみは Terraform Cloud に送信されるように ! を付与しています。 前述したように monorepo 運用している背景から、今後も ディレクト リが増えたとしても、このファイルをメンテナンスする必要がないよう ホワイトリスト 形式にしたいという意図です。 TF_IGNORE 環境変数 に trace を指定し、除外されたファイルを表示して確認すると以下のような出力となります。 $ terraform plan #(省略) Skipping excluded path: .git Skipping excluded path: .git/COMMIT_EDITMSG Skipping excluded path: .git/FETCH_HEAD #(省略) Skipping excluded path: code/shell/aws/sts/set-sts-profile.sh Skipping excluded path: code/terraform Skipping excluded path: doc #(省略) Skipping excluded path: doc/node_modules Skipping excluded path: doc/node_modules/.bin Skipping excluded path: doc/node_modules/.bin/acorn #(省略) 一見、記述の意図したとおり除外できているように見えます。 しかしながら、デフォルトで除外されている .terraform は Terraform Cloud に送信されてしまっていたため、ローカル plan 時間が長くなっていたとわかりました。 .terraform ディレクト リには terraform init 時に Provider Plugin がダウンロードされます。この プラグイン が数百 MB の容量のため、送信に時間を要します。 $ du -h .terraform 369M .terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0/darwin_amd64 369M .terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0 369M .terraform/providers/registry.terraform.io/hashicorp/aws 14M .terraform/providers/registry.terraform.io/hashicorp/random/3.3.2/darwin_amd64 14M .terraform/providers/registry.terraform.io/hashicorp/random/3.3.2 13M .terraform/providers/registry.terraform.io/hashicorp/random/3.4.3/darwin_amd64 13M .terraform/providers/registry.terraform.io/hashicorp/random/3.4.3 28M .terraform/providers/registry.terraform.io/hashicorp/random 397M .terraform/providers/registry.terraform.io/hashicorp 397M .terraform/providers/registry.terraform.io 397M .terraform/providers 4.0K .terraform/modules 397M .terraform したがって、私のように ホワイトリスト 風な記述をする場合は、 .terraform ディレクト リを明示的に指定する必要があります。 # ignore all ** # include terraform !code/terraform/ # ignore .terraform code/terraform/**/.terraform/ ##←これ .terraformignore に追加後はローカル plan 時間が 1 分ほどとなり、大幅に短縮されました! $ time terraform plan # (省略) Skipping excluded path: code/shell/aws/sts/set-sts-profile.sh Skipping excluded path: code/terraform Skipping excluded path: code/terraform/aws/excluded/.terraform/ Skipping excluded path: code/terraform/aws/excluded/.terraform/environment Skipping excluded path: code/terraform/aws/excluded/.terraform/providers Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0 Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0/darwin_amd64 Skipping excluded path: code/terraform/aws/excluded/.terraform/providers/registry.terraform.io/hashicorp/aws/4.52.0/darwin_amd64/terraform-provider-aws_v4.52.0_x5 # (省略) terraform plan 6.07s user 0.58s system 11% cpu 58.623 total 1 分ほどであれば生産性への影響は許容範囲であると評価しており、Terraform Cloud の Remote モードの利用を継続することにネガティブな要素はなくなりました。 なお、 ブラックリスト 形式で除外したいパスを丁寧に記述すれば、Terraform のドキュメントに記載のとおりデフォルトで .git と .terraform ディレクト リは除外されます。 同じような境遇の方のご参考になれば幸いです! 最後までお読みいただきありがとうとございました。 注意点 ローカル plan の所要時間は、Terraform の IaC 化コードのファイルサイズやネットワーク帯域・通信品質に依存するため、同じ効果が得られるとはお約束できないこと、ご留意ください。 私たちは一緒に働いてくれる仲間を募集しています! 金融機関のようなお客様においても クラウド ファーストやマイクロサービスのようなモダンな技術の採用が進みつつあります。ご自身のインフラ/ クラウド スキルを エンタープライズ 領域でも活用したい・挑戦したいという方がいらっしゃいましたら、ぜひご応募ください! 金融システム インフラ・アーキテクト 金融システム インフラエンジニア 執筆: 寺山 輝 (@terayama.akira) 、レビュー: @mizuno.kazuhiro ( Shodo で執筆されました )