maybe daily dev notes

私の開発日誌

EtherscanのContract verificationをAPIで実行する

渦中のEthereumネタ。

はじめに

Etherscanでコントラクトのアドレスを開くと、コードが表示されることがある。

Contractのコード

コントラクトのユーザーたちは、このコードを見ることで中の実装を把握でき、安心してコントラクトを利用することができる。 実はEthereum自体にはデプロイされたスマートコントラクトのコードを取得する機能はなく、これはEtherscanがオフチェインで提供している機能である。

この機能を使うためには、コントラクトをデプロイした後、Etherscanにコンパイルの構成情報とソースコードをEtherscanに教えれば良い。これによりEtherscanが改めてコードをコンパイルし、デプロイされたコントラクトとコードが合致することを検証する。これは次のページから申請することができる。Verify & Publish Contract Source Code

今回の記事では、この作業をプログラマブルかつhardhatなどのツールを使わずに行う方法を説明する。なお、この公式ドキュメントに同じタイトルのページがあるが、このページは情報が古く不完全である。今回紹介する方法は、最新でかつより美しい形 (後述) でCodeをEtherscanに登録することができる。

方法

公式ドキュメントどおりにやる(80点の)方法

Etherscan 公式ドキュメントに従えば、次の手順で実行できる:

  1. コントラクトをコンパイル・デプロイし、コントラクトのアドレスを得る
  2. コントラクトのソースコードをflattenする。flattenとは、すべてのインポートを削除し、インポート対象のコードを1ファイルに含めてしまうこと。これには solidity-flattener と呼ばれるツールを使えば良い。大抵各言語でプログラマブルに呼び出し可能なものが開発されており、例えば nodejs, python など。
  3. 下記をパラメータとして、Etherscan APIを実行
  4. VerificationリクエストのGUIDが返ってくる
  5. GUIDを使い、適宜こちらのAPIでVerificationの進行状況をポーリング
  6. 通常10秒程度でVerificationが完了する

この方法では、flattenされたコントラクトコードがそのままEtherscanに登録されるため、ユーザーたちはコントラクトの実装を読みづらい状態となってしまう。例えば、こちらのコントラクトがその状態。

100点の方法

こちらの方法を使えば、コードをflattenせずに登録することができる。例えばこのコントラクトのように。

この方法では、先に紹介した方法のうち2と3だけを変えれば良い。実はVerification用のAPIcodeformat: 'solidity-standard-json-input' と入力すると solcのstandard json input形式を入力することができるので、これを活用する。

まず、送るべきAPIリクエストは以下。

import fetch from 'node-fetch';
// ...
  await fetch(etherscanApiEndpoint, {
    method: 'POST',
    body: new URLSearchParams({
      apiKey: etherscanApiKey,
      module: 'contract',
      action: 'verifysourcecode',
      codeformat: 'solidity-standard-json-input',
      compilerversion,
      constructorArguements,
      sourcecode: standardJson,
      contractaddress,
      contractname,
    }).toString(),
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  });

それぞれの変数の中身を解説していく。

  • etherscanApiEndpoint: Etherscan APIのエンドポイント。Endpoint URLs - Etherscan に掲載されている。Networkごとにエンドポイントが異なるので注意。
  • etherscanApiKey: Etherscan APIAPIキー。ユーザー登録すれば入手可能
  • compilerVersion: デプロイしたコントラクトをコンパイルしたsolcのバージョン。ここ に掲載されている文字列以外はエラーになるので注意。例えば solc-js は以下のような文字列を返すので、不要な部分を削除する必要あり (ここでは .Emscripten.clang の部分。)
> const solc = require('solc')
> solc.version()
'0.8.13+commit.abaa5c0e.Emscripten.clang'
  • constructorArguements: コントラクトをデプロイした際に指定した、コンストラクタの引数をABIエンコードしたもの。これは web3.js を使うと、次のコードで取得できる。
import Web3 from 'web3';
// ...
  // コントラクトをコンパイルして得られるABIとBytecode
  const abi = {...};
  const bytecode = '0x...';
  // コントラクトをデプロイ
  const contract = new Web3.eth.Contract(abi);
  const call = contract.deploy({
    data: bytecode,
    arguments: [1, 2, 3, 4], // コンストラクタ引数
  });
  const constructorArguments = call.encodeABI().slice(bytecode.length);
  // ....

call.encodeABI() で得られる文字列は以下のフォーマットになっている

0x[コントラクトのバイトコード][ABIエンコードされたコンストラクタ引数]

ので、最初のコントラクトのバイトコードを削除すれば、ABIエンコードされたコンストラクタ引数が得られるという仕組み。

ちなみにArguementsはArgumentsのtypoと思われるが、API後方互換性を保つために今となっては直すに直せないのだろう。

  • standardJson: これが肝。このドキュメント のとおりにJSONを作る必要がある。シンプルにすると、概ね以下のようなJSONを作ればOK。
{
    "language": "Solidity",
    "sources": {
        "input.sol": {
            "content": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.0;\n\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\nimport \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\nimport \"erc721a/contracts/ERC721A.sol\";\nimport \"@openzeppelin/contracts/utils/Strings.sol\";\n\ncontract MyContract is Ownable, ERC721A, ..."
        },
        "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
            "content": "..."
        },
        "erc721a/contracts/ERC721A.sol": {
            "content": "..."
        },
        ...
    },
    "settings": {
        "optimizer": {
            "enabled": true,
            "runs": 200
        },
        "outputSelection": {
            "input.sol": {
                "*": [
                    "evm.bytecode.object",
                    "abi",
                    "evm.deployedBytecode"
                ]
            }
        }
    }
}

