— Web – koneta https://koneta.click DIYとデジモノとプログラミングとライフハックをコネた...小ネタ Sun, 13 Mar 2022 11:52:36 +0000 ja hourly 1 https://wordpress.org/?v=6.1 https://koneta.click/wp-content/uploads/2020/02/cropped-icon-32x32.png — Web – koneta https://koneta.click 32 32 HTMLとCSSでサムネイルジェネレータを作ってみました https://koneta.click/p/1028 https://koneta.click/p/1028#respond Sun, 13 Mar 2022 11:52:35 +0000 https://koneta.click/?p=1028 どうも、繁忙期をなんとか乗り越え本ブログを更新できる時間が戻ってきました。しかし、またいつ時間が取れなくなるかはわかりません。そこで、せめてブログの更新時間が減るようにサムネイルを簡単に作れる仕組みを作ってみました。色々作り方はあると思いますが、今回はHTMLとCSSでサムネイルのフォーマットを作り、それを画像化するという方針です。

どんなのができる?

本ブログでは主に2種類のフォーマットでサムネイルを作っているのですが、その中でも一番使っているものを生成できるようにしてみました。

やっていることは、背景画像を半透明の黒で暗くして、そこに半透明の文字抜きの白をのせています。

できたもの

というわけで、このような物ができました。

動作画面イメージ

背景画像の項目で端末内の画像を選択すると背景画像として表示され、その下のタイトルの欄に文字列を入力するとタイトルとして表示されます。基本的に誰にも公開する気はなかったので見た目としては全く手を入れてません。

ちょっとだけコード詳細

大したことはしていませんが、少しだけどんなコードかを書いておこうと思います。

コード全体
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>サムネイルジェネレータ</title>
  <link rel="stylesheet" href="./resource/ress.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.9.0/html-to-image.min.js"></script>
</head>
<body>
  <div class="container">
    <h1>koneta.click サムネイルジェネレータ</h1>

    <div id="thumbnail-background" class="thumbnail">
      <div class="thumbnail__background-black__wrapper"></div>
      <!-- 半透明のテキストエリア テキストはエリアに高さを持たせるためのダミー -->
      <div class="thumbanail__title-dummy__area">
        <p id="p-title-dummy" class="thumbanail__title-dummy__text">タイトルサンプル</p>
      </div>
      <!-- タイトル文字に背景が透過しているように見せるための要素 -->
      <div id="thumbnail-background-dummy" class="thumbanail__title__area">
        <p id="p-title" class="thumbanail__title__text">タイトルサンプル</p>
      </div>
    </div>

    <div class="contoroler">
      <h2>操作</h2>
      <h3>背景画像</h3>
      <input type="file" id="form-thumbnail-background">
      <h3>タイトル(HTML記法でOK)</h3>
      <textarea id="form-text-title">タイトルサンプル</textarea>
      <h3>保存</h3>
      <button id="button-save">画像にする</button>
      <div class="controler__result-img__wrapper">
        <img id="result-image" />
      </div>
    </div>
  </div>

  <script>
    // 背景画像用処理
    const fileInput = document.getElementById('form-thumbnail-background')
    fileInput.addEventListener('change', () => {
      const reader = new FileReader()
      // ファイルが読み込まれたときに実行する
      reader.onload = function (e) {
        const imageUrl = e.target.result
        const thumbnailBackground = document.getElementById("thumbnail-background")
        thumbnailBackground.style.backgroundImage = `url(${imageUrl})`

        const thumbnailBackgroundDummy = document.getElementById("thumbnail-background-dummy")
        thumbnailBackgroundDummy.style.backgroundImage = `url(${imageUrl})`
      }
      reader.readAsDataURL(fileInput.files[0])
    })

    // タイトル用処理
    const textInput = document.getElementById('form-text-title')
    textInput.addEventListener('change', () => {
      // 高さ産出用と背景を見せる用の要素にテキストを設定する
      document.getElementById('p-title-dummy').innerHTML = textInput.value
      document.getElementById('p-title').innerHTML = textInput.value
    })

    // 画像化関係処理
    const saveButton = document.getElementById('button-save')
    saveButton.addEventListener('click', () => {
      const thumbnail = document.getElementById('thumbnail-background')
      htmlToImage.toJpeg(thumbnail)
        .then(function (dataUrl) {
          document.getElementById('result-image').src = dataUrl
        })
        .catch(function (error) {
          console.error('oops, something went wrong!', error);
        });
    })
  </script>
  <style>
    @font-face {
      font-family: 'NikumaruFont';
      src: url(./resource/nikumaru.otf);
    }
    .container {
      width: 100%;
      max-width: 1000px;
      margin: 0 auto;
    }
    .thumbnail {
      width: 100%;
      aspect-ratio: 16 / 9;
      position: relative;
      background-color: #000000;
      background-size: cover;
      background-position: center;
    }
    .thumbnail__background-black__wrapper {
      width: 100%;
      height: 100%;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 10;
      background-color: rgba(0, 0, 0, 0.3);
    }
    .thumbanail__title-dummy__area {
      width: 80%;
      padding: 3% 0;
      position: absolute;
      top: 50%;
      left: 50%;
      z-index: 20;
      transform: translate(-50%, -50%);
      background: rgba(255, 255, 255, 0.9);
      outline: 6px solid rgba(255, 255, 255, 0.9);
      outline-offset: 4px;
      text-align: center;
    }
    .thumbanail__title__area {
      width: 100%;
      height: 100%;
      display: flex;
      justify-content: center;
      align-items: center;
      text-align: center;
      position: absolute;
      top: 0;
      left: 0;
      z-index: 30;
      background-size: cover;
      background-position: center;
      background-clip: text;
      -webkit-background-clip: text;
    }
    .thumbanail__title-dummy__text {
      font-size: 3rem;
      font-family: NikumaruFont;
      color: transparent;
    }
    .thumbanail__title__text {
      font-size: 3rem;
      font-family: NikumaruFont;
      color: rgba(0, 0, 0, 0.3);
    }
    .controler__result-img__wrapper {
      width: 100%;
    }
    .controler__result-img__wrapper img {
      width: 100%;
    }
  </style>
</body>
</html>

今回のコードではサムネイルで使っているフォントやcssのリセットを読み込んでいますが、それらはライセンスの関係もありここでは載せていません。フォントはこちらのにくまるフォント、リセットにはress.cssを使っています。もし、そのまま当サイトと同じフォーマットのサムネイルを作りたい場合は上記の2つをresourseディレクトリに設置してからアクセスしてみてください。

ちなみにフォントやサムネイルの画像素材など、本サイトで使わせてもらっている素材たちはこちらのページにまとめていますので、気になった方はこちらをご覧ください。

要所としては入力された画像やタイトルをサムネイルに反映する部分だと思います。値が入力されたときそれを画面に反映しようと思ったとき、最近ではVueなどのフレームワークがうかぶかもしれませんが、今回のツールくらいであれば直書きで十分です。

まずは画像が選択されたときの処理です。

const fileInput = document.getElementById('form-thumbnail-background')
fileInput.addEventListener('change', () => {
  const reader = new FileReader()
  // ファイルが読み込まれたときに実行する
  reader.onload = function (e) {
    const imageUrl = e.target.result
    // 選択された画像を要素の背景に設定する
    const thumbnailBackground = document.getElementById("thumbnail-background")
    thumbnailBackground.style.backgroundImage = `url(${imageUrl})`
  }
  reader.readAsDataURL(fileInput.files[0])
})

これで画像読み込みはオッケーです。次にテキスト部分は以下のようになります。

const textInput = document.getElementById('form-text-title')
textInput.addEventListener('change', () => {
  document.getElementById('p-title').innerHTML = textInput.value
})

テキストに関してはこれだけです。これだけの記述量ですみ、環境構築も不要な点でもフレームワークは不要だと思います。これでサムネイル画像完成です。

もしこのコードから改造を行う場合は、サムネイルのフォーマットを作っているcssと上記のjsで変更する対象を調整すればいいと思います。

終わりに

もともとサムネイルを作る際はGIMPというフリーソフトを使っており、作業時間は30分ほどかかっていました。それがこのジェネレータを使うことで5分程度でサムネイルを作れるようになりました。また、webページとして作成したため、作業できる端末がGIMPをインストールしているデスクトップやMacBookに限られていたところを、主にブログ更新に使っているChromebookでもできるようになったのが大きいところです。

実はボタンを押すだけで画像を生成するところまで実装しようと思っていたのですが、どうやらいま出ているライブラリでは対応していないcssを使ってしまっているためうまく生成してくれませんでした。そのため今はフォーマット完成後スクリーンショットを撮るという微妙な形になっています。そのうち時間ができたらここらもどうにかしていきたいと思います。

あとは他のサムネイルもサクッと作れるように更新していこうと思います。

]]>
https://koneta.click/p/1028/feed 0
無料でSupabaseのバックアップを取得してみました https://koneta.click/p/1018 https://koneta.click/p/1018#respond Fri, 11 Mar 2022 12:47:40 +0000 https://koneta.click/?p=1018 こんにちは、Supabaseって便利ですよね。Firebaseの代替を狙っているだけあり、無料でDBや会員管理、ストレージなどFirebaseにも負けず便利です。ほとんどの場合は無料で十分だと思います。

しかし、無料で1つだけ物足りない点があります。それがDBのバックアップです。残念ながらバックアップ機能は有料で月$25以上を払わないといけません。そこで今回はPythonでSupabaseのバックアップ/リストアを行うスクリプトを書いたので記事にまとめておこうと思います。

私の用途ではそのうち容量も足りなくなる見込みなので有料プランに変更する見込みなので、みなさんも一時的なものとして見てみてください。

環境準備

本記事ではバックアップの処理をPythonで書くので、その環境を用意します。今回はDocker上のPythonで雑に用意しようと思います。ちなみにここで書くDocker設定は他のスクリプトを書いたときのものを流用しているため、不要な設定などがあるかもしれないので参考までにご覧ください。

