Toil削減取り組みしてみた　~マネージドサービス活用とスクラッチ開発でSREs体感


# はじめに
GW中の肩慣らしとしてこんな感じなものを作ってみた。
ちなみにGW中と言っといてつまずいたポイントがあり結果としてGW明けに投稿となってしまった。。
トラシューも含めてまとめてみたが長くなったのでお時間ある方はぜひ

目的と得られるスキルは下記の通り

__目的__
業務でJIRAを利用しているが、セキュリティ観点から定期的なパスワード変更が必要であり、地味に面倒くさくなっている。(地味にがポイント)
また５月からスポット案件として久々にコーディングする機会をいただいたため久々にと言うことでPythonで色々スクラッチで開発と

__スキル__
・今回はAWSのマネージドをフル活用すべく下記を利用した

:::note info
　・EventBridge
　・EC2
　・SNS
:::
　*EC2に関しては賛否あるが結果としてEC2を利用して正解だった(後述記載)

## 要件を詰める

簡単ではあるが、以下の要件とこうなったらいいなみたいのをまずリスト化してみた。その要件に対して要件を満たす条件を当てはめてみた。

:::note warn
・JIRAを利用しているが、パスワードを月１で変更したいため、月１で自動で変更したい。(現在は月初にパスワードを変更するのを脳内再生している)
:::

⇨ pythonで実装してみる

:::note warn
・月初に一回パスワードが変更できれば良いので、それ以外の用途は今の所なし
なので処理が完了すれば、EC2はOFFして節約したい。
:::

⇨ EventBridgeを利用して電源ON/OFF自動化

:::note warn
・利用しているJIRAはセキュアな環境のため外部アクセスはVPNを利用してアクセスをする。GUIではCisco Any Connectを利用してアクセスをして行なっている。もちろんVPNにアクセスする部分も含めて人の介在なしでアクセスしたい。
:::

⇨ Cisco Any ConnectのCLIはどうやらMac向けに提供されているようだがLinux向けには提供されていないようだったので、互換性OSSであるOpenConnectを利用してみることにした(結果として苦しむ形に)

:::note warn
・ちゃんとパスワード変更とかされたら結果をメールで報告欲しい(ほうれん草をシステムに求める人)
:::

⇨SNSを利用する。boto3利用

## 全体構成
要件の結果を踏まえて下記の構成

![OpenConnect.drawio2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187693/f199c828-8071-8cb1-fb7e-ef96dc62cf77.png)
図1:全体構成(AWS)

__全体ロジックは下記の通り__
1.EventBridegeから停止しているEC2に対して月初に起動させる
2.EC2起動後に、VPN接続するスクリプトを起動し、VPN接続を行う
3.パスワード自動変更ツールが起動し、結果をSNSを通して対象のユーザに通知する
4.ツールの処理よりEC2を停止させる

## OpenConnect導入
下記の記事を参考にして導入

https://qiita.com/kitara/items/90ec0956a10e5e21b8f8

テストがてら実際コマンドを叩いて利用して疎通確認して上手くいったが

OpenConnectの仕様として接続中は下記のように常時ターミナルを開いてないと利用できなく、やや不便な面があった。
``` open connect.
hogehoge@XXXXXX.com$ sudo openconnect  XXXXXXXXXX                                                                
POST ~~ (略)

Established DTLS connection (using GnuTLS). Ciphersuite (DTLS1.2)-(RSA)-(AES-256-GCM).
（これで閉じると、接続終了する)

```

helpを見てみると
オプション、-bを用いるとバックグランドで利用できるようだった。

>Process control:
  -b, --background                Continue in background after startup
      --pid-file=PIDFILE          Write the daemon's PID to this file

