🌊

AWS Lambdaでブロックチェーン上で定期的にtxを発行しSlackへ通知する

2023/10/20に公開

GoQuorumでプライベートブロックチェーン構築・運用するにあたって正常にチェーンが動いているかを確かめるため、定期的にtxを発行(deploy)しその結果をSlackへ通知するというのをAWS Lambdaで実装したのでそれを記録する。(Publicチェーンでも使用できる)

  1. ローカルでLambda用にコードを書いてtx発行テストをする
  2. Slack通知の設定をする
  3. ローカルでLambda用にコードを書いてSlack通知テストをする
  4. Lambdaに関数をディプロイする
  5. AWS EventBridge (CloudWatch Events)で自動化する

使用技術

  • AWS Lambda (Node.js v16.x, web3.js) -> deployコード実装
  • AWS EventBridge (CloudWatch Events)
  • Slack

1.ローカルでLambda用にコードを書いてtx発行テストをする

今回Lambdaでサポートしている Node.js でコードを記述。Lambdaではサポートしているライブラリが限られているので最低限のライブラリを使用するため web3.js のみ使った。Lambdaでの外部ライブラリimport方法は後ほど記述する。

  • 下記のコマンドを順番に実行していく
mkdir test-deploy
cd test-deploy
npm init -y
npm install web3
  • index.jsファイルを作成し下記のコード貼り付ける

詳細なコードの説明は省く
/index.js

index.js
const { Web3 } = require('web3');

const PRIVATE_KEY ="your private key";
// quorumではRPCエンドポイント使用
const PROVIDER_URL = "infuraやAlchemyなどから取得";
const CONTRACTS_ABI_PATH = 'contracts_abi.json';

async function deployContract(web3) {
  const contractData = JSON.parse(fs.readFileSync(CONTRACTS_ABI_PATH, 'utf8'));
  const MyContract = new web3.eth.Contract(contractData.abi);

  return await MyContract.deploy({
    data: contractData.bytecode,
    arguments: [],
  }).send({
    from: web3.eth.defaultAccount,
    gas: 5000000,
    // 今回プライベートチェーンのためgas代は0で指定
    gasPrice: '0x0',
  });
}

exports.handler = async () => {
  const web3 = new Web3(PROVIDER_URL);
  const account = web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY);
  web3.eth.accounts.wallet.add(account);
  web3.eth.defaultAccount = account.address;

  try {
    const contractInstance = await deployContract(web3);
    console.log(
      'Contract deployed at address:',
      contractInstance.options.address
    );
    
    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'DEV:コントラクトが正常にデプロイされました。',
        contractAddress: contractInstance.options.address,
      }),
    };
  } catch (error) {
    console.error(error);

    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'DEV:コントラクトのデプロイ中にエラーが発生しました。',
        error: error.message,
      }),
    };
  }
};

  • index.jsと同じ階層にcontracts_abi.jsonとしてコンラクトをコンパイルしたものを入れる。詳細は省くがhardhatでコントラクトをコンパイルし貼り付けた
    こんな感じ
    /contracts_abi.json
contracts_abi.json
{
    "_format": "hh-sol-artifact-1",
    "contractName": "MyToken",
    "sourceName": "contracts/MyToken.sol",
    "abi": [
      {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
      },
      {
        "anonymous": false,
        "inputs": [
          {
            "indexed": true,
            "internalType": "address",
            "name": "owner",
            "type": "address"
          },
          {
            "indexed": true,
	    .
	    .
	    .
	     }
	],
	"bytecode": "0x60806040523480156200001157600080fd5b50604080518082018252600781526626bcaa37b5b2b760c91b6020808301918252835180850190945260038452624d544b60e81b9084015281519192916200005c91600091620000eb565b50805162000072906001906020840190620000eb565b5050506200008f620000896200009560201b60201c565b62000099565b620001ce565b3390565b600680546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e090600090a35050565b828054620000f99062000191565b9060005
	.
	.
	.
	673582212207e943d19965d15b2b6d6a34dcfc528d40315254b114bc7392060e42f4a23d63964736f6c63430008090033",
    "linkReferences": {},
    "deployedLinkReferences": {}
}
  • このindex.jsを実行するtest.jsを同階層に作成
    /test.js
test.js
const lambdaFunction = require('./index');

async function test() {
  console.log("Test");
  try {
    const result = await lambdaFunction.handler();
    console.log('Result:', result);
  } catch (error) {
    console.error('Error:', error);
  }
}

test();

以上書き終えたら node test.jsで実行
実行結果例

mameta test-deploy %node test.js
Test
Contract deployed at address: 0x782Bd80c9772Aec5E6F23cD4d46a13f574271c9a
Result: {
  statusCode: 200,
  body: '{"message":"DEV:コントラクトが正常にデプロイされました。","contractAddress":"0x782Bd80c9772Aec5E6F23cD4d46a13f574271c9a"}'
}

2.Slack通知の設定をする

Slack通知にはWebhook URLが必要である。取得&設定方法については公式ページ参照のこと。
https://api.slack.com/messaging/webhooks

3.ローカルでLambda用にコードを書いてSlack通知テストをする

Node.jsでのコードの書き方についても上記記事やAWS公式ページを参照できるが、実際に私の手元で動作確認できたコードについては下記になる。
上記のディプロイスクリプトにSlack通知を組み合わせた形。
axiosなど使用した方がよりシンプルなコードになるが、lambdaでサポートされていなかったのでなるべくlambdaでサポートのあるライブラリでの実装を目指した結果httpsurlを使用することにした。

index.js
const { Web3 } = require('web3');
const fs = require('fs');
const https = require('https');
const { URL } = require('url');
require('dotenv').config();

// 環境変数の取得
const PRIVATE_KEY = process.env.ENV_PRIVATE_KEY;
const PROVIDER_URL = process.env.ENV_PROVIDER_URL;
const SLACK_WEBHOOK_URL = process.env.ENV_SLACK_WEBHOOK_URL;
const CONTRACTS_ABI_PATH = 'contracts_abi.json';

// コントラクトのデプロイ
async function deployContract(web3) {
  const contractData = JSON.parse(fs.readFileSync(CONTRACTS_ABI_PATH, 'utf8'));
  const MyContract = new web3.eth.Contract(contractData.abi);

  return await MyContract.deploy({
    data: contractData.bytecode,
    arguments: [],
  }).send({
    from: web3.eth.defaultAccount,
    gas: 5000000,
    gasPrice: '0x0',
  });
}

// Slack 通知
async function notifySlack(success, contractAddress, errorMessage) {
  const currentDateTime = new Date().toLocaleString();
  const parsedUrl = new URL(SLACK_WEBHOOK_URL);

  let messageText;

  if (success) {
    messageText = `DEV :\nコントラクトが正常にデプロイされました。\nDeployed at: ${currentDateTime}\nContract deployed at address: ${contractAddress}`;
  } else {
    messageText = `======================================\nDEV\nコントラクトのデプロイ中にエラーが発生しました。\nDate at: ${currentDateTime}\nerror: ${errorMessage}\n======================================`;
  }
  console.log('messageText', messageText);
  const postData = JSON.stringify({ text: messageText });

  const options = {
    hostname: parsedUrl.hostname,
    path: parsedUrl.pathname,
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(postData, 'utf8'),
    },
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(data);
        } else {
          reject(new Error(`Slack Notification Error: ${data}`));
        }
      });
    });

    req.on('error', (error) => reject(error));
    req.write(postData);
    req.end();
  });
}