というわけでDockerに必要なファイルを用意します。ディレクトリの連携も雑に対応していくためdocker-compose.ymlDockerfileそしてPythonでインストールするライブラリを記述するrequirements.txtを用意します。正直なところ必要なライブラリはSupabaseだけなのでテキストファイルを用意するっ必要もないですが、おまけです。

version: '3'
services:
  python:
    container_name:  supabase-python-backup
    build: .
    volumes:
      - .:/workspace
    tty: true
FROM python:3.9.10

RUN apt-get update

WORKDIR /workspace
COPY ./requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
supabase==0.3.0

ちなみにSupabaseのPython向けライブラリは、現在まだベータ版とのことなので、新しいバージョンが出た際は指定バージョンを変更するようにしてください。

これらと次に紹介するスクリプトを同じディレクトリに設置して下準備完了です。今回はDocker環境で動かすためにこのようにしましたが、supabaseのライブラリが動けば何でも大丈夫です(直接叩けばライブラリすら不要です)。

APIキーを準備

次にSupabaseの操作を行うためのAPIキーを取得します。 Supabaesの管理画面にログインしてメニューのSettings→APIの項目からProject API keysのservice_roleConfigurationのURLをメモしておいてください。

APIキーの位置

スクリプトの用意

さて、次は本題のスクリプトです。今回は雑にクラスにまとめているのでそれだけ掲載しておこうと思います。git?知らない子ですね。

import os
import shutil
import pickle
from supabase import create_client, Client

os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'

class supabaseBackup():
  def __init__(self, supabase_api_url:str, supabase_api_key:str):
    self._supabase: Client = create_client(supabase_api_url, supabase_api_key)

    # エクスポート出力先ディレクトリ
    self._FILE_DIR = './backup'
    
    # Supabaseで設定している1回のクエリの取得件数上限
    self._MAX_ROWS = 1000


  def _save(self, table_name:str, data:any):
    """ データをファイル出力する
    Args:
      table_name (str): 出力するテーブル名
      data (any): 出力するデータ
    """
    with open(f'{self._FILE_DIR}/backup-{table_name}.pkl', 'wb') as f:
      pickle.dump(data, f)

  def _load(self, table_name:str):
    """ ファイルからテーブルのデータをインポートする
    Args:
      table_name (str): インポートするテーブル名
    Return:
      any: ファイルから読み込んだデータ
    """
    with open(f'{self._FILE_DIR}/backup-{table_name}.pkl', 'rb') as f:
      return pickle.load(f)


  def copy_to_date_directory(self):
    """ ディレクトリに保存されているpklファイルを日付ディレクトリに移動する
    """
    import datetime
    date = datetime.datetime.today().strftime("%Y%m%d%H%M%S")
    dir = f'{self._FILE_DIR}/{date}'

    os.makedirs(dir, exist_ok=True)

    # 既存の pkl ファイルを日付ディレクトリにコピーする
    import glob
    for p in glob.glob(f'{self._FILE_DIR}/*.pkl'):
        shutil.copy(p, dir)

  def supabase_backup_table(self, table_name:str):
    """ Supabase の指定テーブルのデータをすべてファイルに出力する
    Args:
      table_name (str): 出力するテーブル名
    """
    i = 0
    rows = []
    while True:
      offset = self._MAX_ROWS * i
      data = self._supabase\
                 .table(table_name)\
                 .select('*')\
                 .limit(1000, start=offset)\
                 .execute()\
                 .data
      if len(data) == 0:
        break
      else:
        i += 1
        rows += data

    self._save(table_name, rows)

  def supabase_restore_table(self, table_name:str, import_to_table_name:str = ''):
    """ Supabase の指定テーブルにファイルからデータをインポートする
    Args:
      table_name (str): インポートするテーブル名
      import_to_table_name (str): インポート先のテーブル名(ステージング環境テーブルにインポートする場合に使用する)
    """
    if import_to_table_name == '':
      import_to_table_name = table_name

    import_data = self._load(table_name)

    for i in range(0, len(import_data), self._MAX_ROWS):
      self._supabase.table(import_to_table_name).insert(import_data[i:i+self._MAX_ROWS]).execute()

  def supabase_clean(self, table_name:str):
    """ Supabase の指定テーブルのデータを空にする
    Args:
      table_name (str): データを空にするテーブル名
    """
    self._supabase.table(table_name).delete().execute()

さて、コードを書いてしまったのでこれ以上書くことはないのですが、実装時に少しハマったところは取得件数上限が1000件なので、それ以上にレコードがある場合はそれだけクエリを実行しなくてはいけないという点です。しかし、クエリ結果のDLは制限なしというのは嬉しいですね。

使ってみる

最後にこのスクリプトを使ってみようと思います。上記でクラスの定義はできているので、使うだけです。

if __name__ == '__main__':
  # Supabase
  SUPABASE_API_URL: str = 'TODO Supabase_URL https://xxxxxxxxxxxxxxxxxxxxxxxxx.supabase.co'
  SUPABASE_API_KEY: str = 'TODO SUpabase_KEY xxxxxxxxxxxxxxxxxxx......xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

  backup = supabaseBackup(SUPABASE_API_URL, SUPABASE_API_KEY)


  # エクスポート
  backup.supabase_backup_table('TODO エクスポートするテーブル名')

  # 保存されているバックアップファイルを日付付きのディレクトリにコピー
  backup.copy_to_date_directory()

  # ステージング環境のテーブルをクリア
  backup.supabase_clean('TODO インポートするテーブル名')

  # ステージング環境にリストア
  backup.supabase_restore_table('TODO インポートするテーブル名', 'TODO インポート先のテーブル名')
$ docker exec -it supabase-python-backup python main.py

このスクリプトで、SupabaseのDBからデータを全件エクスポートして、インポートするという一連の流れが実行されます。設定値をベタ書きするなとか、バックアップファイルの扱い方とかツッコミ所はありますが、私の要望は満たしてくれているので一旦おいておきます。

終わりに

というわけで、無料プランのままバックアップを取る方法を書いてみました。もうDBの容量が8割くらいまで行っているのですぐ有料バックアップを使えるようになりますが、無料で使えるうちは使い倒してやりますよ!

]]>
https://koneta.click/p/1018/feed 0
スプレッドシート+GAS+GCPで簡易的なSSL証明書期限監視システムを構築してみました https://koneta.click/p/990 https://koneta.click/p/990#respond Sat, 15 Jan 2022 12:33:19 +0000 https://koneta.click/?p=990 昨今はLet’sさんやらホスティングサービスで勝手に更新してくれることの多いSSL証明書ですが、意外と手動で更新しなくてはいけないこともあります。

そして、更新ができないとサイトにアクセス時にエラーが表示されてしまい良くないです。

そこで、ちゃんと証明書の更新ができているかを定期的に確認する必要があるのですが、手動ではどうしても忘れやすく、とてもめんどくさいです。

というわけで、 今回は簡易的に証明書の有効期限を確認する仕組みを構築してみようと思います。今回はGCPのCloud RunでfastAPIの処理を動かし、監視するサイトリストをスプレッドシートで管理できるように作ってみようと思います。

指定サイト証明書の有効期限を返すAPIを作成する

まずは指定されたサイトURLの証明書有効期限を返すAPIを作成します。

この工程ではGCPのCloud Run上に確認用の処理をデプロイします。基本的には無料で何とかなる範囲ですがクレジットカードなど課金情報の登録が必要になります。もし課金情報の入力が嫌な場合は、有志が作成しているAPIを使ってみたり、各サイトの証明書期限を手動で入力するようにすれば、この工程は不要になります。

さて、では早速やっていきたいと思います。今回は必要最低限のところだけ書いていこうと思います。

処理の準備

まずはDockerfileの準備です。基本的に動作に必要なものがインストールできれば問題ないのですが、CloudRunの仕様上8080ポートでアクセスできるようにする必要があります。また今回はpipでインストールものをまとめておけるようにrequirements.txtに記述して一括でインストールするようにしておきました。

FROM python:3.7.12

COPY ./apps /apps
WORKDIR /apps

COPY ./requirements.txt ./
RUN pip install -r requirements.txt

CMD ["uvicorn", "main:app", "--reload", "--host", "0.0.0.0", "--port", "8080"]
fastapi==0.63.0
uvicorn==0.13.3
pyOpenSSL==21.0.0
pytz==2018.9

次にfastAPIの処理を実装していきます。今回はfastAPIについては全く触れず処理のみ掲載します。とりあえずコピペしていただければ動くと思います…。

#coding: utf-8
import uvicorn
from fastapi import FastAPI
from routers import CertificateExpiration

app = FastAPI()

@app.get("/")
def get_root():
    return {"message": "Hello World"}

app.include_router(CertificateExpiration.router)
#coding: utf-8
from urllib.parse import urlparse
from fastapi import APIRouter
from pydantic import BaseModel
import datetime
import pytz
import OpenSSL
import ssl

router = APIRouter()
    

@router.get("/certificate-expiration")
async def get_certificate_expiration(url: str):
    try:
        # URLからドメインを抜き出す
        parsed_url = urlparse(url)
        domain = parsed_url.netloc

        cert = ssl.get_server_certificate((domain, 443))
        x509 = OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
        expiration = x509.get_notAfter()

        # 表記を調整
        expiration = expiration.decode('utf-8')
        expiration = f'{expiration[0:4]}-{expiration[4:6]}-{expiration[6:8]}'

        return {"expiration": expiration}
    except:
        # エラーが発生したので今日の日付を返す
        return {"expiration": datetime.datetime.now(pytz.timezone('Asia/Tokyo')).strftime("%Y-%m-%d")}

ファイル構造はこのようになります。