要は sources フィールドに必要なすべてのファイルのソースコードを含めればOK。↑の例で input.sol がデプロイするコントラクト、それ以外が import で依存しているコントラクト。依存するコントラクトを含めるのが少し厄介なところだが、一応次のコードで達成することができる。

import * as fs from 'fs';
import solc from 'solc';
let standardJson: any;

const getStandardJson = async (fileName: string, contractName: string) => {
  const file = fs.readFileSync(fileName).toString();
  const output = 'input.sol';
  const input = {
    language: 'Solidity',
    sources: {
      [output]: {
        content: file,
      },
    },
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      outputSelection: {
        [output]: {
          '*': ['evm.bytecode.object', 'abi', 'evm.deployedBytecode'],
        },
      },
    },
  };
  // standardJson を初期化
  standardJson = input;
  solc.compile(JSON.stringify(input), { import: findImports });
  return standardJson;
};

const importCache: { [key: string]: { contents: string } } = {};
const findImports = (path: string) => {
  if (importCache[path] == null) {
    const file = fs.readFileSync(`node_modules/${path}`);
    importCache[path] = {
      contents: file.toString(),
    };
  }
  // standardJson に依存するソースコードを追加
  standardJson.sources[path] = { content: importCache[path].contents };
  return importCache[path];
};

要は、コンパイルして依存するソースコードを拾っていこうというアイデア。明らかに無駄の多い方法ではあるが少なくとも動作するので、一旦こうしている。ベターな方法を知っている方いれば教えて下さい。

また、もちろん settings.optimizer の部分は実際にコントラクトをコンパイルしたときの設定と同一にすること。これで standardJson は得られた。

  • contractaddress: デプロイしたコントラクトのアドレス
  • contractname: コントラクトのエントリポイントを特定するための文字列。↑の例の場合は input.sol:MyContract とする。要はメインのコードのファイル名とコントラクトの名前を : でつなげればOK。

 これでEtherscanへのAPIリクエストを作ることができた!あとは↑のコードを参考にPOSTリクエストを送信すればOK!

まとめ

Hardhatなどを使わずにAPI経由でEtherscanでコントラクトをverifyする方法を紹介した。現状、この方法を見つけるためだけにわざわざHardhatのコードを読み込む必要があるという悲しい現実もある。この方法が正しくEtherscanのドキュメントにも明記されることを望む。

AWS CDKでWebフロントエンドをデプロイする3つの方法

CDKでWebフロントエンドのSPAをデプロイするときの方法まとめ。下図のありがちなSPA構成を想定している。

このような場合、フロントエンドアプリからAPI GatewayのエンドポイントURLやCognitoのUser Pool IDなどの値を参照する必要があって、それらの値をどのように解決するか、どうデプロイするかなどに選択肢がある。早速、それらの方法を見ていこう。

方法

以下では、フロントエンドが参照すべきリソースを 依存するリソース と呼ぶ。(上図では API Gateway や Cognito User Pool のこと。)

方法1. 依存するリソースをデプロイ後、フロントエンドをデプロイする

依存するリソースをまず先にデプロイし、参照したい値 (エンドポイントURLなど) を確定させる。これらの値をフロントエンドのコードにハードコードするか、ビルド時の環境変数として注入することで、フロントエンドから参照できるようにする。

コードは単純で、BucketDeployment でビルドした静的ファイルをS3にコピーするだけ。

// これをデプロイする前に以下を完了させる
//    1. 依存するリソースのデプロイ
//    2. 依存するリソースの値 (エンドポイントURL) などをフロントエンドに埋め込み
//    3. フロントエンドのビルド
// ビルド生成物が frontend/dist に出力されたものとする
    new BucketDeployment(this, 'Frontend', {
      sources: [
        // frontendのビルド生成物のディレクトリを指定
        Source.asset('frontend/dist'),
      ],
      destinationBucket,
      distribution,
    });

割と直感的なので、よく使われている方法と思う。しかし、欠点がいくつかある:

  • 依存するリソースとフロントエンドのデプロイを1つのスタックにまとめることができない
    • 依存するリソースがデプロイされるまではフロントエンドのビルドができないので、デプロイの単位を分ける必要がある
    • もちろん、依存するリソースをデプロイした後に同スタックにフロントエンドを追加すれば1スタックにできるが、そうするとスタックの作り直しや環境の追加などメンテが大変になるので、やらない前提
  • 値を注入するために、CDK外で何らかの手作業やコードが必要になる
    • 例えばデプロイされたREST APIのエンドポイントを手作業でフロントエンドのコードにコピーしたり
      • 環境が増えてきたときや、環境を頻繁に作り直す場合は、手間になる
    • 例えば依存するリソースを含むスタックの Stack output から値をCLIで取ってきて、ファイルに書き出すなど
      • CDKの事情がフロントエンド側に漏れてしまっている感はある
      • ちなみに s3-assets.AssetProps.bundling を使うと、CDK synth時にこのコマンドを自動で実行させるようなことは可能

方法2. S3Deployment の機能で値をファイルとして書き出す

方法1の欠点を意識してか、数ヶ月前に追加されたのがこの機能。以下のようなコードで、依存するリソースの値をCDKのデプロイ中にファイルとしてS3に書き出すことができる。

const appConfig = {
  topic_arn: topic.topicArn,
  base_url: 'https://my-endpoint',
};

new s3deploy.BucketDeployment(this, 'BucketDeployment', {
  sources: [s3deploy.Source.jsonData('config.json', config)],
  destinationBucket: destinationBucket,
});

この書き出されたファイルをフロントエンドから動的にロードすることで、フロントエンドは動的に値を解決することができる。 実際にこの仕組みを利用した例が ↓のリポジトリ

github.com

書き出したファイルを、Reactコンテキストの初期化時に取得することで、必要な値を注入している。

const fetcher = async () => {
  const res = await fetch('/env.json')
  return res.json()
}