// Lambda 関数のエントリーポイント
exports.handler = async () => {
  const web3 = new Web3(PROVIDER_URL);
  const account = web3.eth.accounts.privateKeyToAccount(PRIVATE_KEY);
  web3.eth.accounts.wallet.add(account);
  web3.eth.defaultAccount = account.address;

  try {
    const contractInstance = await deployContract(web3);
    console.log(
      'Contract deployed at address:',
      contractInstance.options.address
    );

    await notifySlack(true, contractInstance.options.address, null);

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: 'DEV:コントラクトが正常にデプロイされました。',
        contractAddress: contractInstance.options.address,
      }),
    };
  } catch (error) {
    console.error(error);

    await notifySlack(false, null, error.message); // エラーが発生した場合の Slack 通知

    return {
      statusCode: 500,
      body: JSON.stringify({
        message: 'DEV:コントラクトのデプロイ中にエラーが発生しました。',
        error: error.message,
      }),
    };
  }
};

上記実行すると下記の結果を得ることができ、スラックへ通知を送ることができた。

mameta mint_for_lambda %node test.js
Test
Contract deployed at address: 0xB80A6E68F09a8d95318833414e4A737F2F7DF730
messageText DEV :
コントラクトが正常にデプロイされました。
Deployed at: 10/19/2023, 12:47:38 PM
Contract deployed at address: 0xB80A6E68F09a8d95318833414e4A737F2F7DF730
Result: {
  statusCode: 200,
  body: '{"message":"DEV:コントラクトが正常にデプロイされました。","contractAddress":"0xB80A6E68F09a8d95318833414e4A737F2F7DF730"}'
}

4.Lambdaに関数をディプロイする

残る工程はローカルで実行してきたコードをLambda上で動かすこと(本セクション)と自動で実行すること(次セクション)。

Lmbdaの挙動についてはよくわかってないので説明を省くが、必要なことは下記3点

  1. 必要なコードの記載
  2. コードで使用し、Lambdaでサポートのないライブラリのレイヤー(今回でいうとweb3)

1.必要なコードの記載

Lambdaでは関数のエントリポイントとしてhandlerで始まるルールがあるが、今回ローカルで試す時にすでにその形にしているので修正する必要はない。
ただ同じディレクトリ構造で必要なファイルをもってくる。

また具体的な関数作成方法などについては下記参照のこと。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/getting-started.html

ファイルを設置したらまずDeployボタンでディプロイする。
その後Testボタンを押下するとweb3モジュールが見つけられないというエラーがでる。fshttps, urlはすでにLambdaでサポートしているがweb3については自分でモジュールを持ってくる必要がある。

Lambdaでサポートのないライブラリのレイヤー

そういったモジュールを持ってくるのにLambdaにレイヤーという機能がある。

作成方法は大まかに下記の手順だが詳細は調べてほしい。
https://xp-cloud.jp/blog/2019/01/12/4630/

  1. nodejsディレクトリ作成
  2. その中で必要なモジュールimport (今回ではnpm install web3)
  3. nodejs/node_modulesという構造になっていることを確認
  4. nodejsディレクトリをzip化する
  5. zip化したものを上記写真コンソールのレイヤーの作成でアップロードする

作成できたらLambda関数のレイヤー項目から追加する。

追加できたら再度コンソール上でDeployしその後Testボタン押下。正しくレスポンスが帰ってくれば成功!

Slackにも通知がきていることを確認しておきましょう。

AWS EventBridge (CloudWatch Events)で自動化する

最後はEventBridgeを使用し実行を自動化する。私は1時間に1回tx発行を確認したいという要件だったためそのように設定。

設定方法については下記スクショを参照のこと。


これで1時間おきにSlackへ通知が来ていれば成功!テスト的に1分間に1回くるようにするとEventBridgeの挙動が確認できてよい。

足りない点質問ご意見あればお気軽にお願いします!

Discussion