【selenium】便利になりゃなんでもいいのか！それしかないのか！アホか...

というわけで

# Develop funとはなんなのか
もちろん開発を楽しむことだと思いますが、楽しい開発って何だっていうことを考えると「自分自身が課題を抱えているときに、自分の意志でそれにとりくむ」開発だと思います。

そういう意味で、今回のタスクは上司から降りてきたわけでも誰かが困っていたわけでもなく、僕自身が困っていて、僕自身が楽になるために自主的に取り組んだタスクです。
内容はDevelop funを伝えるためのものではありませんが、僕にとって間違いなくDevelop funなタスクだったので、ご紹介させていただきます。

#　むかしむかし...
ある会社には、顧客環境で作業を行う際に `「作業報告」`をweb上で提出する文化があったそうな。
以前は客先に行って都度人間が柔軟に作業をおこなっていたわけじゃが、近年では[Infrastructure as Code（IaC）][1]の重要性は今更語るまでもなく、様々な作業が自動化されていっておる。
そんな中、この作業報告を提出するというのはいまだに人間が手でやっておったので、`作業者は毎回内容は同じで作業対象環境が違う作業報告を書いて提出し、ジョブをたたいて成功したら「作業完了しました」だけ書いて終了報告を提出する`という作業を行っておったのよ。

# そんな中、ある男が立ちあがる...
その男、データ移送作業を生業とする、未熟なSREであった。
日に2,3社を対象とした作業、その作業は大半が自動化済みである。
男は思った...
- 「作業報告の中身変わらないのに何で毎回手で書かないといけないのか」
- 「それが面倒で作業報告が適当な内容で提出される問題もおきてるし」
- 「作業報告2,3枚も書いてそれと紐づけてジョブ実行すると、普通に取り違えて紐づけちゃったりするの面倒」
- 「というか`どの環境に対して作業を実行するかはジョブに入れたパラメータが正で、作業報告が承認されないとジョブは先に進まないんだから、作業報告を書くのはどう考えてもジョブ実行後かつ作業報告確認前だろ`」
- 「しかも`作業内容はジョブに依存するんだから作業報告内容はジョブ実行時に自動で書かれたほうが間違いない`じゃん」

# しかし、自動化は簡単ではなかった
・作業報告はweb上でフォームに内容を入力して提出する
・`作業報告を作成するためのシステムはAPIを提供していなかった`
・APIの提供についとうて直接相談するも、「対応はする、対応はするがいつまでに対応するとは言っていない」という利根○ライクな回答（機能要望は多いが工数が少ないとこなので仕方ない

「あかん、これ待ってても楽にならない奴や...」
そう考えた男は...

# 悪魔の力に手を染める
上役は言う
「いや、そんな強引にやってもメンテナンスも大変になりそうだしやめたほうがいいよ」

同期の友人も言う
「さすがにそれは力業すぎで草」
「なにそれかっこ悪い」
「理想的じゃないよね」
「場当たり的な対応すぎでは」

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/4a9a45e3-81e0-24ce-8c08-77e591356c08.png)


*いかにも聞こえのよいお言葉。*
*美辞麗句で飾り立てちゃあいるが、つまるところ工数改善たるプロセス開発。*
*その最たる存在意義は――――――*
___「俺の作業が楽になること」___ *だ*

自動化の自動化たる所以。
オペレーションミスの排除という副産物、
IaCとしての存在意義...

便利になりゃなんでもいいのか！
それしかないのか！

アホか...

***「便利になりゃいいんだよ」（ボソッ***

作業が楽になる
手順書に沿った手動作業、イライラしながらただ同じ作業を繰り返す日々
***てめぇだけが鮮やかにワンクリックで作業を終える。***

定型業務での有効性
こいつに尽きる！

つきましては我々...
###[selenium][2]を導入しますッッッ！！！！！！

#悪魔の力、selenium
seleniumとは即ち、ブラウザ操作の自動化である。
HTML要素を相対パスや絶対パスで指定し、クリックしたり文字を入力したりする。
つまるところ、それだけ。
主に自動テストなどに用いられる技術である。