/
│  Dockerfile
│  requirements.txt
│
└─ /apps
   │  main.py
   └─ /routers
            CertificateExpiration.py

今回やることだけであれば、main.pyCertificateExpiration.pyを分ける必要もないのですが、将来的にこのシステムにほかのAPIも追加していきたいと考えているため、今後処理を追加しやすいように分割して実装しています。

Cloud Runのプロジェクトを作成

処理が書けたので次はデプロイ先であるCloudRunの準備を進めます。まずはCloudSDKのインストールです。OSによって方法がちょっと違うので、公式のマニュアルを参考にしてください。

インストールができたらGCPの管理画面へアクセスし、プロジェクトを作成します。

手順としては管理画面にログインすると画面上部のプルダウン(下記画像内では「プロジェクトの選択」)があり、その中の「新しいプロジェクト」から作成できます。作成時にプロジェクIDが表示されているのでこれをメモしておいてください。

画面イメージ

初めてCloudRunを使う場合は決済情報を登録する必要がります。これはハンバーガーメニューの「お支払い」から登録することができます。これでGCP管理画面の操作はおしまいです。

次に端末から下記のコマンドを実行しプロジェクトを選択しておきます。

$ gcloud init

あとは画面に表示される項目に合わせて登録していけば準備完了です。

デプロイ

プロジェクトの準備ができたのでいよいよデプロイです。デプロイは下記のコマンドで実行できます。プロジェクトIDの部分は、上記手順で確認したそれぞれのIDに書き換えてください。

$ gcloud builds submit \
     --tag gcr.io/[プロジェクトID]/backend-app
$ gcloud run deploy [プロジェクトID] \
     --image gcr.io/[プロジェクトID]/backend-app \
     --region asia-northeast1 \
     --platform managed \
     --allow-unauthenticated

これらのコマンドはシェルなどにまとめておくと今後デプロイするときの役に立つのでオススメです。ステージング環境にアップロードしたり自動でトラフィックの切り替えたくない場合は適宜書き換えちゃってください。

監視するサイトリストを作成する

さて、下準備は完了したので次はスプレッドシート上での作業になります。いつも通りにシートを作成してください。シートの内容は以下の通りです。証明書期限の項目は処理で自動入力されるため、事前に必要なのはサイト名とURL、通知フラグ、そして式を残日数に入力します。ちなみに通知フラグのチェックボックスはメニューの挿入→チェックボックスから入れることができます。

シート入力イメージ

残日数に入力する式は以下のようにD列の日付から証明書の期限が切れる残日数を計算します。おまけで証明書期限が入力されていない場合は空白になるようにしています。

=IF(D3="","",(IFERROR(DATEDIF(TODAY(), D3,"D"),0)))

もしシートの構造を変えたい場合はこちらの式を次に紹介する監視処理の値を修正してください。

サイトリストの期限を確認する処理を実装する

サイト監視リストができたので最後に監視を行う処理を用意して作業完了です。監視にはGASを使います。画面上メニューの拡張機能からApps Scriptを選んでください。入力する処理は以下の通りです。2箇所通知の送信先メールアドレスと上記で作成したAPIのURLはそれぞれの環境に合わせて書き換えてください。


// 通知先のメールアドレス
const NOTIFICATION_TO_ADDRESS = "[送信先メールアドレス]"

// API URL
const API_URL = '[API URL]'

// 通知を送信する残日数リスト
// ※ 最小の日付からは毎日通知する
const NOTIFICATION_EXPIRATION_DATE_LIST = [7, 30, 45];
const MIN_EXPIRATION = Math.min.apply(null, NOTIFICATION_EXPIRATION_DATE_LIST);

const ROW_ID_TABLE_START = 2; // サイトリストが始まる行数

const COLUMN_ID_SITE_NAME  = 1; // サイト名を掲載している列番号
const COLUMN_ID_SITE_URL   = 2; // サイトURLを掲載している列番号
const COLUMN_ID_EXPIRATION = 4; // 有効期限残日を算出している列番号
const COLUMN_ID_SEND_FLG   = 5; // 送信フラグを指定している列番号

/* 証明書の有効期限を確認する
 *
 */
function checkCertificateExpiration(url) {
  const requestUrl = `${API_URL}?url=${url}`
  const response = UrlFetchApp.fetch(requestUrl)
  const responseCode = response.getResponseCode()
  const responseText = response.getContentText()

  if (responseCode === 200) {
    const responseTextObject = JSON.parse(responseText); 
    return responseTextObject['expiration'];
  } else {
    // 有効期限が取得できなかったので今日の日付を入れておく
    return Utilities.formatDate( date, 'Asia/Tokyo', 'yyyy-MM-dd'); 
  }
}


/* 通知する
 * @param array 行の内容
 */
function sendNotifiication(rows) {
  notificationText = "";
  for (let i in rows) {
    const row = rows[i];
    notificationText += `サイト名: ${row[COLUMN_ID_SITE_NAME]} 残日数: ${row[COLUMN_ID_EXPIRATION]}日\nURL: ${row[COLUMN_ID_SITE_URL]}\n`;
  }

  const subject = "【重要】 SSL証明書失効警告";
  const body = `SSL証明書失効期限が迫っています!\n\n${notificationText}`;

  MailApp.sendEmail(NOTIFICATION_TO_ADDRESS, subject, body, {
    noReply: true
  });
}

/* リストの残日数を確認して指定された日付だった場合は通知する
 *
 */
function checkExpiredDate() {
  const sheet = SpreadsheetApp.getActiveSheet();
  const rows = sheet.getDataRange();
  const numRows = rows.getNumRows();
  let values = rows.getValues();

  //  証明書の有効期限を更新する
  for (let i = ROW_ID_TABLE_START; i <= numRows - 1; i++) {
    const row = values[i];

    // 通知送信フラグがONのもののみ有効期限を更新する
    if (row[COLUMN_ID_SEND_FLG]) {
      const certificateExpiration = checkCertificateExpiration(row[COLUMN_ID_SITE_URL]);
      sheet.getRange(i+1, COLUMN_ID_EXPIRATION).setValue(certificateExpiration);
    } else {
      sheet.getRange(i+1, COLUMN_ID_EXPIRATION).setValue('');
    }
  }

  // 有効期限を更新したので残日数を再計算する
  SpreadsheetApp.flush();
  values = rows.getValues();


  // 送信するサイトの列情報リスト
  let notificationRows = [];

  for (let i = ROW_ID_TABLE_START; i <= numRows - 1; i++) {
    const row = values[i];

    // 通知送信フラグがONのもののみ判定する
    if (row[COLUMN_ID_SEND_FLG]) {
      if (NOTIFICATION_EXPIRATION_DATE_LIST.indexOf(row[COLUMN_ID_EXPIRATION]) !== -1) {
        // 指定日数の場合は通知リストに追加する
        notificationRows.push(row)
      } else if (row[COLUMN_ID_EXPIRATION] <= MIN_EXPIRATION) {
        // 指定日数の最小日からは毎日通知リストに追加する
        notificationRows.push(row)
      }
    }
  }

  // 通知対象のサイトがある場合は通知を行う
  if (notificationRows.length > 0) {
    sendNotifiication(notificationRows);
  }
}

実行時にはcheckExpiredDate関数を選択して実行ボタンを押します。初回はスプレッドシートにアクセスする権限やメールを送信する権限など許可が求められます。許可画面では一度警告画面が表示されますが、詳細からすすめることができます。

最後に通知確認のため、証明書の期限残にあわせてNOTIFICATION_EXPIRATION_DATE_LISTで設定している日数を調整して実行してみてください。「【重要】 SSL証明書失効警告」というメールが送信されていれば作業完了です。

終わりに

というわけで今回は、ほぼスプレッドシートで証明書監視機能を構築してみました。これまでGASはなんとなくダサい感じがしていて食わず嫌いをしていたのですが、今回触ってみてそのお手軽さにハマってしまいそうです。今後も色々小さいアプリを作って行きたいと思います。

]]>
https://koneta.click/p/990/feed 0
GoogleスプレッドシートをCMS&APIとして使ってみました https://koneta.click/p/894 https://koneta.click/p/894#respond Sat, 27 Nov 2021 12:32:38 +0000 https://koneta.click/?p=894 今、ちょっとしたWebサービスを構築しているのですが、その中でちょっとしたお知らせ機能を実装したくなり、NEWS機能を実装することになりました。しかし他の機能も実装している中、NEWSの登録機能まで作るのは大変なので、どうやったら簡単にデータを用意できるかを考えてみました。

このような場合、最近であればmicroCMSなどのヘッドレスCMSなどを使うのがあるあるになるかと思います。しかし今回はとっても小規模なので、それすらオーバースペックな気がしてしまいました。そこで今回はGoogleさんのスプレッドシートにNEWS内容を入力して、それをAPIから取得する方針でやっていきたいと思います。

スプレッドシートを用意する

まずはNEWSを入力するシートを用意します。シート自体はいつも通り作成して、下記の画像のように必要な情報を記入していきます。

記事サンプル

今回は、NEWSの日時とタイトル、本文を入力してみました。もし他にもNEWSのカテゴリだったり筆者だったり必要な情報がある場合はその分だけカラムを追加していけば大丈夫です。

データの準備ができたら、次の手順のためにシートの名前とドキュメントのIDを確認しておきます。シートの名前は画面下から確認できます。この名前は自分で好きなものに変更することができるため、わかりやすい名前にしておくといいと思います。また、ドキュメントのIDはURLから確認できます。https://docs.google.com/spreadsheets/d/[ここがドキュメントのIDです]/edit#gid=0 このIDをメモしておいてください。

スクリプトを用意

データの準備ができたら次にスプレッドシート側でスプレッドシートの内容をAPIとして呼び出せるようにスクリプトを用意します。

上記で用意したスプレッドシートのメニューから「拡張機能」→「Apps Script」と選択していきます。そして表示されたエディタに下記のコードを入力します。1行だけ上で確認したシート名とドキュメントIDに書き換えてください。

