AWS Lambdaでブロックチェーン上で定期的にtxを発行しSlackへ通知する
GoQuorumでプライベートブロックチェーン構築・運用するにあたって正常にチェーンが動いているかを確かめるため、定期的にtxを発行(deploy)しその結果をSlackへ通知するというのをAWS Lambdaで実装したのでそれを記録する。(Publicチェーンでも使用できる)
- ローカルでLambda用にコードを書いてtx発行テストをする
- Slack通知の設定をする
- ローカルでLambda用にコードを書いてSlack通知テストをする
- Lambdaに関数をディプロイする
- 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
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
{
"_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
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が必要である。取得&設定方法については公式ページ参照のこと。
3.ローカルでLambda用にコードを書いてSlack通知テストをする
Node.jsでのコードの書き方についても上記記事やAWS公式ページを参照できるが、実際に私の手元で動作確認できたコードについては下記になる。
上記のディプロイスクリプトにSlack通知を組み合わせた形。
axios
など使用した方がよりシンプルなコードになるが、lambdaでサポートされていなかったのでなるべくlambdaでサポートのあるライブラリでの実装を目指した結果https
とurl
を使用することにした。
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点
- 必要なコードの記載
- コードで使用し、Lambdaでサポートのないライブラリのレイヤー(今回でいうと
web3
)
1.必要なコードの記載
Lambdaでは関数のエントリポイントとしてhandler
で始まるルールがあるが、今回ローカルで試す時にすでにその形にしているので修正する必要はない。
ただ同じディレクトリ構造で必要なファイルをもってくる。
また具体的な関数作成方法などについては下記参照のこと。
ファイルを設置したらまずDeploy
ボタンでディプロイする。
その後Testボタン
を押下するとweb3
モジュールが見つけられないというエラーがでる。fs
やhttps
, url
はすでにLambdaでサポートしているがweb3
については自分でモジュールを持ってくる必要がある。
Lambdaでサポートのないライブラリのレイヤー
そういったモジュールを持ってくるのにLambdaにレイヤーという機能がある。
作成方法は大まかに下記の手順だが詳細は調べてほしい。
- nodejsディレクトリ作成
- その中で必要なモジュールimport (今回では
npm install web3
) - nodejs/node_modulesという構造になっていることを確認
- nodejsディレクトリをzip化する
- zip化したものを上記写真コンソールの
レイヤーの作成
でアップロードする
作成できたらLambda関数のレイヤー項目から追加する。
追加できたら再度コンソール上でDeploy
しその後Test
ボタン押下。正しくレスポンスが帰ってくれば成功!
Slackにも通知がきていることを確認しておきましょう。
AWS EventBridge (CloudWatch Events)で自動化する
最後はEventBridge
を使用し実行を自動化する。私は1時間に1回tx発行を確認したいという要件だったためそのように設定。
設定方法については下記スクショを参照のこと。
これで1時間おきにSlackへ通知が来ていれば成功!テスト的に1分間に1回くるようにするとEventBridge
の挙動が確認できてよい。
足りない点質問ご意見あればお気軽にお願いします!
Discussion