Git Branching Strategy for Customizing and Hosting OSS Repositories OSS をフォークして自社向けにカスタマイズし、さらに独自ホスティングまで行うケースがあり、定期的にオリジナルを追従しつつ、それぞれ差分を明確にしていくため、現在運用してる手法をまとめてます。 ブランチ main - メインブランチは追加する内容とデプロイ実行環境を提供します。 original/application-name - ここにオリジナルの内容をリモート (upstream) から取り込みます。 customize/application-name - ここは original/application-name から checkout および rebase しつつ、常に original/application-name の前のコミットとしてカスタマイズ内容が存在するようにします。 application-name の箇所はオリジナルのリポジトリ名です。 記録用のブランチ original/version/application-name customize/version/application-name version はソフトウエアバージョンでも日付でも好きに選べばいいでしょう。 カスタマイズを行い、リリースした時点のオリジナルとカスタマイズブランチをチェックアウトして保持しておきます。GitHub 等においては、original/version/application-name から customize/version/application-name との差分をマージ禁止 Pull Request として作成保持しておくと、diff を振り返えりやすいです。 操作方法 1. upstream(オリジナルリポジトリ)の登録 まずはリポジトリに、オリジナルのリポジトリを upstream として登録します。 1 2 git remote add upstream https://github.com/ORIGINAL/application-name.git git fetch upstream 2. original/application-name ブランチの更新 オリジナルの変更を取り込むためのブランチです。 ...
さくら VPS のローカルネットワークに接続するインターフェイスを Ubuntu 20.04 で設定する
さくら VPS には同一リージョンの複数サーバーを仮想スイッチングハブに接続することでプライベートネットワークを構築することができます。今回は Ubuntu 20.04 をセットアップした時の設定のメモです。 いつもは OS アップグレードしていたので気づかなかったですが、最近の Ubuntu はネットワークインターフェース設定が netplan というユーティリティに標準は置き換わっています。そのため、その設定の紹介になります。 ローカルネットワークの構築設定 さくら VPS のコントロールパネルで、スイッチを追加して、各サーバーのネットワーク接続画面でそれぞれ追加したスイッチに接続させます。(なお、現状はサーバーはシャットダウンしておかないとこの適用できません。) インターフェースを確認 $ ip addr show でネットワークインターフェースを確認します。 2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000 link/ether 52:54:12:00:XX:XX brd ff:ff:ff:ff:ff:ff inet 153.120.xx.xxx/23 brd 153.120.xx.255 scope global ens3 valid_lft forever preferred_lft forever inet6 2401:2500:102:2b03:xxx:xxx:xx:xxx/64 scope global valid_lft forever preferred_lft forever inet6 fe80::5054:12ff:fe00:xxxx/64 scope link valid_lft forever preferred_lft forever 3: ens4: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 52:54:12:00:XX:XX brd ff:ff:ff:ff:ff:ff 4: ens5: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000 link/ether 52:54:12:00:XX:XX brd ff:ff:ff:ff:ff:ff 今回は ens で始まる物理インターフェイスの2番目の ens4 をスイッチに接続したものとしてセットアップします。 ...
自前で JWT を発行、検証を Node.js で行う
Node.js で JSON Web Token (JWT) の ID トークンを発行し、検証する処理のメモです。 JWT のメリットはアクセストークンそのものが有効期間やコンテクストを保持できるため、個別にリクエストごとにセッションIDからセッション情報を引くという機構を省くことができます。そのため、自前の API サーバにおいて、アクセストークンとして JWT の ID トークンを使えるとサーバ処理を省力化できます。 今回の例では、JWT の検証をグローバルにできるようにはせず、サーバ内でのみ行うこととします。一般的な JWT トークンは JSON Web Key (JWK) Set から提供されており、JWT の署名検証ができるようになっていますが、それは行いません。 依存ライブラリ Auth0 がものすごく便利なライブラリを提供してくれています。 https://github.com/auth0/node-jsonwebtoken $ npm i -save jsonwebtoken $ npm i -save-dev @types/jsonwebtoken 鍵の生成 公開鍵と秘密鍵を別々にすることも可能ですが、分けると管理が面倒なのと同一サーバ内で処理するため、PEMファイルで管理します。 $ openssl genrsa > auth.pem 発行と検証処理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import fs from 'fs' import jwt from 'jsonwebtoken' const PEM = fs.readFileSync('auth.pem') function issueToken(subject: string): string { const opts: jwt.SignOptions = { algorithm: 'RS256', issuer: 'https://issuer.example.com', expiresIn: '1d', // https://github.com/zeit/ms } const payload = { sub: subject, } return jwt.sign(payload, PEM, opts) } export async function verifyToken(idToken: string): Promise<object> { const opts: jwt.VerifyOptions = { algorithms: ['RS256'], issuer: 'https://issuer.example.com', maxAge: '1d', // https://github.com/zeit/ms } return new Promise((resolve, reject) => { jwt.verify(idToken, PEM, opts, (err: Error, decoded: any) => { if (err) { reject(err) } else { resolve(decoded) } }) }) } // 実行確認 ;(async () => { const idToken = issueToken('userId') const result = await verifyToken(idToken) console.log(result) })() JWT 署名時の expiresIn より時間が経過している場合は、検証時に TokenExpiredError: jwt expired がスローされます。 時間内であっても発行時よりも maxAge が経過している場合は、検証時に TokenExpiredError: maxAge exceeded がスローされます。 実行結果 1 2 3 4 5 6 { sub: 'userId', iat: 1582639938, exp: 1582726338, iss: 'https://issuer.example.com' } まとめ ログイン成功時に issueToken を呼んで ID トークンをクライアントに発行します。クライアントは API リクエストの Authorization ヘッダに付与して、サーバ側は verifyToken で検証することで、認証および subject(sub)フィールドなどからユーザー情報を取得することができます。
さくら VPS の Ubuntu 18.04 で LUKS によるディスク暗号化をするには
さくら VPS は、AWS LightSail のようにディスクの暗号化をサービス側でしているという情報がありませんでした。昨今のハードディスク廃棄問題も気になるところで、あまり漏れたくない情報を保存したいのでディスクをまるごと(/boot は除く)暗号化してみます。 Ubuntu 18.04 のインストール コントロールパネルから カスタムOSインストール Ubuntu 18.04 を行います。 途中の「Partition disks」で Guided - use entire disk and set up encrypted lvm を選択します。そしてパスフレーズを設定します。 OS セットアップ後 VPSのコントロールパネルで緑の起動ボタンをクリックして、さらにコンソールボタンメニューのシリアルコンソールも即座に開いておきます。 シリアルコンソール画面がパスフレーズ入力で止まるので、ここで先ほどのパスフレーズを入力して起動させます。 暗号化状態の確認 # cryptsetup status /dev/mapper/vda5_crypt /dev/mapper/vda5_crypt is active and is in use. type: LUKS1 cipher: aes-xts-plain64 keysize: 512 bits key location: dm-crypt device: /dev/vda5 sector size: 512 offset: 4096 sectors size: 208207872 sectors mode: read/write flags: discards 起動時のアンロック設定 現状は起動時にパスフレーズを入力するしかありません。ここにさらに別途鍵ファイルをbootパーティション内に追加して自動起動するようにします。 ここからは Debian/Ubuntuで暗号化 LVM を使いつつ自動起動する - @znz blog の受け売りが大半です。大変参考になりました。ありがとうございます。 ...
新しい PC に使用していた SSD を流用して Windows 10 をインストールするには
目的 元々データディスク(セカンドドライブ)として使っていた SSD を、新しい PC のシステムディスクとしてデータをすべてフォーマットせずに流用する場合の方法です。本来は一旦別の HDD 等にデータを移動しておくべきですが時間がかかるので、データが大きいのと空き容量がまだので既存データを後方に移動して、前方にシステムディスクとしてのパーティション構成を構築します。 データの移動 EaseUS Partition Master Free をインストールします。 EaseUS®公式サイト|無料のパーティション管理ソフト - EaseUS Partition Master Free 「EaseUS Partition Master」パーティション編集ソフト - 窓の杜 このソフトウェアを使うと、パーティションのサイズ変更が可能になります。それで前方に空きが出るようにパーティションを移動変更します。なお予約済みパーティションなどが先頭にあった場合は削除します。 パーティションを構成する 続いて diskpart コマンドを使います。管理者権限で起動した PowerShell などで diskpart を起動します。 UEFI/GPT ベースのハードドライブパーティション を参考に構築します。 パーティション構成は先頭から下記のようにします。 ESP システムパーティション FAT32 100MB MSR 予約パーティション RAW 16MB 回復ツールパーティション NTFS 768MB Windows パーティション NTFS 残り全部 データパーティション NTFS 元々データ全部 list disk でディスクを一覧します。 select disk 1 で編集するディスクを選択します(ここではディスク1)。 list partition でパーティションを一覧します。この時点でデータパーティションだけ存在するはずです。 ESP システムパーティションの作成 create partition efi size=100 でパーティションを作成します。続けて format quick fs=fat32 label="System" としてフォーマットします。 ...
さくら VPS の前段にウェブアクセラレータを置いて Let's Encrypt を設定する
さくらの Web アクセラレータは無料枠のある CDN サービスです。これをさくら VPS の Nginx で運用するサイトの前段において、Let’s Encrypt で SSL 化したときの手順をまとめました。 Let’s Encrypt は certbot ツールが便利なのでこちらをインストールします。 SSL 証明書の発行には、ドメイン認証のために http 経由で http://対象ドメイン/.well-known/acme-challenge/ 下にあるワンタイムトークンを取得できるようにする必要があります。このパスは、https 化した後に http アクセスをリダイレクトする場合も除外するようにしておかないといけません。 Let’s Encrypt で証明書を発行 ルートディレクトリの設定 対象ドメインのルートディレクトリを作成します。 # mkdir /var/www/example.tilfin.net Nginx の設定 対象ドメインの server (http) ディレクティブ内に、下記の location ディレクティブを追加します。Nginx をリロードします。 server { listen 80; server_name example.tilfin.net; ... location ^~ /.well-known/acme-challenge/ { default_type "text/plain"; root /var/www/example.tilfin.net; break; } ... } certbot で証明書を発行 # certbot certonly -d "example.tilfin.net" -w /var/www/example.tilfin.net Saving debug log to /var/log/letsencrypt/letsencrypt.log How would you like to authenticate with the ACME CA? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1: Nginx Web Server plugin (nginx) 2: Spin up a temporary webserver (standalone) 3: Place files in webroot directory (webroot) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Select the appropriate number [1-3] then [enter] (press 'c' to cancel): ここで 3 を入力します。 ...
Ubuntu で RAM ディスクを設定して docker をオンメモリで動かすには
自分は Windows PC ホスト内で VirtualBox で Ubuntu 系 Linux Mint を起動しているのですが、docker のコンテナやボリュームデータをまるごとオンメモリで高速に動作させたいと思い、RAMディスクを設定して載せるようにしました。RAM ディスクだと OS をシャットダウンすると消えてしまいますが、Docker の場合、古いコンテナのデータがどんどん溜まっていくために掃除できて開発上好都合でもあります。 RAMディスクの設定 まずパス /ramdisk をマウントポイントとして RAM ディスクを設定することにします。 マウントポイントの作成 ディレクトリを作成して、スティッキービットを付けて誰でも書き込み可能にします。 $ sudo mkdir /ramdisk $ sudo chmod 1777 /ramdisk /etc/fstab の設定 /etc/fstab に下記を追記します。 tmpfs /ramdisk tmpfs defaults 0 0 一旦、OSを再起動します。(fstab に設定しなくても一時的には mount コマンドで対応することも可能) Docker のコンテナとボリュームを置き場を設定 以下の設定中は Docker を停止しておきます。 /ramdisk にディレクトリを作る $ sudo mkdir -p /ramdisk/docker/containers $ sudo mkdir -p /ramdisk/docker/volumes シンボリックを張る /var/lib/docker の containers, volumes が /ramdisk/docker を見るようにします。 $ sudo -i # cd /var/lib/docker # mv containers containers.old # mv volumes volumes.old # ln -s /ramdisk/docker/containers # ln -s /ramdisk/docker/volumes 動作確認 Docker を起動して、適当なコンテナを走らせて、 /ramdisk/docker 内にファイルが作られることを確認します。 ...
Twitter のアクセストークンをスクリプトで取得する
昨今、Twitter API の App 登録が審査など手順が増えたので、気軽に App を増やさなくなりました。そのため、既存の App に対して管理アカウントとは別のアカウントを認可させたいときがあります。 下記の Ruby スクリプトは gem oauth を使って、サーバを立てずにブラウザだけで認可してアクセストークンを取得するものです。認可後のリダイレクト時に付随するクエリ内容を適切に処理さえすれば、対象の Callback URL が 404 Not found であろうとも問題ないです。一時的にサーバを立てる必要もなく、一連の本来サーバサイドで行う処理をローカルから実行して処理できます。 なお Callback URL が自分の持ち物でないとセキュリティ上問題があるのは当然です。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 require 'uri' require 'oauth' CONSUMER_KEY = '<your consumer key>' CONSUMER_SECRET = '<your consumer secret>' CALLBACK_URL = '<your callback url>' consumer = OAuth::Consumer.new(CONSUMER_KEY, CONSUMER_SECRET, { site: "https://api.twitter.com", scheme: :header }) request_token = consumer.get_request_token(oauth_callback: CALLBACK_URL) req_url = request_token.authorize_url(oauth_callback: CALLBACK_URL) puts <<EOS Go to the following URL and authorize your account on a browser: #{req_url} Copy the redirected url from the browser and paste it: EOS input = STDIN.gets redirected_url = input.chomp rurl = URI.parse(redirected_url) params = {}.tap do |h| URI.decode_www_form(rurl.query).each do |entry| h[entry[0].to_sym] = entry[1] end end access_token = request_token.get_access_token(oauth_verifier: params[:oauth_verifier]) puts <<EOS =================================== user_id: #{access_token.params[:user_id]} screen_name: #{access_token.params[:screen_name]} Access Token: #{access_token.token} Access Token Secret: #{access_token.secret} =================================== EOS スクリプトを実行すると authorize URLが生成されるので、それを認可したいアカウントでサインイン済みのブラウザに貼り付けて移動します。そのまま Twitter の認可画面でボタンを押すと、本来自分のサービスのURLにリダイレクトされます。この状態でURL欄を中身コピーして、貼り付けます。そうすると続きを処理してアクセストークンして出力します。
GraphQL DataLoader の実践的な使い方
DataLoader とは GraphQL におけるデータ取得において内部で N+1 処理になるのを回避するためのツールです。例えば、あるリソース(A)一覧とそれに関連付けられた 1:1 のリソース(B)を一気に要求すること場合、最初のリソース(A)リストをクエリした後に、各レコードに対してリソース(B)を毎回クエリすると遅くなります。リソース(B)に対してそのキーをスタックしていき、まとめてクエリすることで N+1 回を 1+1 回に減らすことができます。この機能をまとめたものが DataLoader です。 実践的な使い方 条件検索について DataLoader はあくまでも Key-value キャッシュ付きストアと考えた方がいいです。GraphQL の検索 Query で条件指定する場合は DataLoader を経由せずに直接行います。但し結果はその後の下階層で再度利用する可能性があるので prime メソッドでキャッシュしておきます。 キーによるレコード検索について マスターリソースに対する場合は、単純に主キーで取得およびキャッシュします。 リレーションリソースに対する場合は、複合キーが cacheKeyFn の処理によってユニークな文字列になるように変換します。サロゲートキーは使いません。 要求したユーザーによって結果が変わる場合 この場合は、リクエストコンテキストに応じて DataLoader インスタンスを生成し付与します。コンテキストを生成するミドルウエア内で、DataLoader インスタンス自体をセットするか、それらインスタンスを生成できるメソッドを提供するといいでしょう。 どのレイヤーのデータを DataLoader で扱うべきか GraphQL の Type とデータベースのレコードスキーマが完全に一致せずに、データの変換が必要となる場合の問題です。基本的にデータベースレイヤーに DataLoader を適用して、GraphQL Type には都度変換する方がいいと考えます。この理由としてリゾルバ周りの変更の影響を受けないことや、 N+1 はそもそもデータベース(ストレージ)への負荷であるからです。 コンテキストから DataLoader をどうモデルに渡すか コンテキストはリゾルバのパラメータとして受け取りますが、DataLoader インスタンスは下位レイヤーのモデル(ロジック層やストレージ層)で使いたいので、そのままストレートにモデルのコンストラクタなどで渡すのが一番だと思います。これに引っ張られて Active-Record パターンは JavaScript ベースでは使いずらいと思います。Ruby on Rails 等だとスレッドに情報をセットして、透過的に扱うことも可能ですが。
DynamoDB DataMapper For JavaScript でインデックス付きテーブルを作成するには
Amazon DynamoDB DataMapper For JavaScript という TypeScript にも標準対応している DynamoDB の JavaScript 向けデータマッパーライブラリがあります。 このライブラリはテーブル作成・削除の機能もあるので、テスト用テーブル作成もこちらを使ってスクリプト化しようと考えました。 ただドキュメントが少なく、グローバルセカンダリインデックスを付与する方法を探すのに手間取ったため、ここにメモしておきます。 スキーマ定義とテーブル作成 プライマリキー(通常のHASHキー)は user_id です。 外部キー(GSIのHASHキー)は email です。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import { attribute, hashKey, table } from '@aws/dynamodb-data-mapper-annotations' @table('users') class User { @hashKey({ attributeName: 'user_id' }) userId!: string @attribute({ indexKeyConfigurations: { 'emailIndex': 'HASH' } }) email!: string } async function createTable() { await mapper.ensureTableExists(User, { readCapacityUnits: 1, writeCapacityUnits: 1, indexOptions: { 'emailIndex': { type: 'global', projection: 'keys', readCapacityUnits: 1, writeCapacityUnits: 1, } } }) } createTable() .then(() => { console.info('Created DynamoDB table users') }) .catch(err => { console.error(err) }) 結果確認 $ aws dynamodb describe-table --table-name users --endpoint-url http://localhost:4569 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 { "Table": { "TableArn": "arn:aws:dynamodb:us-east-1:000000000000:table/users", "AttributeDefinitions": [ { "AttributeName": "user_id", "AttributeType": "S" }, { "AttributeName": "email", "AttributeType": "S" } ], "GlobalSecondaryIndexes": [ { "IndexSizeBytes": 0, "IndexName": "emailIndex", "Projection": { "ProjectionType": "KEYS_ONLY" }, "ProvisionedThroughput": { "WriteCapacityUnits": 1, "ReadCapacityUnits": 1 }, "IndexStatus": "ACTIVE", "KeySchema": [ { "KeyType": "HASH", "AttributeName": "email" } ], "IndexArn": "arn:aws:dynamodb:ddblocal:000000000000:table/users/index/emailIndex", "ItemCount": 0 } ], "ProvisionedThroughput": { "NumberOfDecreasesToday": 0, "WriteCapacityUnits": 1, "LastIncreaseDateTime": 0.0, "ReadCapacityUnits": 1, "LastDecreaseDateTime": 0.0 }, "TableSizeBytes": 0, "TableName": "users", "TableStatus": "ACTIVE", "KeySchema": [ { "KeyType": "HASH", "AttributeName": "user_id" } ], "ItemCount": 0, "CreationDateTime": 1560437300.665 } }