function getData(id, sheetName) {
  var sheet = SpreadsheetApp.openById(id).getSheetByName(sheetName);
  var rows = sheet.getDataRange().getValues();
  var keys = rows.splice(0, 1)[0];
  return rows.map(function(row) {
    var obj = {}
    row.map(function(item, index) {
      obj[keys[index]] = item;
    });
    return obj;
  });
}

function doGet(e) {
  var data = getData('[※※※ドキュメントID※※※]', '[※※※シート名※※※]');
  var output = ContentService.createTextOutput(JSON.stringify(data, null, 2));
  output.setMimeType(ContentService.MimeType.TEXT);
  return output;
}

入力出来たら一度手動で実行してみます。まずはプルダウンから実行売る関数を選択します。今回はdoGetを選択します。

関数選択中

最後に、プルダウンの左にある実行を押します。初回はGoogleアカウントの承認が必要になります。「このアプリはGoogleで確認されていません」と表示されますが、自分で作成しているアプリなので「詳細」から進んじゃってください。これでスクリプトが実行されているはずです。「お知らせ 実行完了」と表示されていれば大丈夫かと思います。

正常に実行されていない場合は、ドキュメントIDやシート名があっているか、実行している関数がdoGetになっているかを確認してみてください。

最後にこのスクリプトに外部からアクセスできるようデプロイします。デプロイは画面右上の「デプロイ」ボタンからできます。このボタンから歯車マークの「ウェブアプリ」を選択します。

デプロイ層あ

あとは必要な情報を入力します。今回はサイトのフロントエンドから呼ばれるため「アクセスできるユーザー」は「全員」を選択します。これでデプロイは完了です。最後にAPIにアクセスするためのURLが発行されているので、あとはここにアクセスするだけでデータを取得することができます。

APIとして呼び出してみる

最後に用意できたNEWS取得APIからデータを取得しています。まずは上記のスクリプトを用意したときに発行されたURLに直接アクセスしてみます。データが取得できていれば作業完了です。

今回、私はNuxtJSでサイトを構築していますが、おまけとしてコンポーネントからaxiosで呼び出しているところも書いておきます。

<template>
  <v-row>
    <v-col v-if="isLoadedNews" cols="12">
      <h3>NEWS</h3>
    </v-col>
    <v-expansion-panels accordion>
      <v-expansion-panel v-for="(news, index) in newsList" :key="index">
        <v-expansion-panel-header>
          {{ news.date }} - {{ news.title }}
        </v-expansion-panel-header>
        <v-expansion-panel-content>
          <p style="white-space: pre-wrap" v-text="news.body"></p>
        </v-expansion-panel-content>
      </v-expansion-panel>
    </v-expansion-panels>
  </v-row>
</template>
<script>
export default {
  props: {
    isDisplayLoading: {
      type: Boolean,
    },
  },
  data: () => ({
    newsList: [],
  }),
  computed: {
    isLoadedNews() {
      return this.newsList.length > 0
    },
  },
  async mounted() {
    await this.$nextTick(async () => {
      if (this.isDisplayLoading) this.$nuxt.$loading.start()
      await this.$axios.get(this.$NEWS_API.url).then((response) => {
        this.newsList = response.data
      })
      if (this.isDisplayLoading) this.$nuxt.$loading.finish()
    })
  },
}
</script>

Vuetifyが入っていたりローディング用の変数があったりと関係ない部分がありますが、取得部分はawait this.$axios.get(this.$NEWS_API.url).then でデータを持ってくることができています。

サイトの様子

というわけで、APIからデータを簡単に取得することができました!

おわりに

さて、つれずれなるままに書いてみました。今回は超最低限のCMSとしてスプレッドシートを使ってみました。なんといっても簡単にNEWS記事を管理することができ、APIから簡単に取得までできお手軽でした。最低限機能としては優秀だったかと思います。需要を満たしていればこれだけでも十分という場面は結構ありそうです。

ただ、今回紹介した方法だけでは画像やリンクが張れなかったり、そもそもデータ取得のレスポンスが遅かったりと気になる点はけっこうあります。なので今回の方法は「あくまでこんな方法もあるらしいですぜ」という感じでお願いします!

]]>
https://koneta.click/p/894/feed 0
ワークフローツールn8nで超簡易的死活監視を構築してみました https://koneta.click/p/765 https://koneta.click/p/765#respond Sat, 29 May 2021 15:43:33 +0000 https://koneta.click/?p=765 前にオープンソースのワークフローツールであるn8nをDockerで簡単に構築するという記事を書きましたが、今回はその活用法の一つとして、簡易的なWebサイトの死活監視&Slack通知のツールとして使ってみたいと思います。

通常であれば、専用のツール環境を構築したり、小難しいスクリプトを書かなくてはいけませんが、今回の方法ではGUIでポチポチするだけでおおよそ動いてくれます。

下準備

まずは下準備です。大前提としてn8nは構築済みということで…。もしやってみたいけどまだ構築していないという場合は私の記事でも参考にしてもらえると幸いです。

さて、n8nは準備できたので、次にSlackに投稿するためのアクセストークンを取得します。今回は監視結果をSlackに送りたいのでSlackのトークンを取得しますが、メールで送信する場合や別のサービスを利用する場合はそれぞれの方法に合わせたトークンなりを用意しておいてください。

トークン取得は本題ではないのでサラッと行きます。

  1. SlackにログインしつつSlackAPIの管理画面にアクセスする。
  2. 右上の「Your Apps (作成されたアプリ)」→「Create an App (新しいアプリを作成する)」をクリックする。
  3. アプリの名称、ワークスペースを選択して「Create App」をクリックする。
  4. 作成したアプリの設定画面左側の「Basic Information (基本情報)」から「Add features and functionality (特徴と機能の追加)」内の「Permissions (権限)」を選択する。
  5. 「Add an OAuth Scope」のボタンをクリックして chat:writechat:write.customizeの権限を追加する。
  6. 画面左メニューの「App Home」で遷移した後、「Your App’s Presence in Slack (Slack へのログイン状態)」のEditボタンをクリックする。
  7. ボットの名前とユーザ名を入力してAddボタンをクリックする。
  8. 左側メニューの「OAuth & Permissions (OAuth & 権限)」をクリックする。
  9. 「Install App to Workspace」をクリックして、権限内容を確認したのち「許可する」をクリックして有効化する。
  10. アクセストークンが発行される。

上記の手順でアクセストークンを取得することができます。発効後再度確認したくなった場合は、左側メニューの「OAuth & Permissions (OAuth & 権限)」から確認することができます。子のトークンが外部に漏れると自由に投稿されてしまうので注意です。

これで下準備はできたのであとはn8nで設定していくだけです。

ワークフロー作成

準備ができたので、さっそくワークフローを作成していきます。今回の完成図は以下のような形です。

完成図

今回はn8nからHTTPリクエストを飛ばして想定通りのテキストが返ってくるかを確認して、想定通りなら何もしない。想定のデータが返ってこない場合はSlackに通知を飛ばす。というワークフローを作ってみます。

本記事では「右上のプラスボタンからノードを追加して矢印で繋げることでフローを作成する」のような基本的なn8nの操作は省略していきます。前回記事ではちょっと詳しめに書いているので、基本操作を把握していない方はご覧になってください。

1. 定期実行

まずはワークフローの開始地点を設定します。ワークフロー作成時にはデフォルトで「Start」というノードが登録されていますが、こちらは自分で動かしたときに「ここからはじまるよ」というノードです。今回は手動ではなく一定の時間で自動的に動いてほしいので、「Cron」のトリガーノードを追加します。

cron設定画面

今回は1時間に1度実行してみようと思いますので、ノードの設定から「Mode」は「Every Hour」で「Minute」は0で設定します。「Mode」は毎時以外にも毎分, 毎日, 毎週, カスタムもありますのでご自身の環境/要望に合わせて調整しててみてください。

2. HTTPリクエスト

次は監視のメイン部分となるHTTPリクエストです。リクエストするにはその名の通り「HTTP Request」ノードを選択します。設定画面が出てきたら、URL欄に監視したいURLを入力します。あとはレスポンスの形式に合わせて「Response Format」の項目を「JSON」や「String」などに設定します。

リクエスト設定画面

3. リクエスト結果で条件分岐

さてリクエストはできたので、取得した結果をもとに必要があれば通知を送ってみたいと思います。初見だと、ちょっと設定しにくいと思いますが、一回やってみるとかなりシンプルなので初回は頑張ってみてください。

条件分岐には「IF」ノードを使用します (もしTrue/Falseより多くの分岐がしたければ「Switch」ノードが使えます)。この条件分岐ノードで想定内のデータが取れていればOK。取れていなければNGでSlack通知に進むというようにフローを組めば大丈夫です。

実際に条件分岐ノードを設定してみようと思います。今回は2パターンを考えてみます。下記の設定をする前に一度フローを実行しておいてください。実行しておくことで下記の条件設定時に選択肢として各レスポンスデータを選択できるようになります。

Webページ向けレスポンスがHTML形式(String)

まずは単純にWebページに対してリクエストした場合です。この場合ではレスポンスで想定されるテキストが含まれているかで判定するのがいいと思います。今回はレスポンスで返ってくるHTML内に「Hello World」が含まれていれば正常という想定で設定してみます。

IF設定画面 HTML ver

これを設定するには、まず「IF」ノードの設定画面上の「Value1」で、前のノードから取得したHTMLデータを選択します。HTMLデータを選択するには、入力欄右にある歯車マークから「Add Expression」を選択し、そこから「Nodes」→「HTTP Request」→「Output Data」→「JSON」→「Data」を選択します。もし選択肢として出てこない場合は、フローを実行するかフロー自体がつながっているかを確認してみてください。

