見出し画像

【CI/CD】GitHub Actionsで定時SSHアクセスしてスクレイピング結果をメール送信

はじめに

みなさんこんにちは、ALH開発事業部のREIYAです。

今回はGitHub Actionsを使って

  • 定時で処理を起動する

  • SSHアクセスして自身のサーバ等の処理を動かす

  • 処理結果(今回はスクレイピング)をメールで送信する

といった流れを具体的にまとめていきます。
なお、記事中で使用しているレポジトリはこちらです。

また、今回作成予定の処理の流れは以下のようになっています。


GitHub Actionsとは

GitHubのレポジトリごとに、

  • 〇〇ブランチへプッシュしたら

  • PR作成したら

  • 定時

等のトリガで、処理を起動できるものです。とても便利。
ymlファイル1枚つくるだけで、1アクションつくれます。簡単!
まずはレポジトリを作成しましょう。

この私の過去記事のように、テンプレートを作っておき
テンプレートから作成すると便利です。
あらかじめ作っておいたActionsもテンプレートに入れておくとよいでしょう。
※今回は説明のために空のレポ作るところから見ていきます。
※その他、レポジトリにライセンスやガイドラインを置く方法はこちら

こんな感じで適当に作ってみましょう。

作れるとこうなります。

Actionsタブに移動して
Simple workflowのConfigureボタンを押します。

このように、デフォな内容のymlが作られます。

内容ですが

  • onパート: 起動するタイミングを指定できる

  • jobsパート: 実行する内容を書く

ですので、とりあえず以下のようにしてみましょう。

name: sample action

on:
  schedule:
    - cron: 0 0 25-30,1-2 */1 *
  workflow_dispatch:

jobs:
  getinfo:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: develop
      - name: SSH Access
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          script: |
            cd git/githubactions-test
            git pull
            python sample.py
  sendmail:
    needs: getinfo
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: develop
      - name: Setup Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.8'
          architecture: 'x64'
      - name: Display Python version
        run: |
          python -V
      - name: Install Library
        run: |
          pip install -r requirements.txt
      - name: Exec
        run: |
          python create_mail.py
      - name: Send Mail
        uses: dawidd6/action-send-mail@v3
        with:
          server_address: ${{ secrets.MAIL_HOST }}
          server_port: ${{ secrets.MAIL_PORT }}
          username: ${{ secrets.MAIL_USERNAME }}
          password: ${{ secrets.MAIL_PASSWORD }}
          subject: 🧸【実行結果】- ${{ github.repository }}
          to: ${{ secrets.MAIL_TO }}
          from: ${{ secrets.MAIL_FROM }}
          html_body: file://sendmail.html

Actions内容の解説

全部の構文は公式を見ると良いです。

起動条件

まずここ。

name: sample action

on:
  schedule:
    - cron: 0 0 25-30,1-2 */1 *
  workflow_dispatch:
  • name: 好きな名前を指定します。Actionそのものの名前になります。

  • on: schedule以外にもpushやpull_requestが選べます。

  • cron: https://crontab.guru/ などを参考に、起動時間を指定します。

  • workflow_dispatch: これをつけると、GitHubの画面から手動で実行できます。

簡単ですね。
なお時刻ですが、GitHubのNTPサーバなのかロケールなのか
タイムゾーンがUTCですので、日本時間だと+9時間になることに注意してください。

また、遅延(というかズレ)があります。体感ですが最大30分程度遅れます。
こちらにありますように、厳密なスケジュールを組みたい場合は

  • 自前のサーバでcronを使う

  • 30分程度遅れることを見越して、30分前に起動する

  • 高頻度で起動し、処理中で時間を確認する

等工夫が必要です、あくまで自動で動いてくれるありがたみがメインですね。

jobs

つぎに処理部分ですが、以下のような構成になっています。

jobs:
  getinfo:
    # 処理
  sendmail:
    # 処理

jobsの部分はそういうものなので変えないでください。
getinfoやsendmailは好きに命名できます。
これらをjobと呼び、上から順番に実行されます。

step

ではjobの中身の、stepsたちをみていきましょう。まずはgetinfoの処理を見てみます。
イメージとしては、stepごとにVMが起動して処理を実行する感じ。

  getinfo:
    # 使用するVMイメージを指定。
    runs-on: ubuntu-latest
    steps:
      # これを書くと、ブランチチェックアウトしレポジトリ直下フォルダに。
      - uses: actions/checkout@v4
        with:
          ref: develop
      - name: SSH Access
        # SSH接続のためのライブラリがマケプレで公開されてますので使います
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          port: ${{ secrets.PORT }}
          username: ${{ secrets.USERNAME }}
          password: ${{ secrets.PASSWORD }}
          # サーバ接続後の処理を記載
          script: |
            cd git/githubactions-test
            git pull
            python sample.py