export default function useEnv() {
  const { data: env } = useSWR<Env>(dev ? null : '/env.json', fetcher, {
    revalidateOnFocus: false,
    revalidateOnReconnect: false,
  })
// https://github.com/aws-samples/nextjs-authentication-ui-using-amplify-ui-with-cognito/blob/main/app/lib/useEnv.ts

この方法では、フロントエンドをビルドする段階では値を解決しなくて良い。このため、方法1とは異なり、フロントエンドと依存するリソースを1スタックでまとめてデプロイすることができるのは利点。

欠点としては、以下が考えられる:

  • 現状クロススタックで値を解決できないバグがあるため、方法1とは逆にスタックを分割するのが困難
    • 元の実装はCDKの創造主である eladb によるものだが、かなりハッキーに見えるので、根本解決には時間を要すると(完全外野からの)予想
    • 一応、SSM Parameterを経由することで値を解決するWorkaroundはある
  • フロントエンド側で上記のような特別な実装が必要
    • エンドポイントURLをネットワークから動的に読み込むのは一般的とは言えない
    • これもCDKの事情がある種リークした結果で、あまり好ましくはない

方法3. CDKデプロイ中にフロントエンドをビルドする

この記事で推したい方法。そもそもこれらの問題が起きる原因は、依存するリソースの値はそれらをデプロイするまで確定せず、またフロントエンドアプリはそれらの値が確定するまでビルドできないという点が主だった。このために、フロントエンドを別にデプロイしたり、リソースの値を動的に取得するような措置をしていた。

もう一つ簡単な解決方法があって、それがデプロイ中にフロントエンドをビルドするというもの。CloudFormationのカスタムリソースの中でフロントエンドをビルドすれば、依存するリソースの値も同スタック内で解決できるし、フロントエンドのビルドの中に静的に値を埋め込むことができる。 リポジトリはこちら:

github.com

これを使うと、以下のインターフェースでフロントエンドアプリをデプロイできる。*1

import { NodejsBuild } from 'deploy-time-build';

declare const api: apigateway.RestApi;
declare const destinationBucket: s3.IBucket;
declare const distribution: cloudfront.IDistribution;
new NodejsBuild(this, 'ExampleBuild', {
    assets: [
        {
            // フロントエンドのソースコードがあるローカルのディレクトリ
            path: 'example-app',
            exclude: ['dist', 'node_modules'],
        },
    ],
    destinationBucket,
    distribution,
    // Build artrifactが出力されるディレクトリ
    outputSourceDirectory: 'dist',
    // Buildに使うコマンド
    buildCommands: ['npm ci', 'npm run build'],
    // ビルド環境に注入する環境変数 (deploy-time valueも利用可能)
    buildEnvironment: {
        VITE_API_ENDPOINT: api.url,
    },
});

これなら1スタック構成でもマルチスタック構成でも同じように対応できる。また、フロントエンド側にCDKの知識は必要なく、よくある環境変数を埋め込む方法を利用できる。方法1や2の欠点が解消されたことがわかる。

一方で欠点もあり、

  • ビルドという割と複雑な作業を、汎用的なConstructに実行させている
    • 環境によってビルドにまつわる事情は無数にあると思われるので、もしかするとこの汎用Constructでは吸収できない場合もあるかもしれない
  • CDKデプロイのたびに (フロントエンドのコードに変更があれば) フロントエンドがビルドされるので、デプロイの時間が長くなりがち
    • 例えばバックエンドのフロントエンドを1スタックにまとめて、 cdk watch しているような場合、開発効率が悪化するかもしれない
    • このため、開発環境においてはフロントを別スタックに隔離するなどの措置が必要かも

とはいえ、現状趣味プロジェクトで使っている分には、今のところバッチリ動作しているし、特に不都合もないので満足している。

まとめ

CDKでWebフロントエンドをデプロイする方法を3つ紹介した。それぞれPros Consがあるので、ぜひ一度お試しください。

*1:deploy-time valueとはCDK用語で、デプロイ時に確定する値のこと。リソースのARNやREST APIのエンドポイントなど。

npm install は package-lock.json を変更しないことがほとんどだった

昨日までずっと npm install の挙動について勘違いしていたので、改めて整理する。

なお、引数付きの npm install <package_name> コマンドではなく、 npm install 単体で打つときの話。

npm install の仕様

npm 5.4.2 以後 (つまり、現代ではほとんどの環境がこちら)

  1. はじめに、既存の package-lock.jsonpackage.json に齟齬がないか確認する。
  2. 齟齬がなければ、 package-lock.json のとおりにインストールする。この時、 npm ci とは異なり、既存の node_modules ディレクトリは削除されない。
  3. 齟齬がある場合、 package.json に記載のバージョンに適合するように、 package-lock.json が変更される。

ここで齟齬と言っているのは、package.json に書かれたバージョン指定が実際にインストールされているバージョンと整合しないことを指す。 例えば "web3": "^1.7.3"package.json に書かれているのに対して、 package-lock.json では "web3": "1.7.2" がインストールされている場合など。

このため、一度 npm install して package-lock.json を生成したら、 package.json を変更しない限りは何度 npm install の結果はdeterministicであることが保証されている。

この仕様は、GitHubのIssueコメント欄にnpmメンテナが書いたものが由来なので、それなりに信ぴょう性がある。なぜかドキュメントには明記されていないのでやや不安はあるが。

github.com

ただし、

If you do run into a case where npm@^5.4.2 mutates a package-lock.json that was otherwise compatible with the paired package.json please open a new issue. This sort of thing would constitute a high priority bug.

というのが曲者で、 要は package-lock.json が (本来変更されないはずの場合でも) 変更されてしまうようなバグはありえるということ。

実際、2021/3にこのようなIssueが立っていて、現状は不明のまま。

github.com

とはいえ、仕様がこうなっているということは、いずれはバグも修正されて仕様どおりの挙動になるのだろう。 このため、本番用のビルドなどクリティカルな用途以外では、仕様を信じて npm install を使っても良さそう。

npm 5.4.2 より前 (つまり、現代では無関係)

先のIssueを見るかぎり、npm install はまったく package-lock.json を無視していたらしい。 (そもそもnpm@4以前は package-lock.json すらなかった模様) このStackoverflowも参考になる:

stackoverflow.com

5.4.2 がリリースされたのが 2017/9 頃 らしいので、その以前から npm を使っていた人は誤った認識を持っているかもしれない。

自分の勘違い

但し書き: この節に書いてあることはすべて勘違いで誤っているので注意。

  • npm install は常に package-lock.json を無視してパッケージをインストールするコマンドである。npm update との違いはよくわからない
  • package-lock.json のとおりにインストールするコマンドは npm ci のみだ
  • npm ci は実行のたびに node_modules ディレクトリを削除して全てのパッケージを取得し直すので、なんて非効率なことか、このIssue を未だに放置している現状は異常としか言いようがない
  • package-lock.json のとおりにかつ既存のnode_modulesディレクトリを残しながらパッケージをインストールするには、 yarn install --frozen-lockfile しかない
  • この点だけで npm を捨てて yarn に移行するまである!

上記の仕様理解が正しいとすれば、これらは完全に勘違いだったことになる。 古い誤った知識をアップデートできずただただ文句を言うだけだった自分が恥ずかしい。懺悔します。

言い訳

勘違いし続けたのは、ちまたの言説で「CIサーバーでは必ず npm ci を使え」と言われすぎているからだと思う。

実際は上記の仕様なのであれば、 npm install を使っても依存関係のインストールは deterministic になるので、実用上の問題があるとは思えない。 むしろ、 npm installpackage-lock.json が更新されるということは package.jsonpackage-lock.json に不整合がある状態ということである。 この状態を早めに検知するために npm install を使うほうが良いのではとすら思う。

ただし一点不安なのは、npm のバグにより 意図せず package-lock.json が更新されてしまう場合。 実際自分もこれまでそのようなことがあった気がするのである。 これが起きるとするなら、npm install を使う CI/CD が生成するアーティファクトを信用できなくなる。 それなら、リスク0の npm ci を使うという選択は妥当だろう。

「CIサーバーでは必ず npm ci を使え」と唱える人は、背景をそこまで説明してほしかったなという気持ちになる。

まとめ

まとまらないが、自分は npm install の仕様を信じて、開発時は基本 npm install 一本で行きたいと思う。

AWS CDKでアプリをデプロイしていると、 npm ci の挙動がデプロイ速度の律速になることがまれにあるため。 これで yarn を導入せずに済むな。

AWS Lambdaだけでアンケートフォームを作れる

Lambda function URL活用案件として、Lambdaだけを使ってアンケートフォームを作ってみた。 実際は回答の閲覧用にSlackも使ってるが、ほかは本当にLambdaだけ。

アーキテクチャ

デモはこれ。 (いつか消すかも)

コード

冗長な部分もあるので、要点だけ抜粋。フルのコードはこちらに載せた

これをLambdaのマネコンでデプロイし、Function URLを設定すれば良い。 また、 SLACK_WEBHOOK_URL 環境変数を設定すれば、アンケート結果をSlackに送信できる。

const https = require('https')

exports.handler = async (event) => {
  console.log(event)

  const req = event.requestContext.http;
  const sourceIp = req.sourceIp;
  // can perform IP address restriction here
  // e.g. if (sourceIp != "11.4.51.4") throw new Error()

  if (req.method == 'GET') {
    if (!(req.path == '/')) {
      return {
        statusCode: 404,
        body: 'Not found',
      };
    }
    return {
      statusCode: 200,
      body: getHTML(),
      headers: {
        'content-type': 'text/html'
      }
    };
  }
  else if (req.method == 'POST') {
    const response = JSON.parse(event.body);
    await processResponse(response);
    return {
      statusCode: 200,
      body: 'success',
    };
  }
  return {
    statusCode: 400,
    body: 'Bad Request',
  }
};

const processResponse = async (response) => {
  const webhookUrl = process.env.SLACK_WEBHOOK_URL;
  await post(webhookUrl, response);
};

const post = async (url, data) => {
  // c.f. https://stackoverflow.com/questions/40537749/how-do-i-make-a-https-post-in-node-js-without-any-third-party-module
}

const getHTML = () => {
  return `
<!doctype html>
<html>
  <head>
    <meta charset="UTF-8" />
    <script src="https://cdn.tailwindcss.com"></script>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
  </head>
  <body>
    <div class="flex container mx-auto py-10">
      <div class="grow">
        <form id="myForm">
          <div class="mb-6">
            <label for="q1" class="block mb-2 text-sm">
              Q1. 名前を教えて下さい。</label>
            <input
              type="text"
              id="q1"
              name="q1"
              placeholder="たかし"
              required
            />
          </div>
          <div>
            <button
              type="submit"
            >
              Submit
            </button>
          </div>
        </form>
      </div>
    </div>

    <script>
      $('#myForm').submit(function (event) {
        event.preventDefault();
        var $form = $(this);
        var $button = $form.find('button');
        $.ajax({
          url: '/',
          type: 'POST',
          data: JSON.stringify(
            $form.serializeArray().reduce((json, { name, value }) => {
              json[name] = value;
              return json;
            }, {}),
          ),
          headers: {
            'Content-Type': 'application/json',
          },
        });
      });
    </script>
  </body>
</html>
`;
}

Function URLをブラウザで開くと、以下のようなアンケートフォームが表示される。

送信すると、Slackに連携される。

基本的な機能が揃っていることはお分かりいただけただろうか。

ポイント

1. ホスティングが単一のLambdaで完結する

Lambda Function URLはAmazon API Gatewayの必要性を低減してくれた。今回も、この程度の要件であればLambda単体で十分処理可能。

Lambdaを1つマネコンでデプロイすれば完結する単純さも魅力。 何らかの事情でGoogleフォームなどサードパーティー製ツールを利用できない方は、検討してみてはいかが。

ちなみに今は結果をSlackに連携しているが、上記の processResponse の実装を変えれば、任意の連携を実現可能。 例えばDynamoDBに保存したり、Amazon SNSに送信したりといったように。

index.js 1つで完結させるために

今回は完全手作業でのデプロイを想定し、可能な限りLambdaの構成をシンプルにしている。

例えば、ファイルが複数になると急激に手作業でのデプロイが面倒になる。 HTMLなどは別ファイルに分けたほうが本来見通しが良いが、getHTML 関数に押し込めている。

また、ライブラリを使うとBundlingが必要になり、これも単純な手作業ではデプロイできなくなる原因。 このため、バックエンドの実装はライブラリを一切使っていない。

フロントエンドについては CDN経由でライブラリの読み込みが可能なので、 DOM操作のために jQuery (初めて触った!) だけ使っている。jQuery、もう死んだものだと思ってたが、ここ1年リリースがなくたしかに死んでそうだった。

ただ、ReactなどモダンなフレームワークはWebpackの利用を前提としている感があり、案外超Lightweightな用途では使いづらい気がした (preactもつらそう。) jQueryだけだと古風すぎるので、HTMLの色付け用にTailwindを使っている。

フォーム画面作るのにHTML/CSSの知識が必須なので、そういうのができる人に利用は限られると思う。

セキュリティ面

Lambda Function URLは現状ビルトインの認証が IAM 認証しかないが、IAM認証 はブラウザからのアクセスでは使いづらい。

今回は認証なしでPublicアクセスを前提としている。 一応、入力されたEventからクライアントのIPアドレスを取れるので、IPアドレス制限くらいは容易に実現できる。

exports.handler = async (event) => {
  const req = event.requestContext.http;
  const sourceIp = req.sourceIp;
  // 簡易なIPアドレス制限
  if (sourceIp != "11.4.51.4") throw new Error()

Basic認証を実装したら便利そうだと思ったが、なぜか現状は使えないようだった。 (www-authenticate ヘッダーが自動でDropされるような挙動を示す)

まとめ

Lambdaだけでアンケートフォームを作ってみた。

メリットは、

  • インフラは管理不要
  • 極限まで簡単な構築手順
  • 画面は無限にカスタマイズ可能

何らかの理由でこの手のSaaSが使えないという方は、検討してみても良いかも。

もつ名チートシート

もつをよく食べるのだが、部位の名前が異常に覚えられない。おそらく別称がたくさんあるため混乱するのだと思われる。

ここでは自分が食べたことのあるものに限って簡単な早見表をつくる。

全般

  • もつ: 臓物(ぞうもつ)、内臓のこと
  • ホルモン: 放る(捨てる)もん(もの) モツの別称

ぼんじりや軟骨などは内臓ではないため厳密にはモツとは呼べない気がするが、現実のモツ屋では混用されている。

牛は腸とか胃が多い。

  • ミノ: 第1の胃。普通に焼肉屋にも置いてあるメジャー選手。
  • ハチノス: 第2の胃。蜂の巣状の構造が見られることから。
  • センマイ: 第3の胃。見た目は灰色でトゲトゲしている。コリコリしてうまい。たまに臭みあり。
  • シマチョウ(テッチャン): 大腸。噛み切れない。
  • マルチョウ(コテッチャン): 小腸。別称がかわいい。
  • ハラミ: 横隔膜周辺の肉。もはやモツ感のないほぼ普通の肉。

豚は牛よりも多くの部位が食用される。

  • シロ: 大腸。これが豚ホルモンの代表格。
  • カシラ: 顎の筋肉。ほぼ肉。たまに臭みあり。
  • ガツ: 胃。あまり印象がない。
  • コブクロ: 子宮。クセがなくてコリコリしてる。うまい。
  • テッポウ: 直腸。やわらかい。たまに臭いのでタレ推奨。
  • ハラミ: 横隔膜周辺の肉。ほぼ普通の肉。
  • ハツ: 心臓。これもほぼ肉。
  • ノドブエ: 声帯。もつ屋で軟骨といったらだいたいこれがでてくる印象。

鳥はほぼ余すところなく食用される。

  • キンカン: 黄卵の前身。今度食べる。
  • レバー: 肝臓。日本で最も有名なもつ。
  • ぼんじり: 鳥が羽に付ける油がでてくるお尻のパーツ。脂っこい。
  • ハツ: 心臓。 レバーもどきな印象。
  • 砂肝: 胃の一種(砂嚢)。鳥はここで食べ物をすりつぶすらしい。うまい。
  • やげん軟骨: 胸骨の先端についている軟骨。
  • せせり (ネック、こにく): 首周りの筋肉。ほぼ肉。

まとめ

だいたいこれくらい覚えておけば9割方カバーできそう。 調べると他にも内臓の数だけ部位名がでてくるが、実際の店で見るものは少ない。

また、動物の種類が違っても呼び方が同じ部位がある (これが混乱を招く) ので、店で頼む際は注意したい。

ちなみにカバー写真のもつはただのイメージで、これほど高級なもつは食べたことがない。おすすめの店はここ

参考資料

NodeJsFunctionのビルド時に依存関係を解決する3つの方法

タイトルの通り、AWS CDKのNodeJsFunctionを使う時に依存関係を解決する方法が数パターンあるので比較してみる。

以下のTypeScriptのLambdaをデプロイするものとする:

// よくあるAPI GatewayのLambda Authorizer
import { APIGatewayRequestAuthorizerHandler } from 'aws-lambda';
import { verify, decode } from 'jsonwebtoken';
export const handler: APIGatewayRequestAuthorizerHandler = async (event, context) => {
  const token = event.headers?.Authorization;
  if (token == null) return denyPolicy;
  if (verifyToken(token)) {
    return allowPolicy;
  }
  return denyPolicy;
};

const verifyToken = (token: string) => {
  const decoded = decode(token, { complete: true });
  return true;  // WIP
};

const allowPolicy =  {  /* WIP */ };
const denyPolicy = {  /* WIP */ };

また、フォルダ構造は以下のようになっているとする:

.                               # CDKプロジェクトのルートディレクトリ
├── bin
│   └── cdk.ts
├── backend
│   └── authorizer              # Lambda関数のコード
│       ├── index.ts            # LambdaのTypeScriptコード
│       ├── package.json        # ① Lambda用のpackage.json 
│       └── package-lock.json
└── lib
│   └── stack.ts                # CDKのスタック定義
├── cdk.json
├── package.json                # ② CDKのpackage.json
└── package-lock.json

aws-lambda はただの型定義のためesbuildに無視されるので良いとして、jsonwebtoken ライブラリは依存関係として解決される必要がある。

方法の候補

方法1. rootのpackage.jsonに書く方法

上のツリーで②の package.json に必要な依存関係 (ここでは jsonwebtoken )を書く方法 。 CDK側のコードは以下で、最もシンプルに済む。

    new NodejsFunction(this, 'AuthHandler', {
      entry: 'backend/authorizer/index.ts',
    }

また、ルートディレクトリで一度 npm ci を実行すれば必要なインストールがすべて完了するのも良い点。

一方明らかなデメリットは、すべての依存関係が一つの package.json に集約されるので、各Lambdaが実際にどのライブラリに依存しているのか不明瞭になること。

例えば3つのLambdaがある場合に、それらの依存関係を一つの package.json で管理すると、以下のような状態になる。これでは誰が何のライブラリを使っているのかわからず、ライブラリの更新・削除などが大変になることが予想できる。

{
  "name": "cdk",
  "bin": {
    "cdk": "bin/cdk.js"
  },
  "scripts": {
    ...
  },
  "devDependencies": {
    ...
  },
  "dependencies": {
    "@types/aws-lambda": "^8.10.93",
    "aws-cdk-lib": "2.20.0",
    "constructs": "^10.0.0",
    "jsonwebtoken": "^8.5.1",  // Lambda 1の依存関係
    "extract-zip": "^2.0.1", // Lambda 2の依存関係
    "node-fetch": "^3.2.4" // Lambda 3の依存関係
  }
}

とはいえ、あまり依存関係がないLambdaや、全Lambdaで共通の依存関係がある場合は、この方法でも十分機能する。

方法2a. 個別のpackage.jsonに書く方法

方法1のデメリットを解消したい場合は、Lambdaごとにpackage.json を作成(上のツリーで①のファイル)して、そこに依存関係を書けば良い。

CDK側の定義は方法1と同じでOK。

    new NodejsFunction(this, 'AuthHandler', {
      entry: 'backend/authorizer/index.ts',
    },

個別の package.json で依存関係を管理するので、方法1のデメリットは解消される。

一方デメリットは、各Lambdaのディレクトリで npm ci などを実行して node_modules がローカルにインストールされた状態でなければ、CDKのsynthesizeが失敗してしまうこと。 例えば以下のようなエラーを目にしたことのある方も多いのではないだろうか。

[ERROR] Could not resolve "jsonwebtoken"
    index.ts:4:20:
      4 │ import { verify, decode } from 'jsonwebtoken';
        ╵                                 ~~~~~~~~~~~~~
  You can mark the path "jsonwebtoken" as external to exclude it from the bundle, which will remove this error.

このあたりを読むとわかるが、CDK は esbuild の bundling 前に自動で npm ci を実行してくれない。このため、esbuild は必要な依存関係を解決できずに失敗する。

このデメリットは、複数人で1プロジェクトを開発しているときに顕著になる。 例えば以下のようなプロジェクト構成の場合、開発者1が cdk deploy するためには、lambda1, 2, 3 すべてのディレクトリで npm ci を実行する必要がある。

.                               # CDKプロジェクトのルートディレクトリ
├── bin
│   └── cdk.ts
├── backend
│   ├── lambda1                 # 主に開発者1が担当
│   │   ├── index.ts
│   │   ├── package.json
│   │   └── package-lock.json
│   ├── lambda2                 # 主に開発者2が担当
│   │   ├── index.ts
│   │   ├── package.json
│   │   └── package-lock.json
│   └── lambda3                 # 主に開発者3が担当
│       ├── index.ts
│       ├── package.json
│       └── package-lock.json
├── lib
│   └── stack.ts
├── cdk.json
├── package.json
└── package-lock.json

これは人によっては面倒に感じるだろうし、環境構築の手間が増える意味でも好ましくはないだろう。

とはいえ一度インストールしてしまえばその後気にする必要はないので、この方法を採っている人も多いと思われる。

方法2b. 個別のpackage.jsonに書く方法 (自動インストール)

方法2aの問題は、CDK内で解決することができる。以下のコードを使えば良い。

    new NodejsFunction(this, 'AuthHandler', {
      entry: 'backend/authorizer/index.ts',
      depsLockFilePath: 'backend/authorizer/package-lock.json',
      bundling: {
        commandHooks: {
          beforeBundling: (i, o) => [`cd ${i} && npm ci`],
          afterBundling: (i, o) => [],
          beforeInstall: (i, o) => [],
        },
      },

細かいところは割愛するが、基本的には commandHooks.beforeBundling 内で npm ci を実行し依存関係をインストールしている。 beforeBundling の処理は esbuild の実行前に実行されるので、これで esbuild はエラーなく実行できる。

depsLockFilePath も必要なプロパティだが、とりあえずはおまじない的に捉えても大丈夫だろう。 NodeJsFunction のImplementation detailが漏れ出ている箇所なので、気になる方はコードを読んでみると良い。

CDK内でインストールが完結するので、利用者は何も考えずに cdk deploy を実行すればデプロイできるのがメリット。

一方で、少しCDKのコードが複雑になる点と、synthesizeのたびに npm ci が走るため遅いのがデメリット。 npm ci は実行のたびにすべての依存関係をダウンロードするので、実用的な開発のためには npm install を使うか、yarn install --frozen-lockfile を使うよう検討したいところ。そもそもnpmにも --forzen-lockfile と同等のフラグが実装されればよいのだが……

github.com

方法3. NodeJsFunctionnodeModules に書く方法

もう一つ方法があって、 nodeModules プロパティ に必要な依存関係を書く方法。

    new NodejsFunction(this, 'AuthHandler', {
      entry: 'backend/authorizer/index.ts',
      depsLockFilePath: 'backend/authorizer/package-lock.json',
      bundling: {
        nodeModules: ["jsonwebtoken"],
      },

この方法なら、方法2aのように明示的に npm ci を実行する必要はないし、方法2bよりも仕組みはシンプルになる。

nodeModules に指定した場合、esbuildはこのライブラリをbundlingせずに external なライブラリとしてimportを残す。このため、bundlingのタイミングでライブラリの実体がなくてもエラーなく実行される。bundlingが完了した後、NodeJsFunction内の処理で自動で npm install される

パッケージのバージョンは depsLockFilePath で指定されたバージョンになるので、depsLockFilePath を明示的に指定する必要がある。ここで指定する lockFileは、必要なライブラリのバージョンが明記されてさえいればよく、場所は①でも②でも良い。

明らかなデメリットは、依存関係の名前が二重管理になる点。パッケージを追加するたびに package.jsonnodeModules の両方を編集する必要がある。

また、そもそも Native extension が含まれるライブラリは esbuild で bundling できないので、方法1〜3のどれを使うにせよ bundling.nodeModules にライブラリ名を明記する必要がある。この辺りの詳細はesbuildのドキュメントを参照するのも良い。

まとめ

NodeJsFunctionのビルド時に依存関係を解決する方法を3通り紹介した。 いずれの方法も一長一短なので、それぞれのメリットデメリットを把握した上で使い分けるのが良いと思われる。

私自身は、単純なLambdaは方法1、複雑なものも方法2aで妥協して (npm scriptでnpm ciをラップするなど) 使う場合が多い。

Appendix

方法1が動作する理由

depsLockFilePath プロパティを指定しない場合、NodeJsFunctionをバンドルする際のrootディレクトリはCDKのプロジェクトディレクトリ(正確にはcdkプロセスのworking directory。気になる人はこの辺を掘るとわかる。)になる。ルートの node_modules ディレクトリに必要な依存関係がインストールされているので、NodeJsFunctionをビルドする際にはそれが参照される。

小さめのデスクトップPCを組んだ

最近新型デスクトップPCを組んだので、顛末を記録しておく。

要求

顧客からの要求は以下の通り。

  1. 予算10万円程度 (OS/周辺機器含まず)
  2. 最新ゲームがそこそこできる
  3. かさばらない (部屋においてもじゃまにならない)

3がなにげに厄介。 というのも私はこれまで大は小を兼ねるの思想で、大型のデスクトップしか組んだことがなかったからだ。今も自分用は Define XL R2 というクソデカケースを使っている。 ここを満たすためには小型・低発熱・静音なパーツ選定をする必要があり、今回ちょっとしたパラダイムシフトを味わうことができた。

パーツ選定

上記要件を満たすような選定をする。

ケース

今回はPCの外見が最重視されていたため、ケースから選ぶことにした。 TSUKUMOで見つけたこいつがお眼鏡にかない、決定。

JONSBO T8 Silver というケースだが、これがかなり小さくて驚いた。小さめのゴミ箱くらいのサイズで、ちょうど部屋の隅に収まるのである。下の写真でコンセントと比較するとその小ささがわかるだろう。

すっぽり!

ちなみにこれ、TSUKUMOでは14800円だったがAliExpressだと送料込11700円で見つかった。中国から輸送してなおその値段は安すぎる。ただし到着まで2週間ほど要した。

ケースが決まったので、これに合うパーツを選んでいく。

CPU

ゲーム用ということでまずはCore iかRyzenシリーズかで迷う。調べてみると、今回のターゲットとなる価格帯1万円台のCPUはIntelの方が選択肢多い印象だった。 というか今価格comを確認しても、Ryzenの低価格帯モデルは全然売ってない。品切れなのか…?

そんな理由でIntelに当たりをつけてしばらく中古市場 (a.k.a. フリマアプリ)を漁っていたところ、Intel core i3 10320 を14000円で入手できた。(さらにクーポンで700円引き!)

用途的にはもっと弱いi3でも良かったが、巷の情報によればこれが少し珍しいモデルらしいので惹かれてしまった。あと、i3 10105などにしても2000円安くなる程度だったので、それならいいかという妥協もある。

【自作PC】世界で5人くらいしか持ってなさそうなCPUの入手に成功。「Core i3 10320」 - YouTube

単一コアの性能は同世代のi5よりも高いので、割りとコスパ良かったんではと思っている。その他の細かな比較は脚注に書いた。*1

CPUは Intel i3 10320 で決まったので、これに合うマザーボードを選んでいく。

マザーボード

ケースのJONSBO T8はMini-ITX規格のみ搭載可能なので、その中から選ぶ。 といっても、

  • Mini-ITX
  • LGA 1200搭載可能
  • 10000円台

マザーボードはかなり選択肢が少なく、価格コム調べで3モデルしかない。

結局MSIの H510I PRO WIFI にした。ほとんど今回のユースケースで意味のある機能差はなかったのだが、あえて理由を挙げるなら以下:

と言うかマザーボードWi-Fiモジュールが内蔵されているのにびっくりした。最近のマザーボードにはアンテナを生やせる。

これでベースは整った。あとは適当に合うパーツを選んでいく。

GPU

中古のGTX 1060 Mini 3GBを21500円で購入(更にクーポンで1000円引き!)。特にこだわりなく、中古市場にある中でコスパ良いものを選んだ。 ケースの都合でボードが短いものしか選択できず、また高発熱のものは排熱しきれない・うるさくなる可能性がある。GTX1060はその辺りの懸念とのバランスも良い。

RAM/ストレージ

RAMは2枚しか挿せないので、8x2で16GB。 ストレージはとりあえずM.2 SSDの1TBとした。 ケースの都合上拡張性に乏しいので、ここは顧客とよく相談して決める必要がある。

CPUクーラー

当初標準クーラーを使う予定だったが、実際動かしてみると高負荷時に思ったよりうるさい(ファン径が小さいので甲高い音がでる)ので、変更することにした。要件が後から追加されるのは世の理。

ただしT8に入るクーラーは高さ66mmまでと、かなり選択肢が限られる。 結局レビューの評価が良いNoctua NH-L9iに決定。中古で黒いバージョンを5000円で入手できた*2。これはかなり高級感があり見た目もかっこ良くてよかったです。ファン径も92mmと大きくなったので、かなり静音化もされた :tada:。

電源

JONSBO T8はATXSFX電源の2規格に対応しているが、ATX電源を入れると非常に狭くなるので、より小さいSFXの方を選びたい。 また電源容量的には、上記パーツの最大消費電力を合計すると約230Wなので、300Wもあれば十分。

上記条件で市場を探ると、Corsair SF450が見つかった。6500円。実際に見るとSFXはかなり小さくてビビる。今までATXしか使ってなかったが、低容量電源ならもうこっちでいいんじゃないか。

その他

T8はデフォルトで140mmのケースファンが1つ付属するのだが、これが起動中は虹色に輝くので視覚的にやかましい。顧客からのクレームがきたので、光らないファンを代わりに用意した。960円。

これにて全てのパーツの選定・購入が完了したので、粛々と組み立てていく。

組み立て編

購入経路がさまざまのため、各パーツが異なる時期にバラバラと送られてくる。

できるだけ組み立てを迅速に行うため、パーツが届くごとに順次動作確認することにした。(特に今回は多くが中古パーツなので重要)

マザボ・CPU・RAM・電源があれば最低限動作するので、そこから検証していく。

仮組みの様子

ちなみに上の写真では乱雑にビニール袋の上にマザーボードを置いているが、この状態では動作がめちゃくちゃ不安定だった。少し触れたらフリーズするなど。最低限箱の上に置くなどして基板を浮かせたほうが良さそう。

パーツ自体はいずれも問題なく動作した。フリマアプリ万歳。

完成形がこれ。実はこれはクーラーをNoctuaに交換する前の写真。ケーブルが硬いので整理はこれが限界だった。

小さめケースで組むのは初めてだったが、やはり狭くて作業しづらいものの完成後の満足感は高いなという発見があった。密度高いのが良い。

結果

カテゴリ 名前 値段 [円]
電源 Corsair SF450 CP-9020104-JP (中古) 6500
ケース JONSBO T8 Silver 11706
マザーボード MSI H510I PRO WIFI 13463
ストレージ Crucial CT1000P2SSD8JP 9685
メモリ KLEVV DDR4 2666 PC4-21300 8GB x 2枚 5933
CPU Intel Core i3 10320 BOX (中古) 13300
CPUクーラー Noctua NH-L9i chromax.black (中古) 5000
ケースファン Scythe KAZE FLEX 140 KF1425FD12S-P 960
GPU NVIDIA GTX 1060 mini 3GB (中古) 20500
合計 87047

(デザイン費含まず)

なんということでしょう。当初の予算からお釣りが出るくらいになった。OSと周辺機器含めても10万強程度。こうして顧客(妻)のゲーミングもといネットサーフィングマシンが完成したのであった。

まとめ

小型PC楽しい。次はもっと小さいのもやってみたい。

あと、フリマアプリはたまに掘り出し物が出品されるので、常に監視したくなる。メルカリ→ラクマ→Paypayフリマと巡回してると割とメンタル削られるので、妥協できる値段のものがあればさっさと買ってしまうのが良いように感じた。

買った後はもうその商品は検索しないのが良い。より安いのが出品されたら凹むので。

*1:また、最近のCPUには通常モデルからグラフィック機能を削ったF付きのモデルがあり(i5 10400Fなど)、これは通常モデルより3000円ほど安くなる。実際今回は別にGPUを付けるのでFモデルでも十分なのだが、GPUの調子がおかしいときの原因調査の際、結局CPUの内蔵グラフィックを活用した経験もあったので今回も避けた。予備のGPUを常に確保している人には、F付きモデルが良さそう。 第8,9世代のCPUも見てみたが、中古の割にあまり安いものがなく、またマザーボードの入手性も下がっているので、当時の最新世代から選ぶことにした。

*2:ヒートシンクはともかくファンの中古はやや抵抗ある。今回に限っては元の持ち主が短期間のみの使用ということだったのでそのまま使っている。