HTTPRequestの結果対象に指定している様子

「Value1」が選択できたら、「Operation」は「Contains」、「Value2」は調べたい文字列「Hello World」と入力します。

これでレスポンスデータに「Hello World」が含まれているか?の条件分岐ができるようになりました。

API的レスポンスがjson形式(JSON)

もう一つのパターンとしてはAPIへリクエストした場合でjson形式のレスポンスが返ってくる場合の設定方法です。こちらも基本的に想定されるデータと一致しているかで判定を行います。今回はシンプルにレスポンスとして下記のようなjsonが返ってくると想定します。

[
    {
        "Hello": "World"
    }
]

つまり、レスポンスデータ内の「Hello」項目が「World」で返ってくれば正常という判断になります。これをn8nで設定するには、まず「Value1」の項目に「Hello」。「Operation」の項目は「Equal」。そして「Value2」の項目は1つ前のHTTPリクエストの結果、つまり歯車マークから「Add Expression」を選んで「Nodes」→「HTTP Request」→「Output Data」→「JSON」→「Hello」を選択します。

IF設定画面 JSON ver

これで「Hello」項目が「World」で返ってくるか?の条件分岐ができるようになりました。

4. Slack通知

最後にSlack通知部分を作成します。通知を送るにはこれまた名前通りの「Slack」ノードを追加します。追加したノードは上記で作成した条件分岐ノードの「false」側につなげます。もし監視してOKの場合にも通知を贈りたい場合は別途「true」に設置することでOKパターンで通知を送ることもできます。今回は通知しないため「NoOp」ノードをつなげて明示的に何もしないようにしています。

「Slack」ノードを使用する前にAPI設定をしておく必要があります。これはノード設定画面の一番上の「Slack API」の鉛筆マークから設定することができます。

鉛筆マークを押した後の画面

APIの設定画面が表示されたらあとは適当な名前を付けて「Access Token」の項目に本記事初めに取得したSlackのアクセストークンを入力して完了です。ちなみにこの設定はほかのフローでも使いまわすことができ、使用したAPI設定は画面左のメニュー内「Credentials」から見ることができます。

Slack設定画面