さて、secretsについて説明します。
GitHubではレポジトリごとにsecretsを設定でき、認証情報等を設定することで
上記のように呼び出せます。
登録はここからやるか

GitHub CLIを用いてもできます。

gh secret set HOST --body 'ホスト名'
gh secret set PORT --body '22'
gh secret set USERNAME --body 'ユーザ名'
gh secret set PASSWORD --body 'パスワード'
gh secret set MAIL_HOST --body 'smtp.gmail.com'
gh secret set MAIL_PORT --body '465'
gh secret set MAIL_USERNAME --body 'ユーザ名'
gh secret set MAIL_PASSWORD --body 'パスワード'
gh secret set MAIL_FROM --body 'test@gmail.com'
gh secret set MAIL_TO --body 'test@gmail.com,test2@gmail.com'

※GitHubCLIが未インストールの場合はbrew install等で入れる必要があります。
※通常のGitコマンドと違い、issueやPRの作成などができる系のやつです。

これにより、サーバ上にこのレポジトリをcloneしておいたとして
pullしてからsample.py実行、とできます。(sample.pyは別途作ってください。)

サンプルのレポジトリ内では

  • seleniumでスクレイピング

  • 取得した値をスプレッドシートに書き込み

みたいなことをしています。

# サンプルです

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from google.oauth2.service_account import Credentials
import gspread
import datetime
from time import sleep

def getDriver():
    options = Options()
    options.add_argument("start-maximized")
    options.add_argument("enable-automation")
    options.add_argument("disable-infobars")
    options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-gpu")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-extensions")
    options.add_argument("--disable-browser-side-navigation")
    options.add_argument('--ignore-certificate-errors')
    options.add_argument('--ignore-ssl-errors')
    prefs = {"profile.default_content_setting_values.notifications" : 2}
    options.add_experimental_option("prefs",prefs)
    driver = webdriver.Chrome(options=options)
    driver.set_window_size(1980, 1040)
    return driver

# URLにアクセス
driver = getDriver()
driver.get("https://sample.sample.com/sample")
sleep(3)

# なんかID パスワードいれてログインボタン押すとする
driver.find_element(By.CSS_SELECTOR, "#sample-id").send_keys("sample")
driver.find_element(By.CSS_SELECTOR, "#sample-password").send_keys("sample")
driver.find_element(By.CSS_SELECTOR, "#sample-btn").click()
sleep(3)

# なんか値取得してみる
target = driver.find_element(By.CSS_SELECTOR, "#sample-selector")
txt = target.get_attribute("textContent")
result = target.text
driver.quit()


def getSpreadSheet(sheet_key):
    scopes = [
        'https://www.googleapis.com/auth/spreadsheets',
        'https://www.googleapis.com/auth/drive'
    ]
    # サンプルなので動きません
    credentials = Credentials.from_service_account_file('サービスアカウントキー.json', scopes=scopes)
    gc = gspread.authorize(credentials)
    sh = gc.open_by_key(sheet_key)
    return sh