つまり、コードでブラウザ上での操作を全部書いてしまうということなのだが、ブラウザ上でというところが問題。
ブラウザで操作するあらゆる作業に対応できる反面、当然システムが更新され使用しているHTMLタグが変わっただけでも動かなくなる諸刃の剣。
正に力業、最終手段、悪魔の力である。

#基本構成
まず基本的に作業はjenkinsjobをたたくというの慣習だったので、たたくとDockerレジストリに突っ込んでおいたselenium,Chromiumの実行環境をつかって作業報告システムから作業報告を起票し、そのURLを吐き出すというジョブを作った。

ついでにユーザーからjenkinsまでの経路がhttpだったが、作業報告システムを実行する上でそちらの認証情報も必要だったので、httpsにして攻撃への耐性を高めた。

jenkinsだとジョブのリビルドををされることで、別人に成りすまし手作業報告を起票できるという問題があったため、jenkinsの実行ユーザーのもつ情報と作業報告システムの認証情報を照らし合わせて問題がないかチェックする実装を行った。
こちらはJenkinsの[プラグイン][3]を使用することで簡単に実装できた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/540428/d844159e-7731-da68-7b4d-499ba7aae810.png)

#DockerImage
実行環境の中身は以下
pythonで実装しているので、alpineを使用しており、中身は主にChromiumとselenium、その周辺のモジュールになっている。
Chromiumとdriverのversionをそろえないと動かないことに注意。