APIの設定が終わったら「Slack」ノードの設定をしていきます。「Authentication」では「Access Token」を選択します。あとは投稿先のチャンネルを入力 (例: #notice-alert など)し、「Text」で通知したい内容を入力します。「Text」入力時欄の右側にある歯車マークから、他のノードの情報を持ってくることもできます。今回の例では「HTTP Request」のノードからリクエストを飛ばした「url」の情報をテキスト内に加えて通知しています。

終わりに

今回作成したワークフローをエクスポートしたモノを下に載せておきます。こちらをインポートして、監視対象のURLとSlackトークン指定を修正していただければ動作するはずです。

エクスポートファイル (クリックで展開)
{
  "name": "watch-example.com",
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        10,
        240
      ],
      "disabled": true
    },
    {
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "mode": "everyHour"
            }
          ]
        }
      },
      "name": "Cron",
      "type": "n8n-nodes-base.cron",
      "typeVersion": 1,
      "position": [
        150,
        240
      ]
    },
    {
      "parameters": {
        "url": "https://example.com",
        "jsonParameters": true,
        "options": {}
      },
      "name": "HTTP Request",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        300,
        240
      ]
    },
    {
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "World",
              "value2": "={{$node[\"HTTP Request\"].json[\"Hello\"]}}"
            }
          ]
        }
      },
      "name": "IF",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        450,
        240
      ]
    },
    {
      "parameters": {},
      "name": "NoOp",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        610,
        130
      ]
    },
    {
      "parameters": {
        "channel": "#notice-alert",
        "text": "=NOT WORK!!!\n{{$node[\"HTTP Request\"].parameter[\"url\"]}}\n<!channel>",
        "attachments": [],
        "otherOptions": {},
        "blocksUi": {
          "blocksValues": []
        }
      },
      "name": "Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 1,
      "position": [
        610,
        330
      ],
      "credentials": {
        "slackApi": "HOGEHOGE"
      }
    }
  ],
  "connections": {
    "Cron": {
      "main": [
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "IF",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "IF": {
      "main": [
        [
          {
            "node": "NoOp",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": true,
  "settings": {},
  "id": "1"
}

今回は簡易的な監視システムをn8nで構築してみましたが、今後もn8nの使い道を見つけたら共有していきたいと思います。

]]>
https://koneta.click/p/765/feed 0
NGINXリバースプロキシでVPS上のDockerの複数サイトを運営しますよ https://koneta.click/p/672 https://koneta.click/p/672#respond Sun, 07 Mar 2021 08:11:33 +0000 https://koneta.click/?p=672 私はこれまで、枯れた技術達しか使わず、作るサイトはすべてLAMP環境を直接構築して運営するという方針でいきてきました。

しかし、開発環境でDockerを使うようになると、本番環境でサイトごとに別の環境を構築して公開するのが億劫になってきました。そこで最近ではやっとサイトをDocker上で運営するようになりました。

ここで問題になってくるのが複数のサイトをどうやって公開すればいいのか問題になります。そこで今回はサイト達のコンテナの前段としてNGINXで構築したリバースプロキシを通すことでマルチサイトを構築する方法をまとめようと思います。

構成

設定項目を書く前に今回の構成を書いておこうと思います。

構成図

基本的にはリバースプロキシを構成する1つのDocker(docker-compose構成)とその後ろに紐づくサイト群(それぞれdocker-compose構成)という形です。

外とつながる部分はNGINXのコンテナが担い、設定したドメインごとに後ろに紐づくWebサイトのコンテナに割り振ります。加えてLet’s Encryptのコンテナを使用することで自動的にHTTPSのアクセスに対応します。

今回はこの構成をVPS(ConoHa)上のDockerで構築していきます。ちなみに、ConoHa上でのDocker構築は以前記事にしましたので、まだ構築していない方は参考にしていただけると幸いです (ConoHaのDockerイメージを使ってもらっていいと思います)。

NGINXの設定

では早速NGINXの準備をしていきます。とはいえ、今回はDocker上で構築していくので下記のファイルを作成しコンテナを実行するだけで作業完了です。

version: '3'

services:
  nginx-proxy:
    build: jwilder/nginx-proxy
    restart: on-failure
    labels:
      - com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy=jwilder/nginx-proxy
    ports:
      - 80:80
      - 443:443
    volumes:
      - proxy:/etc/nginx/vhost.d
      - proxy:/usr/share/nginx/html
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - ./certs:/etc/nginx/certs:ro
    network_mode: bridge

  letsencrypt:
    image: jrcs/letsencrypt-nginx-proxy-companion
    restart: on-failure
    depends_on:
      - nginx-proxy
    volumes:
      - proxy:/etc/nginx/vhost.d
      - proxy:/usr/share/nginx/html
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./certs:/etc/nginx/certs:rw
    network_mode: bridge

volumes:
  proxy:

作用祭な説明は省略しますが…上記の設定でリバースプロキシの設定とLet’s Encryptでの証明書取得も自動で行ってくれます。なのであとはサイトを構築しているコンテナに「どのドメインで使用するか」の設定を追加して終わりです。

設置するサイトの設定

次にNGINXで割り振るサイト側のコンテナに設定を追加していきます。とはいえ設定する項目はかなりシンプルです。

version: '3'
services:
    ...
    environment:
      VIRTUAL_HOST: [サイトのドメイン]
      LETSENCRYPT_HOST: [サイトのドメイン]
      LETSENCRYPT_EMAIL: [Let's Encryptに使うメアド]
    network_mode: bridge
    ...

上記の例のようにenvironmentnetwork_modeに項目を追加します。この設定を追加し、NGINXのコンテナを起動したあとにこのコンテナを実行すると、あとはサイトを表示するための設定と証明書の取得/設置まで自動的にやってくれます。

注意点/参考

証明書取得

初めNGINXの設定をミスしていて、コンテナの起動停止を繰り返してしまいました。すると、しばらくして証明書が正常に取得できなくなってしまいました。

原因はコンテナのエラーログを見れば一発でしたが、Let’s Encryptのリミットに到達していました。どうやらコンテナの起動時にドメインに対応する証明書を取得していて、試行錯誤中にそれを繰り返していたことでリミットに到達してしまったようです。

一度リミットにかかってしまうと、解除されるまで待つしかありません (ドメインやIPアドレスを変えるという荒業はありますが…)。というわけで私も解除されるまで1週間立ち往生しました。

NGINXコンテナの試行錯誤をする際はLet’s Encryptのコンテナは実行しないようにするといいと思います。

NGINX設定の追加

基本的にはイメージからコンテナを実行すればいい感じに動作してくれますが、サイトにより追加の設定が必要になると思います。

そんなときは設定ファイルに追加しておいてあげれば大丈夫です。私のサイトでもアップロード容量を多くしたかったので、下記のような設定を追加しています。

FROM jwilder/nginx-proxy
RUN { \
      echo 'client_max_body_size 500m;'; \
    } > /etc/nginx/conf.d/my_proxy.conf

あとはdocker-compose.ymlでのイメージ読み先をこのDockerfileにしてあげれば勝手に設定が読み込まれます。ちなみに上記の設定ではNGINXで割り振る全サイトに設定されます。個別のサイトに設定したい場合はmy_proxy.confの代わりに{設定したいドメイン}.confのファイル名で作成してあげれば大丈夫とのことです。

ここらへんの説明は使用しているイメージのGitに書かれていそうです。

コンテナの名前解決をしてくれない

上記で載せているcomposeファイルはデフォルトのnetwork_mode: bridgeを使用しているため、名前解決の機能がありません。そのためコンテナ名を使用してのアクセスができます。

そこで、自分でネットワークを作成するパターンも載せておきます。まずは共通で使用するネットワークを作成します。

$ docker network create --driver bridge common_link

次にproxy用と各サイト用両方のdocker-compose.ymlの最下部に下記のネットワーク設定を追加します。

networks:
  default:
    external:
      name: common_link

あとは通常通りに起動してみればコンテナ名で名前解決してくれるようになります。

サイトごとにBasic認証をかけたい

開発中のサイトやツール用で使用する場合はBASIC認証をかけたい場合があると思います。そんなときもパスワードファイルを作成するだけなのでとても簡単です。

まずはdocker-compose.ymlに下記の設定を追加します。ホスト側のディレクトリはどこでもいいのですが、コンテナ側のディレクトリは/etc/nginx/htpasswdに設定してください。

services:
  nginx-proxy:
    ......
    volumes:
      ......
      - ./htpasswd:/etc/nginx/htpasswd     # ← この行を追加

設定ができたらこのディレクトリにBASIC認証用のID/PWファイルを設置します。このときのファイル名はBASIC認証をかけたいドメイン名と同じにします。つまり本サイトに認証をかけるのであればkoneta.clickになり、サブドメインがある場合もそのままsubdomain.koneta.clickで作成します。作成には下記コマンドが使用できます(オンラインの生成サービスで問題ないです)。

$ htpasswd -c /path/to/htpasswd [BASIC認証 ID]
New password: [BASIC認証 PASSWORD]
Re-type new password: [もう一度]

最後にコンテナを再起動すれば作業完了です。

終わりに

というわけで簡単に書いてきました。なんと言ってもNGINXのDockerコンテナを実行するだけであとは「良しなに」やってくれるのでとてつもなく簡単でした。血反吐を吐きながらApacheの設定ファイルをこねくり回して壊しまくっていたあの頃が懐かしいです。

まぁコンテナまるごとデプロイできる環境が普通になりつつあるので、この記事のないようも古い感じもしますがそこはおいといて色々やっていきたいと思います。

]]>
https://koneta.click/p/672/feed 0
静的化したWordPressのホスティング先をNetlifyからFirebase Hostingに変更しました https://koneta.click/p/608 https://koneta.click/p/608#respond Sun, 03 Jan 2021 06:15:59 +0000 https://koneta.click/?p=608 以前、私はWordPressで構築されているサイトをNetlifyで公開するという記事を公開していました。その結果できたサイトが現在ご覧いただいているこのサイトなのです。

…しかし公開からしばらくすると、ページ表示がなんだかモッサりしているなぁと感じるようになりました。そこで今回はNetlifyからGoogleさんのところのFirebase Hostingに鞍替えし、その作業をまとめて行こうと思います。

今回の作業で公開するサイトはwgetコマンドで静的化したサイトなので…きっと簡単に再現できるはずです。もしWordPressの静的化を行うのであれば前回前々回の記事をご覧いただけると幸いです。

Netlifyが遅い問題

NetlifyはほぼGitリポジトリを選択するだけでお手軽に静的ファイルをホスティングしてくれる使いやすいサイトです。しかし、こちらの記事を参考にすると、Netlifyには日本国内の拠点がなくアクセスが海外旅行するようで、どうしてもレスポンスが遅くなるようです。その遅さは体感できるほどでした。

というわけで、今回はその遅さをどうにか解消したいと思い、Netlifyから同じく基本無料から利用することができるFirebase Hostingを試してみよう!…となったわけです。

ちなみに、実際の測定結果は本記事の最後に掲載していますので、Firebase Hostingとの比較と共に見ていただけるとです。

Firebaseプロジェクトを作成する

まずはプロジェクトを作成しておきます。Googleアカウントはほとんどの方が持っていると思いますので、早速 Firebase のコンソールページにアクセスします。プロジェクト作成自体はページの指示通りに入力していけば迷わずできるはずです。

Firebaseをインストールする

次にデプロイに使用するFirebase CLI をインストールします。インストールの方法は色々ありますが、node環境がないので今回はスタンドアロンバイナリを直接ダウンロードする方法で進めていきます。

手順としては以下のコマンドだけで導入(ダウンロード~パス通し)までできます。

$ mkdir /parh/to/firebase_dir && cd /parh/to/firebase_dir
$ wget -O firebase https://firebase.tools/bin/linux/latest
$ chmod +x ./firebase
$ echo 'export PATH=$PATH:/parh/to/firebase_dir' >> .bashrc

ちなみに他の導入方法は公式ガイドを参考にしてみてください。今回はLinux環境でしたがWindowsやMac環境用の説明も載っていますので指示通りに進めれば導入できるはずです。

Googleアカウントを紐付ける

次にGoogleアカウントとの紐付けを行います。基本的には下記のコマンドを実行して表示されたURLにアクセス、承認してあげるだけです。

$ firebase login --no-localhost
実行結果
i  Firebase optionally collects CLI usage and error reporting information to help improve our products. Data is collected in accordance with Google's privacy policy (https://policies.google.com/privacy) and is not used to identify you.

? Allow Firebase to collect CLI usage and error reporting information? Yes
i  To change your data collection preference at any time, run `firebase logout` and log in again.

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=.....

? Paste authorization code here: [認証後表示されたcodeを入力]

✔  Success! Logged in as [ログインしたGmailアカウント]@gmail.com

しかし、今回は承認後のlocalhostが正常に動作しなかったので(おそらくファイヤーウォールのせい)、今回はlocalhostを使用せず、codeを入力する方式でアカウント紐付けを行いました。このためのオプションが--no-localhostになります。

Firebaseプロジェクトの初期化

次にプロジェクトの初期設定を行います。

$ cd /path/to/web/dir
$ firebase init hosting
実行結果

     ######## #### ########  ######## ########     ###     ######  ########
     ##        ##  ##     ## ##       ##     ##  ##   ##  ##       ##
     ######    ##  ########  ######   ########  #########  ######  ######
     ##        ##  ##    ##  ##       ##     ## ##     ##       ## ##
     ##       #### ##     ## ######## ########  ##     ##  ######  ########

You're about to initialize a Firebase project in this directory:

  /path/to/web/dir/


=== Project Setup

First, let's associate this project directory with a Firebase project.
You can create multiple project aliases by running firebase use --add, 
but for now we'll just set up a default project.

? Please select an option: Use an existing project
? Select a default Firebase project for this directory: project_name (project_name)
i  Using project project_name (project_name)

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? [document_root_dir_name]
? Configure as a single-page app (rewrite all urls to /index.html)? No
? Set up automatic builds and deploys with GitHub? No
? File [document_root_dir_name]/404.html already exists. Overwrite? No
i  Skipping write of [document_root_dir_name]/404.html
? File [document_root_dir_name]/index.html already exists. Overwrite? No
i  Skipping write of [document_root_dir_name]/index.html

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...
i  Writing gitignore file to .gitignore...

✔  Firebase initialization complete!

今回は最初の手順でプロジェクトを作成済みなので、既存のプロジェクトを選択しました。また、デプロイするファイル群はWordPressから作成済みなので404.htmlindex.htmlの生成は不要、また今後のデプロイは自作のスクリプトから実施する予定なので、GitHubの項目もNoで回答していきます。

ここまでできれば準備は完了です。

デプロイする

では最後にデプロイしていきます。とはいえデプロイもコマンド1つです。

$ firebase deploy --only hosting
実行結果
=== Deploying to 'web-konetaclick'...

i  deploying hosting
i  hosting[web-konetaclick]: beginning deploy...
i  hosting[web-konetaclick]: found 1583 files in [documentroot_dir]
✔  hosting[web-konetaclick]: file upload complete
i  hosting[web-konetaclick]: finalizing version...
✔  hosting[web-konetaclick]: version finalized
i  hosting[web-konetaclick]: releasing new version...
✔  hosting[web-konetaclick]: release complete

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/[project name]/overview
Hosting URL: https://[project name].web.app

処理が終わると、ホストされたURLが発行されますので、ここから正常にデプロイされているかを確認することができます。

作業前後

最後に作業前後それぞれでのレスポンス時間を比較して終わろうと思います。冒頭でも書きましたとおり、作業前の環境はNetlifyにデプロイされています。

計測にはGo製のベンチマークツール「hey」を使用しました。お手軽簡単にベンチマークを取ることができるのでおすすめしておきます。

作業前 (Netlify)
$ hey https://koneta.click

Summary:
  Total:	2.1298 secs
  Slowest:	1.3774 secs
  Fastest:	0.1981 secs
  Average:	0.4127 secs
  Requests/sec:	93.9070
計測結果詳細
Summary:
  Total:	2.1298 secs
  Slowest:	1.3774 secs
  Fastest:	0.1981 secs
  Average:	0.4127 secs
  Requests/sec:	93.9070

Response time histogram:
  0.198 [1]	|
  0.316 [143]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.434 [2]	|■
  0.552 [0]	|
  0.670 [4]	|■
  0.788 [0]	|
  0.906 [2]	|■
  1.024 [42]	|■■■■■■■■■■■■
  1.142 [5]	|■
  1.259 [0]	|
  1.377 [1]	|


Latency distribution:
  10% in 0.2010 secs
  25% in 0.2040 secs
  50% in 0.2094 secs
  75% in 0.9042 secs
  90% in 0.9809 secs
  95% in 1.0159 secs
  99% in 1.1090 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0794 secs, 0.1981 secs, 1.3774 secs
  DNS-lookup:	0.0138 secs, 0.0000 secs, 0.0944 secs
  req write:	0.0000 secs, 0.0000 secs, 0.0003 secs
  resp wait:	0.1262 secs, 0.0957 secs, 0.3134 secs
  resp read:	0.0098 secs, 0.0003 secs, 0.4600 secs

Status code distribution:
  [200]	200 responses
作業後 (Firebase Hosting)
$ hey https://koneta.click

Summary:
  Total:	0.9511 secs
  Slowest:	0.4694 secs
  Fastest:	0.0224 secs
  Average:	0.2059 secs
  Requests/sec:	210.2747
計測結果詳細
Summary:
  Total:	0.9511 secs
  Slowest:	0.4694 secs
  Fastest:	0.0224 secs
  Average:	0.2059 secs
  Requests/sec:	210.2747
  

Response time histogram:
  0.022 [1]	|
  0.067 [0]	|
  0.112 [0]	|
  0.157 [129]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  0.201 [17]	|■■■■■
  0.246 [1]	|
  0.291 [2]	|■
  0.335 [12]	|■■■■
  0.380 [8]	|■■
  0.425 [14]	|■■■■
  0.469 [16]	|■■■■■


Latency distribution:
  10% in 0.1220 secs
  25% in 0.1397 secs
  50% in 0.1482 secs
  75% in 0.2916 secs
  90% in 0.4160 secs
  95% in 0.4473 secs
  99% in 0.4690 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0437 secs, 0.0224 secs, 0.4694 secs
  DNS-lookup:	0.0007 secs, 0.0000 secs, 0.0034 secs
  req write:	0.0000 secs, 0.0000 secs, 0.0007 secs
  resp wait:	0.1325 secs, 0.0171 secs, 0.1564 secs
  resp read:	0.0046 secs, 0.0004 secs, 0.1526 secs

Status code distribution:
  [200]	200 responses

というわけで、思っていた以上の高速化を実現することができました。今回はレスポンス時間という観点でのみの比較ですが、Firebase Hostingを利用したほうが調子が良さそうなので本格的に使っていきたいと思います。

おまけ (ConoHa1GB + WordPressそのまま)

最後におまけとして静的化前のWordPressでも調査してみようと思います。こちらは現在ConoHaの1GBプラン上で動作しています。また、これはwget用に用意しているだけなのでだいぶスペックは制限しています。そのため調査時も並列は1つで調査しています。

$ hey https://[origin domain].com -c 1

Summary:
  Total:	22.6286 secs
  Slowest:	13.3695 secs
  Fastest:	0.1897 secs
  Average:	4.7227 secs
  Requests/sec:	8.8384
計測結果詳細
Summary:
  Total:	22.6286 secs
  Slowest:	13.3695 secs
  Fastest:	0.1897 secs
  Average:	4.7227 secs
  Requests/sec:	8.8384
  

Response time histogram:
  0.190 [1]	|■
  1.508 [20]	|■■■■■■■■■■■■■■■■
  2.826 [20]	|■■■■■■■■■■■■■■■■
  4.144 [49]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  5.462 [37]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  6.780 [37]	|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
  8.098 [22]	|■■■■■■■■■■■■■■■■■■
  9.416 [5]	|■■■■
  10.734 [0]	|
  12.052 [6]	|■■■■■
  13.369 [3]	|■■


Latency distribution:
  10% in 1.1740 secs
  25% in 2.9277 secs
  50% in 4.7938 secs
  75% in 6.3292 secs
  90% in 7.7741 secs
  95% in 8.6856 secs
  99% in 12.5423 secs

Details (average, fastest, slowest):
  DNS+dialup:	0.0227 secs, 0.1897 secs, 13.3695 secs
  DNS-lookup:	0.0075 secs, 0.0000 secs, 0.0292 secs
  req write:	0.0001 secs, 0.0000 secs, 0.0005 secs
  resp wait:	3.4589 secs, 0.1632 secs, 13.3391 secs
  resp read:	0.3117 secs, 0.0180 secs, 2.8701 secs

Status code distribution:
  [200]	200 responses

Totalの項目は並列数が違うので無視するとして…その他の項目を見ると静的と比べるとだいぶ遅くなっているのが見えてきます。端末のスペックがそもそも違うので厳密ではありませんが、やはりスピード面では静的サイトに軍配が上がるようです。

参考

Firebase Hosting 公式ガイド … ほぼ ここをみるだけでこの記事は不要です。

]]>
https://koneta.click/p/608/feed 0
続・WordPressをwgetとNetlifyでお手軽に静的サイト化する話 https://koneta.click/p/44 https://koneta.click/p/44#respond Sat, 30 May 2020 17:14:00 +0000 https://koneta.click/?p=44 前回の記事では、もう更新する気のないブログを放置するという方針でWordPressの静的化しました。まだ読んでいない場合は一読お願いします。今回は更新する気バリバリ、いまご覧になっているこのサイトを静的化していきます。静的化する理由は、前回静的化して楽しかったからなのですが、WordPressを静的化することにより、ページ表示の高速化はもちろん、管理画面など動的部分と切り離すことができるので、不正アクセスなどの対策にもなります。

この記事では、静的化元のWordPressの準備からNetlifyへのデプロイまでを書いていこうと思います。この記事に書いてあることを実行することで、このサイトと同じような状況にできると思います。今回は、WordPress静的化の作業をメインに書いていこうと思いますので、Webサーバの設定方法やDNSの設定など直接関係のない部分の説明は省きますのでご了承ください。

オリジンを作成

まずはNetlifyで使用する公開用のドメインとは別に、WordPressにより動的にページを生成して静的化の元にするオリジン用のドメインを準備します。本記事では、本サイトで作業することをイメージし、公開用のドメインを「koneta.click」、オリジン用のドメインを仮に「origin.koneta.click」として書いていこうと思います。今回はサブドメインを使いますが、別のドメインを用意していただいても問題ありません。

オリジン用のドメイン(とWebサーバ)の準備ができましたら、次はWordPressの設定です。とはいえ、特別な設定は不要なので、オリジン用に準備したドメイン(本記事ではorigin.koneta.clickの方)でいつも通りWordPressの初期設定を進めてください。初期設定が終わったら、その後もいつも通りでテーマの設定や必要なプラグインのインストールなど、サイトオープンに必要な作業を行い下準備は完了です。

動的必須部分の対応

今回はサイトを静的化してしまうため、動的で動作する部分は停止しなくてはいけません。これは使用しているテーマやプラグインによるところが多いと思いますが、本記事では多くのサイトで使われているであろう項目を挙げて停止していただきたいと思います。

コメント

まずはコメント機能です。コメントの無効化は簡単で、WordPressの設定画面から記事編集時のコメント有効の設定をデフォルトで無効にできるため、こちらを修正するだけで無効になります。合わせてウィジェットなどに新規コメントなどを表示している場合は、そちらも削除すれば完了です。

本サイトでは、コメント機能は残しておきたかったので、WordPressのコメント機能とは別のDisqusというサービスを使用することで、静的化後もコメント機能を利用できるようにしています。こちらの作業については後ほど紹介します。

検索

次は検索機能です。こちらも対応は簡単でウィジェットに表示されている検索ウィンドウを表示しないようにすれば完了です。しかし、もしテーマに検索機能が組み込まれている場合は、そちらも対応が必要になりますのでご注意ください。

検索機能はGoogleカスタム検索などを活用することで、静的化後も利用することができますが、私の経験上ではサイト内の検索機能はほとんど使われず、カテゴリやタグでの絞り込みは静的化後も使えるため、本記事では検索機能はなかったモノとして進めていきます。

更新情報サービス/ディスカッション設定 など

次は投稿した際に使われる通知系です。今回はオリジン側で投稿機能を残すため、投稿時の処理は動作はするのですが、ここで通知されるのはオリジンのURLで送られてしまうため、効果はなくなってしまっています。そんなわけで、こちらも動かないようにしていいと思います。

設定方法は管理画面設定の「投稿設定」から「更新情報サービス」の項目を削除して保存、また「ディスカッション設定」から「デフォルトの投稿設定」の各種設定をOFFにすれば完了です。

その他の動的部分

先ほど書きました通り、WordPressにおいて動的部分は使用しているテーマやプラグインによってもだいぶ異なります。本サイトで使用しているテーマやプラグインは最低限の機能しかないもののため、上記の項目程度でしたが、高機能なテーマなどを使用して、何もかも動的で生成するしているような場合は、最悪の場合諦めるのも手かもしれません。

静的化する

ここまでできましたら、準備は完了です。いよいよ静的化を行います。静的化は前回の記事と同じようにwgetコマンドを使用し、コマンドのに使用している各種オプションについての説明なども書いてあるのでコマンドは前回記事を参照してください。

これで静的化は完了です。あとはNetlifyで公開するだけなのですが、その手順も前回書いたので今回は省略させていただきます。公開するときのドメインは、オリジンのドメインとは違う公開用の方のドメインで設定してください。

プラスアルファ作業

今回の静的化はwgetにより、画像など静的コンテンツやリンクはローカルだけで解決できるように自動で書き換えられていますが、meta情報などリンク以外のアドレスは変換されません。そうなるとmeta情報は公開しているドメインとは変わってしまいますし、オリジンのドメインがもろバレなので、セキュリティ面でも意味がなくなってしまいます。そこで、プラスアルファ作業として、これらを解消していこうと思います。また先ほど書いた通りコメント機能も使えるようにしてみようと思います。

オリジンを隠す

まずは静的化したファイルから管理画面を含むオリジンのドメインを公開用のドメインに変更しようと思います。変更の方法は何でも大丈夫ですが、私は以下のコマンドを使って一括で変更しようと思います。

# 隠蔽単語を置換
find $WORK_DIR$ORIGIN_DOMAIN -type f | xargs sed -i -e "s|$ORIGIN_DOMAIN|$SITE_DOMAIN|g"

コマンドは上記の通りです。これを静的化したファイルのルート部分で実行することでオリジンドメインと公開用ドメインを置換することができます (後ほど本記事で使用するコマンドをまとめますので、今は無視で大丈夫です)。

あとは、オリジンの方にアクセス制限を行います。こちらは念のための設定ですが、無駄にアクセスされないためにも設定しておいて損はないと思います。アクセス制限の方法はいくつかありますが、今回は簡単にBASIC認証でやってみました。おまけで後ほど出てくるコマンドもBASIC認証に対応させみました。

コメントフォームを追加

次はコメント機能を直していこうと思います。今回は「静的サイト コメント」で出てきたDisqusというコメントサービスを使ってみます。

テーマによって導入手順が異なるため詳細な説明は省かせていただきますが、WordPressのプラグインで導入するのではなく、HTMLタグで導入します。「Install Disqus」を押すと「WordPress」の選択肢がありますが、こちらではなく、手動で登録するため、ページ下部にある「I don’t see my platform listed, install manually with Universal Code」からコメントフォームを表示するHTMLタグを取得できます。

このタグで使用しているテーマのコメント表示部分(おそらくcomments.phpの中)の<?php if (comments_open()) : ?>~~~<?php endif; ?>内を書き換えてあげるだけでコメントフォームを設置することができます。

<?php do_action('bp_before_commentpost'); ?>

<div id="commentpost">

<?php if (comments_open()) : ?>
    [取得したHTMLタグ]
<?php endif; ?>

</div>
<?php do_action('bp_after_commentpost'); ?>

完成イメージは上記の通りです。これでコメントフォームが表示されれば完了です。

自動でコミットするようにする

今のままでは、記事などを更新した後は手動で静的化を行い、NetlifyでのデプロイをするためGitに変更を登録する作業が必要になります。しかし、それは限りなくめんどくさいので…最後に、Netlifyへのデプロイを自動化してみようと思います。NetlifyではGitの指定リポジトリの指定ブランチコミットされた内容を自動的にデプロイしてくれるため、ここでは静的化とオリジンドメインの隠蔽、コミットまでを行う処理を自動で動くようにしてみます。

#!/usr/bin/sh

##### 設定 ##### ##### ##### ##### ##### ##### ##### #####

# サイト情報
SITE_DOMAIN="koneta.click(要修正)"          # 公開用URL
ORIGIN_DOMAIN="origin.koneta.click(要修正)" # オリジンURL

# 作業ディレクトリ
WORK_DIR="/var/path/to/wordpress/(要修正)"

# BASIC認証
BASIC_USERNAME="basic_username(要修正)"
BASIC_PASSWORD="basic_password(要修正)"

##### ##### ##### ##### ##### ##### ##### ##### ##### #####

SITE_URL="https://$ORIGIN_DOMAIN"

cd $WORK_DIR
rm -dfR $WORK_DIR$ORIGIN_DOMAIN

# サイトを静的HTML変換
wget --mirror \
     --no-parent \
     --page-requisites \
     --convert-links \
     --adjust-extension \
     --restrict-file-names=windows \
     --no-verbose \
     --http-user=$BASIC_USERNAME \
     --http-passwd=$BASIC_PASSWORD \
     $SITE_URL

# 隠蔽単語を置換
find $WORK_DIR$ORIGIN_DOMAIN -type f | xargs sed -i -e "s|$ORIGIN_DOMAIN|$SITE_DOMAIN|g"

# 現座時刻を作成
now_date=`date +'%Y/%m/%d %H:%M:%S'`

# 変更内容をコミット
git pull
git add -A
git commit -m "[modify] ブログ更新更新 $now_date"
git push origin master

実際のコマンドは上記のようになるのです。この「(要修正)」部分をご自身の環境に合わせて修正後、cronなどで定期実行されるようにすることであとは放置しているだけで更新の変更箇所がNetlifyに反映されます。これにて作業は押しましです。あとはサイトの更新をするだけです!

終わりに

ここまで長い記事を書いたのは初めてです。技術系記事はほとんど自分用の備忘録で書いていますが、果たしてこの記事が役立つことはあるのか…でも静的化は楽しかったので良しとします。最近は静的サイトジェネレータでのサイト作りが増えていて、WordPressは重い遅い脆弱と散々に言われていますが、個人的には使い慣れていることもありまだまだ使っていきたいと思います!

追記 2021-01-03

静的化したファイルのホスティング先をGoogleさんのところのFirebase Hostingに変更しました。その時の作業ログも記事にしましたので、気になった方は覗いてみてください。

]]>
https://koneta.click/p/44/feed 0
WordPressをwgetとNetlifyでお手軽に静的サイト化する話 https://koneta.click/p/27 https://koneta.click/p/27#respond Mon, 11 May 2020 05:00:00 +0000 https://koneta.click/?p=27 WordPressって便利ですよね。基本的な機能はもちろん、テーマやプラグインがたくさんあって「それっぽい」サイトなら簡単に作ることができます。でもWordPressって無駄な処理が多くて遅いし、管理画面への不正アクセスだったりデメリットも目立ちますよね。特に、もう更新する気のないサイトともなると、本体やプラグインのアップグレードを怠ってしまい、不正アクセスの絶好の標的になってしまいます。

