作業ログ – koneta https://koneta.click DIYとデジモノとプログラミングとライフハックをコネた...小ネタ Fri, 11 Mar 2022 12:59:16 +0000 ja hourly 1 https://wordpress.org/?v=6.1 https://koneta.click/wp-content/uploads/2020/02/cropped-icon-32x32.png 作業ログ – koneta https://koneta.click 32 32 無料で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
ChromeBookで作業したいのでVPSにDockerで「Eclipse Theia」環境を構築してみました https://koneta.click/p/848 https://koneta.click/p/848#respond Fri, 23 Jul 2021 18:28:25 +0000 https://koneta.click/?p=848 先日ChromeBookを購入しました。このちょろめ君は、そのお手軽さで意外と色々なタイミングで出動してくれています。しかしChromeBookでお手軽にできるのはおおよそブラウザのみでできる範囲になります。Androidアプリも使う事はできますが、やはり物足りないもので、ChromeBookでできることを増やそうと奮闘する毎日です。

私のPCの使いみちの中で大きいのが、主にWeb系のコーディングです。その昔であればメモ帳とブラウザだけあればWebページを作ることができる…という話でしたが、現在のWeb系はフロントエンドもツールやフレームワークが充実し、開発環境をしっかり構築しないといけなくなりました。

というわけで今回はブラウザからいい感じの開発環境にアクセスできるようWeb上のエディタとして「Eclipse Theia」を構築してみようと思います。今回はこれをVPS(ConoHa)上に構築することで、どの端末からも同じ環境にアクセスできるようにしてみます。

Docker環境準備

まずはDocker環境の準備です。Docker環境の構築は以前記事にしていますので、まだDocker環境を作っていない方はこちらを参考にしていただけると幸いです。

$ docker --version
Docker version 19.03.13, build 4484c46d9d
$ docker-compose --version
docker-compose version 1.25.5, build 8a1c60f6

今回はVPS(ConoHa)で作業しましたが、中身は普通のCentOSなので普通にDocker(とdocker-compose)環境が作成できれば大丈夫だと思います。

リバースプロキシの準備

次はProxyの用意です。こちらは必須の作業ではありませんが、同じVPS上で動作させているのであれば、簡単に複数サイト/ツールで使用できSSL設定も自動で行ってくれるようになるため使用するのをおすすめしておきます。こちらも以前記事にしていますので参考にしてみてください。

今回はVPS上に設置する事、加えてすでにほかのツールを設置していることやそれぞれのツールにドメインで簡単にアクセスできるようにしたかったのでプロキシの設定が必須でしたが、ローカルで動かす場合やテストで動かしたいだけであれば設定は不要です。

Eclipse Theia環境を作る

docker-compose.ymlを用意

さて、周辺環境の構築ができましたので、いよいよ本題、「Eclipse Theia」の構築をやっていきます。とはいえDocker上に構築していくので、動かすだけであれば操作自体はとっても単純です。

version: '3'

