Webブラウザでのみ設定可能な装置の管理自動化アプリをSeleniumで作ってみた

はじめに

こんにちは、NTTフィールドテクノの福田です。
今回は、業務環境の改善のために導入した、ある装置の管理自動化をめざしたものの、Webブラウザでのみ設定可能という仕様上の制約がありましたので、Selenium(Webブラウザ操作の自動化を行うオープンソースのテストフレームワーク)を使ってなんとかした経緯を紹介する記事になります。
本記事は2025年8月時点の情報に基づきます。


対象読者

本記事が想定する対象読者は以下の通りです。

  • Webブラウザ操作の自動化に興味を持っている人
  • トラブル発生時の「現場の付け焼刃」エピソードに興味のある方

背景・目的

業務環境の改善のために、ある装置を導入することとなりました。しかし、諸般の事情から、保守者が手動で設定・管理をするには限界があることがわかりました。
そこで自動化ツールを作成しようと思ったのですが、Webブラウザ操作でのみ設定可能な特殊仕様の装置でした。
それならば、Webブラウザ操作の自動化に用いられているSeleniumを使って自動化を実現しようと試みました。

Webブラウザ操作の自動化ツールを実現するためにはさまざまな手法があるかと思いますが、今回は、私が多少開発ノウハウのあったPHPによるWebアプリとして作成することにしました。


前提条件・動作環境

本記事は以下の環境・前提条件で執筆しました。

  • ツールサーバ:Dockerコンテナが稼働する環境
    • Webサーバコンテナ:rockylinux:8.9
      • ミドルウェア:PHP 8.2
        • ライブラリ:php-webdriver
    • Seleniumコンテナ:selenium/standalone-chrome:128.0
ポイント

ツールサーバはSeleniumコンテナとWebサーバコンテナが稼働し、Webブラウザ操作自動化を実現するとともに、Webブラウザから操作ができるWebアプリを提供するサーバとします。 諸般の事情から、Webサーバコンテナとしてはphp-apacheではなくrockylinux上に環境を構築するものとしました。


実装その1~Selenium動作環境を整える~

今回はSeleniumのDockerコンテナを利用することとしました。
コンテナの起動にはDocker Composeを使うこととし、以下のような定義としました。

docker-compose.yml
selenium:
    image: selenium/standalone-chrome:128.0
    container_name: selenium
    volumes:
      - ./hogehoge/:/home/seluser/Downloads/
    shm_size: 2g
    environment:
            SE_NODE_SESSION_TIMEOUT: 600
            SE_NODE_MAX_INSTANCES: 10
            SE_NODE_MAX_SESSIONS: 10
            SE_NODE_OVERRIDE_MAX_SESSIONS: true
            TZ: "Asia/Tokyo"
設定ポイント
  • volumes: Webブラウザでダウンロードしたファイルを利用(永続化)するためホストのフォルダをマウントします。装置設定前にバックアップファイルをダウンロードすることがあるため設定しています
  • shm_size: 2g コンテナに割り当て可能なメモリ量を増やし、Webブラウザウィンドウを多数開いたときのメモリ枯渇を防ぎます
  • SE_NODE_MAX_INSTANCES: 10 Webブラウザ1種類あたり10ウィンドウまで開くことができるようにします(余裕を持った設定です)
  • SE_NODE_MAX_SESSIONS: 10 全Webブラウザ合計で10ウィンドウまで開くことができるようにします(余裕を持った設定です)
  • SE_NODE_OVERRIDE_MAX_SESSIONS: true により、MAX_SESSIONS設定をオーバーライドします

実装その2~PHPからSeleniumを操作する下地作り~

php-webdriverの公式githubページに記載のインストール方法や、その他の手法に従い、webdriverをPHPで取り扱えるようにします。
基本はComposer(PHPのライブラリ管理ツール)を使うのが一般的です。
php-webdriverのインストールが完了したら、いよいよコーディングへと移ります。

定数の定義(module_define.php)

Webブラウザ操作のWAITTIMEなど、共通で何度も使うパラメータの定義とその保守性向上のために、定数を定義するPHPファイルを切り出しました。

<?php
...(省略)...
# Seleniumサーバ定義
const SELENIUM_SERVER = 'http://selenium:{port番号}/wd/hub';
# プログラムとしての最大セッション数(サーバの10に対してここでは3と決め打ち)
const SELENIUM_MAX_SESSIONS = 3;