(pidが発行されてる)
``` open connect.
hogehoge@XXXXXX.com$ sudo openconnect -b https://XXXXXXXXX 
POST ~~ (略)
Username:otaku
Password:
POST XXXXXXXXXXXXXXX
Got CONNECT response: HTTP/1.1 200 OK
CSTP connected. DPD 600, Keepalive 15
Connected as XXXXXXXXXXXXXXX, using SSL, with DTLS in progress
Continuing in background; pid 7006
hogehoge@XXXXXXXXXXXXXXX.com:~/XXXXXXXXXXXXXXX $ 
Established DTLS connection (using GnuTLS). Ciphersuite (DTLS1.2)-(RSA)-(AES-256-GCM).
hogehoge@XXXXXX.com $
hogehoge@XXXXXX.com $ ps au |grep 7006
root      7006  0.0  1.0 219828 10788 pts/5    S    22:22   0:00 openconnect -b XXXXXXXXXXXXXXXXXXX

```

実はこれが実装する上で地獄のループに陥るとはここでは思いもしなかった・・・(ちなみにコマンド実行後に&をつけてもユーザ名入れると動かずに失敗する)

## JIRA-APIの利用
公式リファレンスが下記で提供されているので利用する。ちなみにトークン発行してアクセスはなぜか上手くいかなった。やり方おかしいかな。仕方なくBasic認証でやり過ごした。

https://docs.atlassian.com/software/jira/docs/api/REST/8.16.2/#api/2/user-findBulkAssignableUsers

## EventBridge
電源ONやスクリプト実行に関してはEventBrideを利用して自動化を実現した。メンテナンスウインドウも視野に入れたが、EventBrideがなんとなく楽に感じたので今回採用した。
下記参照

https://www.d-make.co.jp/blog/2021/11/20/ec2-cron-to-eventbridge-rule/

こんな感じでcron設定をした

![スクリーンショット 2022-05-15 1.44.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187693/2406e796-8f1c-39fd-0b74-b18faa48b738.png)


## SNS
boto3を導入してSNSを送信する実装を行なった。下記参照

https://dev.classmethod.jp/articles/publish_to_sns_using_boto3/

*ちなみにboto3経由でSNS送信すると、Subjectが日本語でも可能になる(⇦これは大きい)

## プログラム設計(詳細)
今回は、ここ一年まともに手を動かさず怠けていた功罪からGWを契機に実際コーディングしてみようがテーマなので久々に。

下記のようなロジック構成
やりたいことはとてもシンプルなのにこんな複雑になるとは思いもしなかった・・・

![program1.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187693/c1d9e433-0b85-2195-a09e-d6955f973dda.png)
図2:スクリプト構成全体構成(ポンチ)

序盤で展開した内容を引用して当てはめると
>全体ロジックは下記の通り
>1.EventBridegeから停止しているEC2に対して月初に起動させる
>>2.EC2起動後に、VPN接続するスクリプトを起動し、VPN接続を行う
>⇨vpn_connect
>>3.パスワード自動変更ツールが起動し、結果をSNSを通して対象のユーザに通知する
>⇨main_scripts,sns_action
>>4.ツールの処理よりEC2を停止させる
>⇨main_scripts

__１.OpenConnect接続部品(vpn_connect)__
　上記で説明した通り、オプションを付与した場合バックグランド処理が可能になるが、コード内に組み込むと、結果としてなぜかバックグランド処理がされず(なぜかプロセスが消える。多分スクリプトファイルが終了したのと同時にプロセスもkillされるのかな。)、結果として一つのスクリプトとして実装した。

*一部抜粋

```vpn_connet.py
import pexpect

~(略)

expect_origin = pexpect.spawn(openconnect_cmd)
expect_origin.expect (r"\r\nUsername:")
expect_origin.sendline (user_connect)
time.sleep(2)
expect_origin.expect (r"Password:*")
expect_origin.sendline (user_passeword)
expect_origin.interact()
```
対話を求められるため、pexepectを利用して実現している

__2.パスワード変更部品(main_scripts)__
　主なメイン処理。下記のフローに沿って動く
　2-1.VPN接続がされていることをチェック
　2-2.API接続ができるかを確認
　2-3.パスワード実行
　2-4.変更後に正常性確認(2-2と同等)

*一部抜粋