# スプレッドシートに書き込んでおく
key = "XXX-SPREAD-SHEET-KEY-SAMPLE"
book = getSpreadSheet(key)
sheet = book.worksheet("sample-sheet")
val_a1 = int(sheet.acell('A1').value or 0)
val_a1 += 1
print(val_a1)
sheet.update_acell('A2', datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
sheet.update_acell('A3', str(result))

メール送信

メール送信のためのほうのstepを見てみましょう。さきほどはサーバ上でスクリプトを実行しましたが
今回はGitHub上でPython実行するために、setup-python@v2を使用しています。
また、メール送信のためにdawidd6/action-send-mail@v3も使用します。

なお、事前準備として送信者のgmailに2段階認証とアプリパスワードの設定が必要です。
こちらの記事に丁寧にまとまっています。

  sendmail:
    # needsに指定すると、そのstepが成功していないと動かしません!
    needs: getinfo
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: develop
        # ここから、Pythonをセットアップ
      - name: Setup Python
        uses: actions/setup-python@v2
        with:
          python-version: '3.8'
          architecture: 'x64'
        # 一応バージョンを確認する。Actionsの実行結果ログで見れるよ
      - name: Display Python version
        run: |
          python -V
        # pip installのために、requirements.txtを別途作成しておく
      - name: Install Library
        run: |
          pip install -r requirements.txt
        # ここではpython実行で、
        # mail-template.htmlを元に、sendmail.htmlへ書き込みを行うだけ
      - name: Exec
        run: |
          python create_mail.py
        # メール送信処理
      - name: Send Mail
        uses: dawidd6/action-send-mail@v3
        with:
          # 前述の通り、あらかじめシークレットを設定しておきます
          server_address: ${{ secrets.MAIL_HOST }}
          server_port: ${{ secrets.MAIL_PORT }}
          username: ${{ secrets.MAIL_USERNAME }}
          password: ${{ secrets.MAIL_PASSWORD }}
          # github.repositoryなど、GitHub側で用意してくれている変数もあります。
          subject: 🧸【実行結果】- ${{ github.repository }}
          to: ${{ secrets.MAIL_TO }}
          from: ${{ secrets.MAIL_FROM }}
          # HTMLファイルを指定する場合はこう。先頭にfile://が必要です
          html_body: file://sendmail.html

さて、流れとして
mail-template.htmlというファイルにあらかじめ特定の文字を書いておき

<h3>タイトル</h3>
<h5>さぶたいとる</h5>
<ul>
    <li><div style="color: red; font-weight: bold">なんか1: TEMPLATE_1</div></li>
    <li>なんか2: <span style="background-color: yellow">TEMPLATE_2</span></li>
    <li>なんか3: TEMPLATE_3</li>
</ul>
<br />
<h5>さぶたいとる2</h5>

この文字を設定したい値で置換します。よく企業システムのメール送信処理でやるやつの、簡易版ですね

from google.oauth2.service_account import Credentials
import gspread
import datetime

def getSpreadSheet(sheet_key):
    scopes = [
        'https://www.googleapis.com/auth/spreadsheets',
        'https://www.googleapis.com/auth/drive'
    ]
    # サンプルなので動きません
    credentials = Credentials.from_service_account_file('サービスアカウントキー.json', scopes=scopes)
    gc = gspread.authorize(credentials)
    sh = gc.open_by_key(sheet_key)
    return sh

# シートから、スクレイピングした結果を取得する
key = "XXX-SPREAD-SHEET-KEY-SAMPLE"
book = getSpreadSheet(key)
sheet = book.worksheet("sample-sheet")
val = sheet.acell('A3').value or 'no data'

# メールテンプレートを読み込む
with open('mail-template.html', 'r') as f:
    data_lines = f.read()

# 文字列置換で、変数をセットする
data_lines = data_lines.replace("TEMPLATE_1", val)
data_lines = data_lines.replace("TEMPLATE_2", datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S"))
data_lines = data_lines.replace("TEMPLATE_3", "なんか")

# 送る用ファイル sendmail に書き込む
with open('sendmail.html', 'w') as f:
    f.write(data_lines)

これにより、GitHubActionsのVM起動中の、今のステップ「sendmail」でのみsendmail.htmlファイルに値が入ります。
勝手に変更がコミットされたりはしません。

なお、GitHubActionsからコミットしたり
GitHubCLIでやるようなissue作成等をする場合は

ここのWorkflow permissionsRead and write permissionsにする必要があります。
デフォルトだと違うので注意。
ワークフロー内でのみ権限を与えることもできます。

コミットさせるなら

name: コミットさせる

on: # なにか

# ここで、本ワークフローに書き込み権限を与えることもできる
permissions:
  contents: write

jobs:
  commit_bot:
    name: コミット
    runs-on: ubuntu-latest
    steps:
      - name: コミット
        # これが必要!
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git config --local user.email "github-actions[bot]@users.noreply.github.com"
          git config --local user.name "github-actions[bot]"
          git add --all
          git commit -m "GitHubActionsからコミット"
          git push

とすればよいです。

Action結果バッチ

Action結果のバッチをREADMEにつけてあげましょう。

ここからコピーしてもいいですし

https://github.com/ユーザ名/レポジトリ名/actions/workflows/ymlファイル名/badge.svg?branch=ブランチ名

です。ブランチも指定できるので注意。未指定の場合はデフォルトブランチになります。
ので、押下時にリンクさせるように、READMEに

[![sample action](https://github.com/serna37/githubactions-test/actions/workflows/blank.yml/badge.svg?branch=main)]([https://github.com/serna37/salary-notificator/actions/workflows/cron.yml](https://github.com/serna37/githubactions-test/actions/workflows/blank.yml))

のようにつけると

このようになります。実行結果次第でこのように変化します

最後に

自前サーバのcronを汚したくなかったりすることがあると思います。
または、レポジトリごとにスケジュールされてればよく、crontabでの一元管理が不要な時もあります。

GitHubを用いて

  • 定時で処理を起動する

  • SSHアクセスする (これは本来あるべきCDですね)

  • メール送信する

ができるため、CI/CD以外での使い道もあるのではないかと思います。

またなんどもレポジトリにプッシュして動作確認するのが面倒である場合、ローカルでGitHubActionsの動作確認ができるactというものもあります。

いかがでしたでしょうか?ちなみに私は毎月の通知系がWebアプリで来る場合
こういった処理で自動化しています。

今回紹介したものは以下で公開しておりますので、ご自由にお使いください。

ALHについて知る



↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓


↓ ↓ ↓ コーポレートサイトはこちら ↓ ↓ ↓


↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