~~~sh
FROM python:3.7.4-alpine3.10
RUN apk update \
    && apk --no-cache add ttf-freefont chromium=73.0.3683.103-r0 chromium-chromedriver=73.0.3683.103-r0 curl jq \
    && curl -O https://noto-website.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip \
    && mkdir -p /usr/share/fonts/opentype/noto \
    && unzip NotoSansCJKjp-hinted.zip -d /usr/share/fonts/opentype/noto/ \
    && chmod 644 /usr/share/fonts/opentype/noto/*.otf \
    && rm NotoSansCJKjp-hinted.zip \
    && fc-cache -fv
RUN pip install selenium requests pyyaml
~~~

#selenium記述例
※実際のコードから切り出してるので無駄インポートがある点は気にしない

~~~python
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import Select
from selenium.webdriver.common.alert import Alert
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

options = Options()
options.add_argument('--headless')# Run Chrome in headless mode
options.add_argument('--disable-gpu')# Flag required temporarily
options.add_argument("--no-sandbox")
options.add_argument("--disable-setuid-sandbox")
driver = webdriver.Chrome(options=options)
driver.implicitly_wait(60)

_CLICK = "arguments[0].click();"# .click()でも動くが、画面内に表示されない要素をクリックするためにこちらを使用。

class StartOperationAtsupport():
    def login(self):#作業報告システムへのログイン
        try:
            driver.get(_SUPPORT_LOGIN_URL)
            WebDriverWait(driver, 30).until(EC.presence_of_all_elements_located)
            self.w = driver.execute_script('return document.body.scrollWidth')# 画面のサイズを取得して全画面スクリーンショットを取る
            self.h = driver.execute_script('return document.body.scrollHeight')
            assert '@SUPPORT' in driver.title
            driver.find_element_by_name('userId').send_keys(_SUPPORT_ID)
            driver.find_element_by_name('password').send_keys(_SUPPORT_PW)
            driver.execute_script(_CLICK,driver.find_element_by_class_name('btnLogin'))
            driver.execute_script(_CLICK,driver.find_element_by_id(_ENV_DICT[_SUPPORT_ENV]["env"]))
            driver.execute_script(_CLICK,driver.find_element_by_id(_ENV_DICT[_SUPPORT_ENV]["div"]))
        except Exception:
            traceback.print_exc()
            self.getScreenShot(driver,self.w,self.h)
            driver.quit()
            print('Error: login @support faild', file=sys.stderr)
            sys.exit(1)

    def getScreenShot(self,driver,w,h):# 全画面スクリーンショット
        try:
            Alert(driver).accept()# アラートが出てるとスクショ出来ないので消す処理を入れておく。
        except:
            pass
        driver.set_window_size(w, h)
        driver.save_screenshot('screen.png')
~~~
【ポイント解説】
やってることはブラウザでサイトを開いて、HTML要素で対象を指定してクリックしたり入力したりしているだけ。

`driver.get`でURLを指定してサイトにアクセスし、以降HTML要素にアクセスする場合このdriverに対して命令していく。

ブラウザ上の要素を指定して操作していくので`WebDriverWait`などで全要素が読み込み終わるまで待たないと、表示前の要素にアクセスできずにエラーになる。

`driver.find_element_by_`でタグや名前、パスを指定して要素にアクセスする。
このとき、`.send`とすればフォームに値を入力するし、`.click`なら要素をクリックするという感じ。

画面に映り切らずにクリックできないなどの場合、要素までスクロールするという手もあるが、
`driver.execute_script`をつかってjavascriptでクリックするという裏技もある。

エラーが起きてスクリプトが終了した場合でもdriverはサイトを開きっぱなしにするので、スクリプトの終了時やエラー時は必ず`driver.quit()`すること。
また、バックグラウンドで処理する場合はエラーが起きた際どこが問題だったのか読み取るのに苦労するため、`driver.save_screenshot`で画像を保存しておくのが良い。

これ以外にも複数ウィンドウ開く場合は`driver.switch_to.window(driver.window_handles[1])`でどのウィンドウを参照するか切り替えないと要素をクリックできないだとか、WebDriverWait(driver, 30).until(lambda d: len(d.window_handles) > winNum)のように事前にウィンドウの数をとっておいて増えたかどうかで待機するとかいろいろ注意点はあるが、だいたいこんな感じで実装できる。

#その結果...
結局1年たってもAPIは実装されず、この間ジョブはノーメンテナンスで動き続け現時点で実行された回数

実に***400回以上***ッ！

1枚書くのに4分かけていたとするなら1600分で26時間、開発チケット2,3枚倒せる工数改善に成功した。
もしAPIの実装を待っていれば、まる26時間の工数を追加で払ったうえでこの先の工数も追加でかかっているということである。
一方、不格好でもとりあえず工数改善さえしてしまえば浮いた時間で[エンジェル道場][5]に参加して2タイトルダブル1位の成果をたたきだせちゃったりするのだ。（これについてはまた別で記事をあげます）

ここから学べることは、結局理想的で美しい実装は保守性を上げるが、保守性がわるくなろうと動けば工数は改善されるんだから差し引き+ならやってヨシッ！
そして工数改善はいち早く実現することが何より重要ッ！
実装の改善は浮いた工数でやればいい。
だいたいやめるだけならちょっとコードなおすだけでいいわけだし、100点じゃなくてもとりあえず改善しちゃってから考えてよいのだ。
結局待ってても状況は良くならないし、工数もストレスも改善されないのだ。

皆さんも、どうしても耐えられない時は***悪魔の力に頼るといいですよ***。

おしまい。

#追記

このジョブ、悪魔の力で僕しか保守できないと思ってたんですが、やっぱり便利だったので運用に乗ったうえ、一番このジョブを使う他のチームの方が保守も含めてもっていってくれました。
やったぜ。

あーくまのちーからー　みーにーつーけたー♪...

[1]:https://ja.wikipedia.org/wiki/Infrastructure_as_Code#:~:text=Infrastructure%20as%20Code%EF%BC%88IaC%EF%BC%89%20%E3%81%AF,%E4%BF%9D%E6%8C%81%E3%81%99%E3%82%8B%E3%81%93%E3%81%A8%E3%82%82%E3%81%82%E3%82%8B%E3%80%82
[2]:https://www.selenium.dev/documentation/ja/
[3]:https://plugins.jenkins.io/build-user-vars-plugin
[4]:https://qiita.com/advent-calendar/2020/whi
[5]:https://aws.amazon.com/jp/blogs/psa/angel-dojo-season2-2020/