```main_scripts.py
import json
import requests
import subprocess

from   subprocess import getoutput
from   urllib3.exceptions import InsecureRequestWarning

~(略)

def vpn_pid_chk():
    
    out_put = int(getoutput(PARAM.VPN_CONNECT_PID_CMD))
    return out_put

def jira_api_chk(*data_set):

    api_url = f"https://{PARAM.JIRA_API_URL_BASE}/{PARAM.JIRA_API_CHK}"

~(略)
    response = requests.get(api_url,data=None,auth=(user, password), headers=headers_api, verify=False)

    api_chk_return = response.status_code == requests.codes.ok
    
    return api_chk_return

~(略)

```
なるべく、パラメータの読み込みを行いmainスクリプトにベタに書き込まないように工夫。requestsはget,putどれにも対応できるからほんと楽。ちなみにトークン経由で実行したがなぜかうまくいかなかった。
requestの具体的な使い方は下記を参照する

https://requests-docs-ja.readthedocs.io/en/latest/

ちなみに外部モジュールを利用しないとコマンドラインで処理する羽目になるのでいかに楽なのか。(この変数をgetoutputに引数として受け渡し)
> cmd_put = "curl -X PUT -H \"Content-Type:application/json\" -d \'{0}\' -u \"{1}:{2}\" \"{3}\"".format(data_refacter,user,password,put_api)

余談だが、略式で記載しているが所々、try文は入れている。

__3.SNS送信部品(sns_action)__
　2で実行された結果に応じて、指定したメールに飛ばす処理を記載。
　下記のような形で送信される
　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/187693/f4cb309a-deb1-0396-c9dd-7a9f2056eef5.jpeg" width="50%">

　2の段階的な処理に応じてメールを飛ばすようなロジックを組んでいる。
　例：VPN接続が行っていない場合
　　　⇨接続されていないことをメールで飛ばす。

__4.コンフィグパラメータ部品(parameter)__
　パラメータ値などは基本こちらに記載。Classで定義化しており、適宜読み込まれる。例えば、SNS送信する際のSubjectや、VPN接続情報などなど・・。好みもあるがパラメータ値はメインで稼働しているスクリプトにベタに書くよりも別途まとめた方が圧倒的に綺麗。(適宜追加も容易)

*一部抜粋

``` param_class.py
class Const:

    def __init__(self):

~(略)

        # API URL
        self.JIRA_API_URL_BASE = "XXXXX/XXXX"

~(略)

```


## なんでEC2なの
元々は月1だけ動かしたいのでLambdaを検討してたが、前回Lambda使ったし今回は別の感じでやってみようかなっていうのが単純な理由だが結果して、OpenConnectの仕様からEC2にして正解だった。コンテナ利用は逆に値段が高そうだったので除外。おそらくだが、全てのマネージドサービス利用しても **月1$** もかかってなさそう。

## つまずきポイント1-SNS送信
boto3導入後に試しにやってみたが下記のエラーが発生
>NoRegionError~

明示的にリージョンを指定することで解決
>boto3.client('sns', region_name='ap-northeast-1')

## つまずきポイント2-API接続がうまくいかない
PythonでAPI接続を利用する場合は外部モジュールのrequestsが大活躍するわけだが、どう頑張っても下記エラーが発生した

>{"errorMessages":["内部サーバー エラー"],"errors":{}}

実を言うとこれに **大半の時間が取られてしまった。** 外部モジュールの利用が悪いのかとか、色々試したら、パスワード部分に問題があって解決した。(内部サーバエラーって出られてもわけわかんない・・・・もうちょっとエラー出してくれと。結果として向こうに問題あったじゃんとかとか。。)

## 最後に
自動化する上で、EventBridgeの利用
メールを送信する上で、SNSの利用
オンプレで上記を実現するには別のOSSを利用してさらにスクラッチで実装したりとハードルが高かったが随分楽だなって思った次第。特にSNSは感動した・・！あと、ちょいちょい手を動かさないとやっぱテクニカルなスキルは劣化する一方だなって身にしみて感じた。では素敵なSREsライフを