services:
  eclipse-theia:
    restart: always
    image: theiaide/theia-full # 全部入りイメージ
    ports:
      - 50000:3000
    volumes:
      - /path/to/workspace:/home/project/:cached
    environment:
      - VIRTUAL_HOST=[ドメイン (プロキシ用)]
      - LETSENCRYPT_HOST=[ドメイン (Let's用)]
      - LETSENCRYPT_EMAIL=[メアド (Let's用)]
                                
networks:
  default:
    external:
      name: common_link

はい。今回はdocker-composeを使って環境を構築していくので、上記の内容でおなじみのdocker-compose.ymlを作成してください。修正するのはvolumesのワークスペースディレクトリ部分とenviromentのドメイン設定部分だけです。

ちなみに、ディレクトリ設定の方は設定したディレクトリ以下であればその部分だけ表示することができるため、ワークスペースのルートディレクトリを設定してあげるといいと思います。また、プロキシを利用しない場合はenviromentnetworksの設定は削除してもらって大丈夫です。

BASIC認証の用意

今回は外部からアクセスできる場所に設置するため一応アクセス制限用の設定をします。今回は簡単にBASIN認証でお茶を濁そうと思います。やり方は上記のリバースプロキシ設置時の記事に書いてありますので参考にしてみてください。

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

基本的に対応したパスワードファイルを読み込んであげるだけですが、ファイル名をドメインと同じにするのを忘れないようにしてください。

エディタのインストール

最後にオマケとしてCUIにエディタ(micro)を入れておきたいと思います。基本的には完全に無駄な作業です。Eclipse Theiaのコンテナは基本的にUbuntuで動いているらしく、エディタ内にターミナルの機能もあります。このターミナル機能を使うことでSSHで別のサーバにアクセスすることもできます。

しかしSSH先でvimなんぞを使ってしまうとESCキーがエディタ側に吸われてしまうため、vimで入力モードから脱出することができません。そこで(SSH先のサーバに)別のエディタを入れておきます。

curl https://getmic.ro | bash

というわけでmicroというエディタを入れておきました。こちらはCUIのソフトながら使い勝手はGUIにも匹敵するエディタで、少し前におすすめされたいたので入れてみました。まあ、こちらは完全にオマケなのでご自身の使い道に合わせて必要があれば入れてみてください。

実行してみる

準備ができたらいつも通りdocker-composeコマンドで実行して作業完了です。

$ cd /path/to/[docker-compose.ymlディレクトリ]
$ docker-compose up --build -d

実行できたらブラウザからアクセスしてみます。

アクセスした様子

(DNS or hostsの設定が完了していれば) 先ほど設定したドメインでアクセスするとワークスペースのディレクトリでVSCodeを起動したような画面が出てきます。これにて作業完了です。

終わりに

さて簡単に書いてきました。これのおかげで私のChromeBook君の使い道が爆発的に増えました。コーディング環境ができたのはもちろんですが、一緒に構築できたターミナル環境が思っているよりも便利でした。今後もChromeBookの使い道を増やしていけるよう探索を続けていきたいと思います。

]]>
https://koneta.click/p/848/feed 0
Nature Remo APIでn8nから家電を遠隔操作してみました https://koneta.click/p/791 https://koneta.click/p/791#respond Wed, 09 Jun 2021 16:24:36 +0000 https://koneta.click/?p=791 以前の記事でNature Remoというスマートリモコンで生活の未来度合いを向上することができたという記事を書きました。今回はそのRemoの活躍の場を広げるためプログラムから操作する…つまりはAPIを使って操作してみようと思います。加えてn8nでフローから操作もやってみようと思います。

背景

Nature Remoはスマホやスマートスピーカーから家電を簡単に操作することができるスマートリモコンです。私はその中でもNature Remo miniという端末を使用しています。Remo自体は紹介記事を書いていますのでよかったら読んでみてください。

また、IFTTTというサービスを活用することでワークフローを作成して、タイマーや居場所、SNSなどで設定した条件を満たしたときにRemoから家電を操作することができます。しかしIFTTTは最近無料で使用できる範囲が狭くなり使い勝手が悪くなってしまいました。 (私は元々使ってなかったので関係はなかったですが…)

そんな中、IFTTTの代わりとしてよく名前が挙がっているのがn8nです。n8nは無料で色々なサービスの連携/自動実行フローを作成できるのOSSのツールです。

n8nにRemoが公式に対応しているわけではないですが、RemoにはAPIが用意されているためHTTPSリクエストで家電を操作することができます。今回はこのAPIとn8nを使ってちょっと違った遠隔操作をしてみようと思います。

アクセストークンを取得する

APIを使用するためのアクセストークンを取得していきます。というわけでまずは home.nature.global にアクセスしてアクセストークンを作成します。

チープっぽいですが公式です。

ページにアクセスしたらGoogleアカウントで認証するかNatureに登録しているメールアドレスを入力してログイン用のメールを送信してもらってください。ログインができると「home.nature.globalがRemoへのアクセスをリクエストしています。」というページが表示されるので「許可する」を選択して完了です。

トークンは発行直後しか出ないのでモザイクの意味は…

権限の確認が終わると「Generate Access Token」のボタンが表示されます。このボタンを押すとアクセストークンが取得できます。一度ページを離れてしまうとアクセストークンは表示されないのでちゃんとメモしておくようにしてください。また、このトークンがほかの人に知られてしまうと、家電を好きなようにされてしまうので、絶対に外部に漏れないようにしてください。

これでアクセストークン取得完了です。

APIを使ってみる

トークンが取得できたので、n8nでワークフローを設定する前にAPIを使ってみます。APIはHTTPSリクエストで叩くことができるので、リクエストを飛ばせればなんでも大丈夫ですが、今回はcurlコマンドでやってみます。というわけで下記のコマンドを実行してみます。[アクセストークン]のところには1つ前の手順で取得したトークンで書き換えて実行してください。

$ curl -H "Authorization:[アクセストークン]" https://api.nature.global/1/appliances

これによりRemoに登録されている家電とそれぞれに実行できる操作の一覧を取得することができます。コマンド実行時に大切なのがヘッダーとして付与しているAuthorizationです。基本的にAPIを叩くときには必須の項目になります。そして、このコマンドで取得できる家電や操作にはIDが振られています。APIで家電を操作する際にはこのIDをパラメータとして指定するため操作したいIDはメモしておいてください。

今回は照明を操作してみようと思います。照明の操作には先ほどの取得した一覧からIDを持ってきます。

[
    {
        "他の家電情報"
    },
    {
        "id": "※※※※※※ ここのIDが大事 ※※※※※※",
        "device": {
            "...Remoの情報..."
        },
        "model": {
            "...照明自体の製品情報..."
        },
        "type": "LIGHT",
        "nickname": "照明",
        "image": "ico_light",
        "settings": null,
        "aircon": null,
        "signals": [],
        "light": {
            "buttons": [
                "...ボタン類..."
            ],
            "state": {
                "...明るさやON/OFFなど..."
            }
        }
    },
    {
        "他の家電情報"
    }
]

上記が一覧取得の結果を抜粋したものになりますが、この中の「※※※※※※ ここのIDが大事 ※※※※※※」部分に書かれているID
を使って照明を操作します。操作にはまたcurlで今度はPOSTします。

$ curl -H "Authorization:[アクセストークン]"  -X POST -d "button=on" https://api.nature.global/1/appliances/[照明のID]/light

これを実行すると照明が点灯するはずです。逆に消すときはbutton=onの部分をbutton=offにすれば大丈夫です。ちなみにこのボタンの設定値は先ほどの一覧取得した中のbuttonsnameの項目で確認することができます。ここまでできればあとはプログラムから叩いてもいいし煮ても焼いてもです。

n8nから実行する

さて本記事のメインでありながら、正直蛇足っぽいn8nからAPIを叩くフロー作成をやっていきます。とはいえここまでで動くことは確認できているのでn8nのHTTPリクエストノードに設定してあげるだけで動作します。

完成イメージ

ワークフローの完成イメージは上記の通りです。ただ動かすだけであればSTART含め2つのノードだけで作ることもできますが、一応将来性を考えトークンや照明IDの設定はSETノードに切り分けています。

SETには以下のように設定したい値を登録してあげるだけで大丈夫です。設定後一度ノードを実行してあげると次のノードでこの値を使用することができます。

設定値

次がメインのHTTPリクエストノードです。このノードでは基本的にcurlで叩いた値とSETノードで登録した値を登録してあげれば大丈夫です。SETノードで登録した値を使うには各入力欄の右側にある歯車マークから「Add Expression」を選択して「Nodes」からSetValuesString から選択することができます。あとはcurlで試した通りにHeaderとParametersに登録してあげるだけです。

前半
後半

これでフローは完成です。最後にフロー実行ボタンを押して正常に照明が操作できるかを確認してみてください。

エクスポートファイル (クリックで展開)
{
  "name": "Remo-Light",
  "nodes": [
    {
      "parameters": {},
      "name": "Start",
      "type": "n8n-nodes-base.start",
      "typeVersion": 1,
      "position": [
        200,
        0
      ]
    },
    {
      "parameters": {
        "keepOnlySet": true,
        "values": {
          "string": [
            {
              "name": "Authorization",
              "value": "[アクセストークン]"
            },
            {
              "name": "Light-ID",
              "value": "[照明ID]"
            },
            {
              "name": "button",
              "value": "on"
            }
          ]
        },
        "options": {}
      },
      "name": "Set request data1",
      "type": "n8n-nodes-base.set",
      "typeVersion": 1,
      "position": [
        350,
        0
      ]
    },
    {
      "parameters": {
        "requestMethod": "POST",
        "url": "=https://api.nature.global/1/appliances/{{$json[\"Light-ID\"]}}/light",
        "options": {},
        "bodyParametersUi": {
          "parameter": [
            {}
          ]
        },
        "headerParametersUi": {
          "parameter": [
            {
              "name": "Authorization",
              "value": "={{$json[\"Authorization\"]}}"
            }
          ]
        },
        "queryParametersUi": {
          "parameter": [
            {
              "name": "button",
              "value": "={{$json[\"button\"]}}"
            }
          ]
        }
      },
      "name": "Turn Light1",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 1,
      "position": [
        500,
        0
      ]
    }
  ],
  "connections": {
    "Start": {
      "main": [
        [
          {
            "node": "Set request data1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set request data1": {
      "main": [
        [
          {
            "node": "Turn Light1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {},
  "id": "7"
}

オマケとして、ここで作成したフローをエクスポートしたjsonファイルを載せておきます。使用する際はSETノード内のトークンと照明IDを修正してください。

終わりに

さて、そんなわけで今回はRemoとn8nの連携をやってみました。APIを使うことで自分のプログラムからでも操作できるようになるのでRemoの適用範囲をだいぶ広げることができると思います。

今回紹介した内容だけではちょっと物足りない気もしますが、私は今回紹介したフローの起点をWebHookにすることでリクエストされたら自動的にフローが実行されるようにし、使わなくなったKindleをリモコン兼データモニターとして活用しています。

意外といい感じ

別途アプリを入れる必要もなく、Kindle自体の脱獄も不要、Webブラウザが動けばリモコンとして使えるため、意外と気に入っています。そのうちこちらの詳細も記事にしたいと思います。

参考

Remo公式リファレンス

アクセストークン管理

]]>
https://koneta.click/p/791/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
OSSのワークフロー作成ツール n8n をVPS(ConoHa)に構築します https://koneta.click/p/680 https://koneta.click/p/680#respond Sun, 14 Mar 2021 15:53:00 +0000 https://koneta.click/?p=680 この世にはWebサービスやイベントを繋げ自動的にワークフローを実行してくれる便利なツールがあります。この手の有名なツールとしてはIFTTTやZapierが挙がってきます。しかし、これらは一部機能が有料だったり登録できる件数が少なかったりします。そこで出てくるのが今回紹介するn8nです。

n8nは、オープンソースで使用できる自動化ツールで、IFTTTなどと同じように、何かしらの動作やイベントをトリガーとしてアクションが始まり、定義したフロー通りに自動で処理を行ってくれます。

そこで今回はn8nの環境をVPS(ConoHa)上のDockerで構築し簡単な操作方法を実際に試してみたいと思います。

環境構築

基本的な環境構築は公式サイトの「How to get started?」の項目を見ると全て書かれています。Node環境があればコマンドちょちょいで、Docker環境があればコマンド1つでツールを動かし始めることができます。しかしあえて今回はここに掲載されていないdocker-composeで環境を作っていこうと思います。

タイトルではVPSに構築しますと書いていますが、正直Docker環境があれば、結構どこでも大丈夫です。というわけでDocker環境を用意しておいていただきたいのですが、ConoHaでの環境構築は以前記事にまとめていますので、そちらを参考にしていただけるとと思います。

では早速作っていきたいと思います。とはいえDocker(docker-compose)上で構築するため、やることはほぼほぼコマンドを実行するだけです。というわけでdocker-composeを実行するため設定ファイルを用意していきます。内容は以下の通りです。

version: "3"

services:
  n8n:
    image: n8nio/n8n
    restart: always
    ports:
      - "5678:5678"
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER
      - N8N_BASIC_AUTH_PASSWORD
      - N8N_HOST=${SUBDOMAIN}.${DOMAIN_NAME}
      - N8N_PORT=5678
      - N8N_PROTOCOL=https
      - NODE_ENV=production
      - WEBHOOK_TUNNEL_URL=https://${SUBDOMAIN}.${DOMAIN_NAME}/
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ${DATA_FOLDER}/.n8n:/home/node/.n8n
    network_mode: bridge

docker-conpose.ymlだけで完結させることもできますが、取り回しがよくなるよう今回は設定ファイルを別に作成してみます。設定ファイルは.envという名前でymlファイルと同じところに置いておきます。内容は下記の通りです (ほぼ公式サイトのコピペです)。

# どこのフォルダにデータを保存する
DATA_FOLDER=/root/n8n/

# どんなトップドメインにn8nを設置する
DOMAIN_NAME=example.com

# どんなサブドメインでn8nを動かす
SUBDOMAIN=n8n

# BASIC認証のID - ※※※※※ 変更必須です ※※※※※
N8N_BASIC_AUTH_USER=user

# BASIC認証のPASSWORD - ※※※※※ 変更必須です ※※※※※
N8N_BASIC_AUTH_PASSWORD=password

# 定期実行で使用するタイムゾーン
GENERIC_TIMEZONE=Europe/Berlin

この設定で、n8n.example.comで受け付けるための設定ができます。ご自身の環境に合わせて修正してください。ちなみに、タイムゾーンはツール内の設定で変更できるのでそのままでも大丈夫です。

また外部からアクセスできる環境にn8nを設置するのであれば、上記の設定だけではSSLの設定ができたいないのでオープンな環境に設置するには不足しています。今回はn8nコンテナの前段としてリバースプロキシを設置するためSSLはそちらで補います。加えて公式にも書かれていますが、オープン環境に設置するのであればセキュリティ対策として最低限BASIC認証くらいは入れてください。

ここまでくればあとはDockerを起動するだけでn8n自体の設置作業は完了です。

$ docker-compose up --build -d

最後に.envで設定したドメインに合わせてDNSレコードやhostsを設定すれば作業完了です。

追加作業リバースプロキシ

さて、上記設定だけではSSLが未対応なので今回はリバースプロキシを設置して対応していきたいと思います。今回使用するリバースプロキシはDockerで動かす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:

これだけでリバースプロキシ自体の用意とSSLの準備が完了しました。毎度のお手軽さには驚かされます。このdocker-composeは先ほどのdocker-composeとは別に作成したほうがいいです。

次に、Nginx側で「良しなに」設定してもらうための記述を先ほどのn8n用docker-composeに追記していきます。追記項目はドメイン設定とSSL証明書取得用のメールアドレス、そしてコンテナを繋げるネットワーク設定です。

version: "3"

services:
  n8n:
    image: n8nio/n8n
    restart: always
    ports:
      - "5678:5678"
    environment:
      ...(省略)...
      - VIRTUAL_HOST=n8n.knta.cc
      - LETSENCRYPT_HOST=n8n.knta.cc
      - LETSENCRYPT_EMAIL=tunezune.history@gmail.com
    volumes:
      - ...(省略)...
    network_mode: bridge

上記のようにn8nで使用するドメインと証明書取得用のメールアドレス、ブリッジの設定を追記します。最後にNGINXのコンテナを起動、n8nのコンテナを再度起動。これだけで作業完了です。

$ cd /path/to/nginx_docker-compose/
$ docker-compose up -d
$ cd /path/to/n8n_docker-compose/
$ docker-compose up -d

使ってみる

では環境構築ができたので、最後に軽くn8nを使ってみようと思います。題材は「slack投稿を定時実行」でやっていきます。

初期画面

n8nを起動して設定したドメインにアクセスすると上記の画像のようなページが表示されます。こちらがn8nのメイン画面になっています。画像真ん中らへんに表示されているStartのノードが初期フローの開始地点となっています。

基本の作業はこのノードに次のノードを繋げていくことでワークフローを作っていきます。ノードを追加するには右上にある赤色の+ボタンを押すか各ノードのドット部分からドラッグです。

定期実行する開始ノードを追加

上記の操作を行うとノードを選択するウインドウが出てきます。ここから追加したいノードを探します。ノードには2種類あり「Regular」が処理を行うノード、「Trigger」がワークフローの起点になるノードになります。今回は定期実行をするための起点ノードCronを使いたいため、Triger欄からCronを選択しています。

起点を追加した様子

Cronを追加した様子は上記の通りです。ノードを選択した際にノードに対応した設定画面が表示されたと思います。追加後でもノードをダブルクリックすると再度表示されます。今回は定期実行のノードのためいつ実行するかの設定ができます。

次にSlack投稿をしてもらうためSlackのノードを追加します。先ほどと同じようにノード追加画面を開き、今度は「Regular」の項目からSlackのノードを追加します。今回はSlackのAPIキーを取得するところは省略しますが、APIキーの設定、チャンネルと送信するテキストの設定で追加完了です。

また、こちらも本記事では使いませんが、送信するテキストには前のノードのデータ (各サービスのAPIノードであれば取得したデータ、RSS取得ノードならRSSの中身など) を指定することもできます。

フロー完成~!

以上の作業で上記のようなフローが完成です!最後にフローを保存して、右上の「Active」を有効にすることで定期的にSlackに送信してくれるフローが動き始めます。なんともまぁ簡単でした。

終わりに

さて、そんな感じで書いてきました。私の中では自動化というのはいろいろロマンのある作業だったりします。しかし、自動化をするにはいろいろ作業する必要があり、正直めんどくさいという面もあります…..。そんなところでn8nに出会ったわけですが思っていたよりも数倍簡単に環境構築もフロー作成もでき、今回は触れませんでしたがサーバ上のコマンドも実行できるので使い道はどこまでも広げることができます。ありがとういいツールです。何か便利な使い道を思いついたらまた記事にしようと思います。

]]>
https://koneta.click/p/680/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
3DプリンターでPC縦置きスタンドを作りました! – 3Dプリンターのある生活 https://koneta.click/p/643 https://koneta.click/p/643#respond Mon, 01 Feb 2021 13:29:25 +0000 https://koneta.click/?p=643 最近では在宅業務が多く、会社のノートPCを自宅に持ち帰り作業しています。そんな中、困っているのがPCの置き場です。自宅には自分のノートPCやモバイルモニターもあるのですが、それらを重ねて置くしか置き場がないのです。

重ねて置いておくと下のものを取りたくなった際に上のものをどかす必要があり、それがどうにもめんどくさいです。というわけで今回はPC達を立てて置けるようPCスタンドを作っていきたいと思います。

作りたいモノ

さて改めて、今回作るもののイメージを固めていきたいと思います。正直今回作るものはAmazonで調べればいくらでも出てきます。なので単純にPCを縦置きしたいだけであればこちらを購入すればいいわけです。

ただ、買うけでは面白くないので自分作ろうという話…だけでは無いです。

今回、私はノートPC2台とモバイルモニターを収納することが目的なのですが、調べてみると意外と3台を縦置きできるスタンドというのは、ほとんど販売されていません (執筆時点で1商品のみ)。

というわけで今回は3台を縦置きできるスタンドを自作していきたいとおみます。

モデリング

では早速作っていきましょう!

今回は使用するソフトはFusion360です。3DCADということで敷居が高く感じる方もいるかもですが、実際は範囲を決めて押し出して角を取るだけと簡単です。

作業工程

私はまだまだ初心者で簡単な機能しか使っていませんが、今回のモデリング時間は計測を含めて10分程度だったと思います。あとはstl形式で出力してスライスソフトで3Dプリンターで扱う形式に変換してあげるだけです。

スライス

スライスソフトは今回も使用するプリンターに合わせてFlashForgeさんのソフトを使っていきます。

さて、普段の私であれば何も考えずにスライスソフトにモデルを読み込んでサポートを自動挿入、デフォルトの設定でスライス…とやってしまうところですが、今回は「向き」をちゃんと考えてみようと思います。

この向きになります

聞いた話によると、層の向きにより印刷後の強度が変わってくるそうです。調べると色んな情報が出てきますので詳細は省きますが、下記の図のように層の向きと力がかかる向きが合ってしまうと、剥離の原因になります。

雑図!

そんなわけで今回は、上記のスライスソフトのスクリーンショットのように力がかかる向きに強くなるよう、モデル自体を立てて配置してみました。

ちなみに…この配置によって、ヘッドが宙に浮く部分がなくなり、糸引きの原因を回避できるという思わぬメリットもありました。

できた!

そんなこんなで完成したものがこちらです。

完成!

PC達を設置してもいい感じです!

造形精度はぼかす方針

今回使用した3Dプリンターの造形可能サイズ内に収めるため約13cmくらいしかサイズを取れなかったので、1台だけおいた際に倒れてしまうかなと思っていましたが、そんなことはなく、いい感じです。

1台でも大丈夫!

重ねて設置していたときと比べて、3台ともすぐに取り出すことができるので、PCスタンドとうのは便利!3Dプリンターとか関係なくぜひとも導入したいアイテムですね。

終わりに

今回は初の2桁時間の印刷となりましたが、なんとかうまくいってよかったです。写真を撮り忘れたので今回の記事では触れませんでしたが、印刷中に謎のでっぱりができ、ヘッドがそこに引っかかり層が思いっきりズレるという事件も起こっていました。

でっぱりをニッパーで切断することで何とか復帰することができましたが、実際に色々作ってみるといろんなアクシデントに遭遇して面白いですね。こちとら趣味なのでいろんなアクシデントに遭遇しつつ今後も楽しくやっていきたいと思います。

]]>
https://koneta.click/p/643/feed 0
マキノラボズさんのフロアタイルで畳をフローリングに簡単DIY https://koneta.click/p/631 https://koneta.click/p/631#respond Sun, 17 Jan 2021 08:31:40 +0000 https://koneta.click/?p=631 自宅作業部屋の床は畳なのです。一応カーペットを敷いて、椅子からのダメージを軽減させているのですが、カーペットを取ってみると畳に傷が入っていたりへこんでいたりと、貫通ダメージがあるようです。

そこで、今回はMakinolabsさんのシデフロアというフロアタイル、細長いパーツを組み合わせることで簡単に設置できるフローリングを使い、畳からフローリングに変更してみたいと思います。

シデフロア フロアタイル?

シデフロアは接着剤も不要で設置できるフローリングで、細長い板の「サネ」同士を組み合わせることで簡単に設置することができる床材です。長さの調整も普通のカッターナイフで可能、表面はリアルで種類も豊富のようです。

以前ホームセンターで見かけて気になってはいたのですが、とうとうチャレンジしてみました。

今回は椅子が動く分の約1畳のみ施工してみようと思います。私の部屋はベッドも置かれているのですが、今回フローリングにした部分を仮想的な仕事部屋として分割したいと思います。

間取り(雑図)

約1畳ですが測ってみると1畳は少しオーバーしていたので、今回は2畳分を購入しました。購入先は公式Webショップだったのですが、配達してくれた方から「重いので注意してください!!!」と3回ほど念押ししてもらいましたので、ホームセンターなどで購入する方はご注意ください。少なくとも車か配達は必須と思います。

施工します

それでは設置していきたいます!

掃除/下準備

まずはお掃除です。なるべくきれいにしてあげてください。

たたみぃ。 (施工前

次に下準備として床に凸凹がある場合ですが、事前にベニヤ板などで平らにしておく必要があるそうです。平らじゃない床に設置すると浮きや折れの原因になるそうです。(私はめんどくさくなり飛ばしてしまいました。)

仮合わせ/調整

掃除が完了したら商品を開封し、サイズと必要枚数を確認してみます。上記の画像の範囲で14枚使用しました。

使用する範囲が決まったら次に長さの調整を行います。長さの調整はカッターナイフで数回切れ込みを入れあとは折るだけで簡単にできます。また、より強くするために、それぞれのパーツは少しずらして設置するのが推奨されています。

もちろん私はめんどくさくなり、長さの調整は飛ばしてしまいました。しかしデザイン的にはずらして設置したほうが完全にかっこいいので皆さんがお試しの際はちゃんとずらしてくださいね。

設置

ではいよいよ設置です。設置方法は商品と一緒に説明書があり、動画でも見ることができるので一度見ておくとイメージしやすいと思います!つまりここで書く意味は皆無です。でも簡単に書いておきます。

設置は先ほども書いた通り「サネ」部分を組み合わせるだけになります。組み合わせるのもちょっと斜めから入れ込むだけなのでとても簡単です………1列目は。

表面部分は繋ぎ目がわからない…。

2列目以降は、追加する列をあらかじめ繋げておいて、そこから1本ずつスライドで入れていく手順になるのですが、これが1人ではなかなか難しかったです。

まずまとめて持ち上げるとあらかじめ繋げていた部分が外れたりズレたりします。それが解決できても持ち上げた状態でスライドさせるのも難しいです。

本設置前に数枚を使って練習しておいたほうがスムーズに設置できたかもしれません。

と、2列目以降は難しいのですが、今回設置してみて掴めたコツとしては以下の通りです。

POINT

  • 精度を信じる
  • 斜めったときは見えなくても隙間ができているので全体的に軽くはめる直す
  • 1列目から縦のズレがなるべく少なくなるように設置する
  • 2列目以降ははまった部分をなるべく固定する
  • 精度を信じる
完成
ふろーりんぐぅ (施工後

目標の範囲まで設置が完了したら施工は完了です。

ズラさずに設置したので、ベニヤの1枚板のように見えてしまいますが、肉眼ではもう少し色も濃く、フローリングの仕上がりです。もう少し濃い色でもよかったかなと思います。ちなみに今回は「チーク」です。

設置してみて

設置してみての感想ですが、お手軽施工なのに、だいぶいい感じです。

畳を簡単にフローリングに替えられたというのはもちろん、その部分だけを見ればもともとフローリングだったかのような自然な仕上がりになりました。

また、めんどくさがりで推奨される手順を飛ばしましたが、畳の凹凸に影響した浮いている感じなどは無く、しっかり「床」として機能しています。

特に感動したのは「サネ」部分の精度で、慣れるまでうまくパーツ同士を繋げられず斜めになってしまい、「結合部分が曲がっているんじゃ」と思いましたが、全くそんなことはなく、ちゃんと隙間を埋めてあげればピタッとハマってくれるのは気持ちよかったです。

今回は作業スペース部分1畳の施工でしたが、今後フローリングに変えたくなったときはまた使わせてもらおうと思います。

総評

フローリング度★★★★
コツが必要度★★★★★
スッキリ度 1列目★★★★★
スッキリ度 2列目★★☆☆
コスパ良き度★★★★
総評★★★★

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