# その他処理関連定義
const SELENIUM_RETRY = 3;
const SELENIUM_WAITTIME = 100000;
const SELENIUM_TUNINGWAIT = 100000;
const SELENIUM_TUNINGWAIT_LONG = 500000;
const SELENIUM_TIMEOUT = 5;
...(省略)...
?>
ライブラリ読み込み部分抜粋(module_selenium.php)

PHPファイルの先頭に以下のような記述をすることでSelenium操作用ライブラリを読み込みます。(ついでに上で作った定数定義も取り込みます)

<?php
...(省略)...
# 定数の定義
require_once 'module_define.php';
...(省略)...
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverSelect;
...(省略)...
?>
Webブラウザの起動と終了(module_selenium.php)

PHPからWebブラウザを操作する基本的な流れとしては

  1. PHPからSelenium上のWebブラウザを起動
  2. 各種操作
  3. Webブラウザの終了(quit())

となります。 まずはWebブラウザを起動する関数と、終了する関数を準備しました。

<?php
...(省略)...
# Chromeを起動する
function openChrome(){
        try{
                # Chromeオプション設定
                $chromeOptions = new ChromeOptions();
                # 起動オプションを設定(詳細省略)
                $chromeOptions->addArguments([
                ]);
                $capabilities = DesiredCapabilities::chrome();
                $capabilities->setCapability(ChromeOptions::CAPABILITY, $chromeOptions);

                # Seleniumサーバの設定
                $host = SELENIUM_SERVER;

                # Chrome起動
                $driver = RemoteWebDriver::create($host, $capabilities);

                # タイムアウト設定
                $driver->manage()->timeouts()->pageLoadTimeout(SELENIUM_TIMEOUT);

                # Chromeドライバをリターン
                return $driver;
        }catch (Exception $e){

        }
}

# Chromeを終了する
function closeChrome($driver){
        $driver->quit();
}
...(省略)...
?>

実装その3~Webブラウザ操作用の汎用関数の作成~

Webブラウザ操作を以下のように分解して、それぞれ汎用関数を作成しました。
Webブラウザは、主にHTML形式で記載された情報を整形して画面表示をしています。 HTML形式のデーター中のHTMLタグで囲まれた情報単位のことを「要素」と呼び、Seleniumではこの「要素」に対して操作を行うことができます。 ※さまざまな操作のうちの一例となります。
※既に用意されている関数はそれを使うものとします

  • id属性またはname属性により、操作対象の要素を選択する
  • 要素からパラメータを取得する
  • 要素にパラメータ(例えばテキストボックスであれば文字列)を入力する
  • 要素をクリックする

また、テキストボックスに文字を入力する場合(要素選択→要素クリア→文字列の入力)など、一部の操作はひとまとまりにしました。

汎用関数の作成(module_selenium.php)―要素をクリックする関数の例
<?php
...(省略)...
# id属性で取得したエレメントをクリックする
function clickById($driver, $id){
        $retryCount = 0;
        # リトライ回数まで繰り返す
        while(true){
                try{
                        # 要素がクリックできるようになるまで待つ
                        $driver->wait(SELENIUM_TIMEOUT)->until(
                                WebDriverExpectedCondition::elementToBeClickable(WebDriverBy::id($id)),
                                'clickById-Timeout :'.$id
                        );
                        # 要素をクリックし終了
                        $driver->findElement(WebDriverBy::id($id))->click();
                        break;
                # エラー時はリトライ試行
                } catch (Exception $e){
                        $retryCount++;
                        if($retryCount == SELENIUM_RETRY){
                                throw $e;
                        }
                        usleep(SELENIUM_WAITTIME);
                        continue;
                }
        }
}
...(省略)...
?>
ポイント

SeleniumはWebブラウザ操作を自動化しているため、Webブラウザ上の表示にかなり依存する動きをします。
例えば、通信データは受信完了しているが、Webブラウザ表示がまだこれから、という状況で要素を探しても、見つからずにエラーとなる場合があります。
そのため、Webブラウザ表示を待つ処理を書くことになるのですが、これが意外と上手くいかない・・・。
(WAITTIMEを多く取れば安定に向かっていくものの、処理時間がかかりすぎる)
さまざまな先人の知恵を組み合わせて、表示を待つ処理と、エラー時に3回までリトライする処理を組み合わせて、安定動作かつ高速な動作を実現しました。


実装その4~装置操作の流れをまとめた関数の作成~