じゃあどうするか…と考えてみると、WordPressは動的な仕組みとはいえ、ページに出す記事自体は固定のものなので、いっそのこと静的ファイルにしてしまえばいいのではと考えました。静的ファイルになることで、煩わらしい更新作業や管理画面への不正アクセスを恐る必要がなくなります。さらに副産物として、作成した静的ファイルをホスティングサービスに設置することで、高速に表示してもらえるようになります。

というわけで今回はWordPressで動ているサイトからプラグイン不要で静的ファイルを生成し、それを「Netlify」というホスティングサービスに設置することで高速安心な静的サイトを構築していきたいと思います。(wgetコマンドが実行できる環境構築とNetlifyの細かい設定については触れません…。)

静的化の注意点

さて、導入部分ではさも静的化がメリットまみれのように書いてしまったので、ここでWordPressを静的サイト化するデメリットについて触れておこうと思います。

動的部分が動かなくなる

まずは、当然ながらWordPressの動的部分が動かなくなります。記事部分のような静的でも問題ないところを抜くと、よく使われていそうな動的部分はサイト内検索や記事へのコメント機能などの機能が動かなくなります。また、テーマやプラグインによっては意味がなくなったり、正しく動作しなくなるものもあると思います。

テーマやプラグインなど独自のものについては、サイトごとに対応方法も異なるため何とも言えませんが、コメント機能とサイト内検索については外部のサービスを活用することで静的化と同じように動作させることができるそうです。 検索でしたら「Google カスタム検索」コメント機能では「Disqus」というサービスが使えそうです。