装置操作の流れをひとまとまりにした関数を作成しました。
例えば装置にログインする場合は、以下のような操作となります。

  1. Webブラウザから装置にアクセスする
  2. name属性が"username"である要素(テキストボックス)を選択
  3. テキストボックスに、ユーザー名文字列を入力する
  4. name属性が"password"である要素(テキストボックス)を選択
  5. テキストボックスに、パスワード文字列を入力する
  6. name属性が"login"である要素(ボタン)を選択
  7. ボタンをクリックする

これらを、実装その3で作成した汎用関数を使って表現すると以下のようになります。 ※装置はhttps://{IPv4アドレス}で接続できるものとします。

操作用のまとめ関数の作成(module_login.php)
<?php
# selenium操作用汎用関数の読み込み
require_once 'module_selenium.php';
...(省略)...
# 装置にログインする
function login($driver, $ipAddress, $user, $password){
        try{
                # 接続
                $URL = 'https://'.rawurlencode($ipAddress);
                $driver->get($URL);

                # ユーザー名入力
                setInputTextByName($driver, 'username', $user);
                # パスワード入力
                setInputTextByName($driver, 'password', $password);
                # ログインボタン押下
                clickByName($driver, 'login');
        } catch (Exception $e) {
                throw $e;
        }
}
...(省略)...
?>
設定ポイント
  • $driver->get($URL); 「1. Webブラウザから装置にアクセスする」に対応します
  • setInputTextByName($driver, 'username', $user); は前項で作成した汎用関数を使います。「2. name属性が"username"である要素(テキストボックス)を選択」→「3. テキストボックスに、ユーザー名文字列を入力する」に対応します
  • setInputTextByName($driver, 'password', $password); 「4. name属性が"password"である要素(テキストボックス)を選択」→「5. テキストボックスに、パスワード文字列を入力する」に対応します
  • clickByName($driver, 'login'); は前項で作成した汎用関数を使います。「6. name属性が"login"である要素(ボタン)を選択」→「7. ボタンをクリックする」に対応します

実装その5~操作の組み合わせ~

実装その4にてある程度流れをまとめましたが、それをさらにまとめます。 例えば装置のネットワーク設定を変更したい場合の例としては

  1. 装置ログイン
  2. 事前バックアップ(configファイルのダウンロード)
  3. ネットワーク設定に遷移
  4. パラメータ設定
  5. 設定保存
  6. 事後バックアップ
  7. 装置ログアウト

という流れになります。

これら操作を必要な分だけまとめました。

  • 装置の基本設定セット
  • アカウント設定セット
  • ネットワーク設定セット
  • セキュリティ設定セット
  • etc...
装置のネットワーク設定操作の組み合わせ例の図

組み合わせ例の図


できた!・・・と思いきや

さて、Webブラウザ操作を自動化し、保守者がWebアプリ上からほぼワンボタンで、特定の装置の設定が完了するぞ!という状況になったと思いきや、利用者からクレームが入りました。
装置はそれを取り扱うチーム単位で、約30人分くらいのアカウント情報を登録していきますが、そのうち3~4人分についてログインできないというのです(残りの20数人は問題なし)
装置内のアカウント情報を確認すると、なんと一部のユーザー情報が欠けているばかりではなく、重複して登録されているユーザーもいる状況でした。

クレーム発生時のアカウント登録の状況

机上でプログラムを見返しても特に問題があるように見えず、一時的にSeleniumサーバの設定を変更し、動作状況を確認できるようにしてみました。
するとユーザーを新規登録した際にランダムに既存ユーザーの情報が置き換わってしまい、重複も発生するという動作が見えました。

アカウント情報が上書きされ、重複が発生する状況

どこかでSelenium操作用の関数にミスがあるのか・・・?と探しますが特に問題があるように見えず、処理速度の問題だろうか?とWAITTIMEを多めにとるなどをしても特に状況が変わらないなか、こんな情報が・・・。

「手動でユーザーをたくさん登録したときにもたまに起こるんだよね」

なるほど、ということは自動化ツールが悪いのではなく、また手動でも起こり得るということは処理が速すぎてトラブルになっているわけでもない・・・
であるならば、これは装置側のWebインターフェースに起因する可能性があるのではないか?という考えが頭をよぎりながらも、判断が必要になりました。

  • 装置メーカーに連絡して、Webインターフェースの動作について調査する。
  • 力技で乗り切る