ページが増えると静的化に時間がかかる

もう一つのデメリットは今回の方法では記事数が増えると静的化の処理にとても時間がかかるという点です。今回の方法はファイルを作成する際に実際にページにアクセスして返ってきた内容を保存しています。 そのため生成するすべてのページにアクセスする必要があります。これはカテゴリやタグごとの一覧ページもひとつずつ保存する必要があるため、記事が増えることで、一覧ページも増えるため静的化がどんどん遅くなってしまうわけです。

もう運営・更新するつもりがないサイトではこれらのデメリットはあまり関係ありませんが、自分が静的化したいサイトでデメリットを許すことができるかは確認する必要があると思います。

静的化するよ

さて、デメリットについても書きましたので、いよいよ静的化です。静的化するにあたり動かなくなる部分はこのタイミングで消しておきます。私のサイトの場合ではコメント,ピンバック,検索でした。また不要な固定ページもありましたので合わせて削除しました。

準備が完了したのでwgetしていきましょう。wgetをする際はローカルでもリンクが切れないようにするオプションと、ページ内で使用している画像なども一緒にダウンロードするオプション(+α)を設定して実行します。

$wget --mirror \
      --no-parent \
      --page-requisites \
      --convert-links \
      --adjust-extension \
      --restrict-file-names=windows \
      --no-verbose \
      [サイトのURL]

このコマンドを実行することで指定したURLからリンクを辿っていきたどり着いたページがHTMLファイルとして保存されます。あとはこのファイルを煮るなり焼くなりで公開することができます。ちなみに設定しているオプションの効果は以下の通りです。

オプション名説明
–mirror無限に再帰してローカルよりも新しいファイルをDL
–no-parent親ディレクトリは対象にしない
–page-requisitesページを表示するのに必要な全ての画像等も取得する
–convert-links HTML内のリンクをローカルパスに変更する
–adjust-extensionDLファイルに合わせた拡張子をつける
–restrict-file-names指定したOSに対応したファイル名を付ける
–no-verboseログ出力を省略する (オマケ)
[サイトのURL]DLするサイトTOPのURL

ファイルを設置するよ

最後にやっとの思いで作成したファイル達を静的ホスティングサービスで公開していきたいと思います。今回はタイトルにもある通りNetlifyというサービスを使います。Netlifyは静的なサイトを簡単高速に提供できるWebサービスで、公式サイト曰く、わずか3ステップでWebサイトを公開できるようです。

Netlifyにファイルを設置するにはGitリポジトリを使います。Netlifyでは、GitHub, GitLab, Bitbucketのいずれかで作成したリポジトリを接続することで簡単にデプロイすることができます。操作の詳細は省きますが、(Gitの操作ができれば)簡単にできると思います。デプロイ後はドメインやSSLの設定を行うことで普通のWebサイトとして公開することができます。

最後に

さて、ここまでWordPressサイトの静的化について書いてきました。もともとはもう更新する気のないサイトを放置しておくのが怖かったというところからこの作業を始めたのですが、作業を進めていくにつれだんだん楽しくなり、今書いているこのサイトも静的化したくなってしまいました(実際に静的化しちゃいました)。

今回は更新がないサイトということで、簡単に必要最低限やってきましたが、更新があるサイトとなると追加で気にしなくてはいけないことが増えていきます。そんなわけで続編として、更新があるWordPressサイトの静的化の手順もまとめていきたいと思います。

追記 2020/06/01

続編の記事を公開しました!

今回の登場人物

WordPress … サイトやらブログやら簡単に作れるCMS
Netlify … 静的ホスティングサービス
GitHub, GitLab, Bitbucket … Gitホスティングサービス。 私はGitLab派!

]]>
https://koneta.click/p/27/feed 0