あるべき姿は前者だとは思いますが、メーカーとの折衝やその解決にはそれなりの期間を要することが予想できます。
今回は運用開始後であり急ぎ対応が求められるケースであったため、短期間の対処を最優先とし、いったん力技で乗り切ることで進めることにしました。


不具合の修正

アカウント登録用の関数を編集し、重複や消えてしまった(足りていない)ユーザーを対処する処理を追加し、また最終的に全員分のアカウント情報が正しくなるまで繰り返す処理としました。

アカウント登録用の関数の編集
<?php
...(省略 ― データベースからユーザーリストを取り出す処理)...
...(省略 ― 装置のアカウント管理画面に遷移する処理)...
        # 完了までループする
        while(true){
                # 待機
                usleep(SELENIUM_TUNINGWAIT_LONG);
                # ユーザー管理画面よりユーザー一覧を取得
                $users = userManagementGetUsers($driver);

                # ユーザー一覧のうち重複ユーザーを抽出
                $duplicateUsers = findDuplicateArray($users);

                # 重複ユーザーに対して削除処理を実施
                foreach($duplicateUsers as $delUser){
                        userManagementRevokeUser($driver, $delUser);
                }
                # 待機
                usleep(SELENIUM_TUNINGWAIT_LONG);
                # ユーザー管理画面よりユーザー一覧を取得
                $users = userManagementGetUsers($driver);

                # ユーザー一覧のうち過登録を抽出
                $overUsers = array_diff($users, $groupUsersArray);

                # 過登録ユーザーに対して削除処理を実施
                foreach($overUsers as $delUser){
                        userManagementRevokeUser($driver, $delUser);
                }

                # ユーザー一覧のうち不足を抽出
                $shortUsers = array_diff($groupUsersArray, $users);

                # 不足ユーザーに対して登録処理を実施
                foreach($shortUsers as $addUser){
                        userManagementAddUser($driver, $addUser);
                }

                # 待機
                usleep(SELENIUM_TUNINGWAIT_LONG);
                # ユーザー管理画面よりユーザー一覧を取得
                $users = userManagementGetUsers($driver);

                # 比較
                if(count(array_diff($groupUsersArray, $users)) == 0){
                        break;
                }
        }
...(省略)...
?>
ポイント
  • userManagementGetUsers は操作用まとめ関数を使い、テーブルからユーザーのリストを取得します
  • findDuplicateArray は汎用のツール関数(記事記載外)を使い、テーブルの重複した値を抽出します
  • userManagementRevokeUser は操作用まとめ関数を使い、ユーザーを削除します。例えば重複ユーザーを与えると、いったんそのユーザーを全件削除します(後述の足りないユーザーの再度追加でリカバリーします
  • userManagementAddUser により足りないユーザーを再度追加します。
結果

重複ユーザーを削除する処理、過登録を削除する処理、不足分を追加する処理、の3点セットを、内部データベースと装置とで一致するまでループさせることで、アカウント登録状況のズレが解消できるようになりました。
アカウント登録状況を、Webアプリ内部DBと一致させる動作となるので応用が効きます。

  • ユーザーを新規登録したとき
  • 装置を増やした際のアカウント情報流し込み
  • 何らかの事情でWebアプリと装置とでアカウント情報がズレたとき

といったケースすべて、この関数で対応可能です。


まとめ

今回は、Webブラウザでのみ設定可能な装置の管理を行う際の自動化ツールの作成を記事にしました。
また特定の条件下でユーザー登録が正しく行えない状況に対して、力技で対応した例も記事にしました。
設定はWebブラウザ利用のみ対応で、自動化にはSeleniumが必要、というのはレアケースではありますが、本記事をきっかけに興味を持ってチャレンジしていただければ幸いです。


執筆者

福田 匡志
NTTフィールドテクノ サービスマネジメント部 ネットワークサービス部門 クラウド・サーバ・アプリケーションセンタ クラウド・サーバ技術担当(技術・戦略グループ)

主な業務:
サーバの開発・構築・運用に関する、高度化・自動化に向けた技術検討・研究・導入に携わっています。

マイパーパス(モットー):
すべての人がITを活用し、楽しいデジタルライフを送ることのできる豊かな社会の実現のために働きます


参考資料・出典

本記事を執筆するにあたり、以下を参考にしました。


商標

  • Docker, Docker Compose, PHP, Selenium, Chrome(Google Chrome), Rocky Linux等、記載のサービス名・製品名は、各社・各グループ・各権利保有者の商標もしくは登録商標です

© NTT WEST, Inc.