EKSで構築したプライベートチェーンにHelloコントラクトをデプロイしてHello Worldするまで
kubernetesとblockchainの両方の勉強になるかなというのとあんまりgethを使ったnodeの構築みたいな情報がなさそうだったので。
(kubernetesもblockchainも初学者ですが、誤った箇所などありましたらコメントくださいー)
今回の成果物
開発環境
コントラクト
- VSCode(foundry使うから)
- solidity 0.8.15+commit.e14f2714.Darwin.appleclang
- foundry(コントラクトの作成、デプロイするのに)
foundryについてはこちらの記事で紹介されています
クライアント
- Go 1.18.4
インフラ
- kubernetes
- Amazon EKS
コントラクト
Hello Worldするだけのコントラクト
forge init hello --vscode
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
contract Hello {
function hello() public pure returns (string memory) {
return "Hello World!!";
}
}
クライアント
作成したコントラクトのメソッドを呼ぶためのクライアントはいくつか選択肢があるが今回はGoのクライアントを使用。
クライアントモジュールをインストールする。
go get -d github.com/ethereum/go-ethereum
go get github.com/ethereum/go-ethereum/rpc@v1.10.21
go get github.com/ethereum/go-ethereum/accounts/keystore@v1.10.21
abiファイルからソースコードを生成するのにabigenを使用する。
goのethereumクライアントのインストールをしておく必要があるのでここら辺を参考に。
//コントラクトをビルドしてabiファイルを生成する
forge clean
forge build --extra-output-files abi
abigen --abi Hello.abi.json --pkg hello --type hello --out hello/hello.go
設定値をdotファイルで管理したかったので以下インストール
go get github.com/joho/godotenv
コントラクトに繋いでHello Worldする
package env
import (
"log"
"os"
"github.com/joho/godotenv"
)
func init() {
var err error
if os.Getenv("ENVIROMENT") == "prod" {
err = godotenv.Load("../.env.prod")
} else {
err = godotenv.Load("../.env")
}
if err != nil {
log.Fatalf("環境変数の読み込みに失敗しました。.envファイルを作成してください. err: %v", err)
}
}
func RpcUrl() string {
return os.Getenv("RPC_URL")
}
func ContractAddress() string {
return os.Getenv("CONTRACT_ADDRESS")
}
package main
import (
"JY8752/kubernetes-privatenet-hello-contract/env"
"JY8752/kubernetes-privatenet-hello-contract/hello"
"fmt"
"log"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)
func main() {
//client
cl, err := ethclient.Dial(env.RpcUrl())
if err != nil {
log.Fatalf("ethclientの初期化に失敗しました. err: %v", err)
}
//hello contract
address := common.HexToAddress(env.ContractAddress())
hello, err := hello.NewHello(address, cl)
if err != nil {
log.Fatalf("hello contractの取得に失敗しました. err: %v", err)
}
res, err := hello.Hello(&bind.CallOpts{})
if err != nil {
log.Fatalf("helloメソッドの呼び出しに失敗しました. err: %v", err)
}
fmt.Println(res)
}
プライベートチェーンの作成
コントラクトとクライアントはできたのでチェーンを構成する。
dockerイメージの作成
gethを起動し、ブロックチェーンにつながるためには最初のブロックを作成する必要がある。
最初のブロックの作成に以下のようなgenesisファイルを作成する。
{
"config": {
"chainId": 15,
"homesteadBlock": 0,
"eip150Block": 0,
"eip155Block": 0,
"eip158Block": 0,
"byzantiumBlock": 0,
"constantinopleBlock": 0
},
"coinbase" : "0x0000000000000000000000000000000000000000",
"difficulty" : "0x1",
"extraData" : "",
"gasLimit" : "0x2fefd8",
"nonce" : "0x0000000000000042",
"mixhash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000",
"timestamp" : "0x00",
"alloc": {
"945cd603a6754cb13c3d61d8fe240990f86f9f8a": { "balance": "500000000000000000000000000" },
"66b4e7be902300f9a15d900822bbd8803be87391": { "balance": "500000000000000000000000000" },
"104f0d848da3f760dddadc56fc4ab78305110dba": { "balance": "500000000000000000000000000" },
"addfaa808c59581f04cdadfc0be28ebfb520e839": { "balance": "500000000000000000000000000" },
"450a8a99bf5ad49db301f6068c619de2400de6f7": { "balance": "500000000000000000000000000" }
}
}
genesisファイルを元に初期化する。
geth --datadir . init genesis.json
成功するとkeystoreディレクトリが新たに作成されている。
アカウントを作成しているので、そのパスワードをファイルに記載しておく。
pass
pass
pass
pass
pass
gethの初期化と起動スクリプトを作成する。
#!bin/sh
geth --datadir /var/share/ethereum --nodiscover --maxpeers 0 \
init /var/share/ethereum/genesis.json \
&& \
geth --datadir /var/share/ethereum --networkid 15 \
--nodiscover --maxpeers 0 --mine --miner.threads 1 \
--http --http.addr "0.0.0.0" --http.corsdomain "*" \
--http.vhosts "*" --http.api "eth,web3,personal,net" \
--ipcpath /tmp/geth.ipc --ws --ws.addr "0.0.0.0" \
--ws.api "eth,web3,personal,net" --ws.origins "*" \
--unlock 0,1,2,3,4 --password /var/share/ethereum/password --allow-insecure-unlock
Dockerfileを下記のように作成する。
FROM ethereum/client-go:v1.10.23
COPY genesis.json /var/share/ethereum/
COPY keystore /var/share/ethereum/keystore/
COPY password /var/share/ethereum/
COPY entrypoint.sh /
RUN chmod 744 /entrypoint.sh
EXPOSE 8545 8546 30303 30303/udp
ENTRYPOINT ["/entrypoint.sh"]
kindを使ったkubernetesマルチノード上にチェーンを構築
EKSにデプロイするのが本題だが、一旦ローカルでkubernetesクラスターを作成し、そこにデプロイしてみる。
ローカル上にkubernetesを使用してマルチノードを構築するのにkindを使用。
以下のようなkind.ymlを作成する。
apiVersion: kind.x-k8s.io/v1alpha4
kind: Cluster
name: blockchaine-cluster
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
extraPortMappings:
- containerPort: 30045
hostPort: 30045
protocol: TCP
kind create cluster --config kind.yml
control-plane1つとworkerノードが3つできる。
次にkubernetesのマニフェストを以下のように作成する。
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
apps: private-net
name: private-net
spec:
replicas: 1
selector:
matchLabels:
app: private-net
template:
metadata:
labels:
app: private-net
spec:
containers:
- image: ghcr.io/jy8752/kubernetes-privatenet-hello-contract:main
name: private-net
---
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
app: private-net
name: private-net
spec:
ports:
- name: rpc
port: 8545
nodePort: 30045
- name: ws
port: 8546
nodePort: 30046
selector:
app: private-net
type: NodePort
status:
loadBalancer: {}
dockerイメージはGithub Packagesを使用して公開している。
作成したマニフェストファイルを適用する。
kubectl apply -f private-net-deploy.yml
port30045で接続できるので確認。
geth attach http://localhost:30045
> Welcome to the Geth JavaScript console!
EKS上にチェーンを構築する
eksctlを使用してEKSクラスターを作成する。(20 - 30分くらいかかる)
eksctl create cluster \
--name eks-private-net \
--nodegroup-name ng-private-net \
--node-type t3.large \
--nodes 3
作成が完了したらノードを確認しておく。
kubectl get node -o json | jq ".items[]|{name: .metadata.name, ExternalIP: .status.addresses[1].address}"
{
"name": "ip-192-168-20-197.ap-northeast-1.compute.internal",
"ExternalIP": "13.114.138.50"
}
{
"name": "ip-192-168-55-191.ap-northeast-1.compute.internal",
"ExternalIP": "35.79.222.168"
}
{
"name": "ip-192-168-87-66.ap-northeast-1.compute.internal",
"ExternalIP": "3.112.83.0"
}
マニフェストを適用する。
kubectl apply -f private-net-deploy.yml
security groupを設定してportに穴を開けておく。
aws ec2 authorize-security-group-ingress \
--group-id sg-0b543374e4be3930d \
--ip-permissions IpProtocol=tcp,FromPort=30045,ToPort=30045,IpRanges='[{CidrIp=0.0.0.0/0,Description="allow geth attach"}]'
attachしてみる。
geth attach http://13.114.138.50:30045
> Welcome to the Geth JavaScript console!
コントラクトをデプロイしてみる。(エラーが出るので--legacyオプションをつける必要がある。)
source .env.prod; forge create --rpc-url $RPC_URL --private-key $PRIVATE_KEY src/Hello.sol:Hello --legacy
クライアントでリクエストしてみる。
ENVIROMENT=prod go run main.go
> Hello World!!
できた!!!!
まとめ
開発中にローカルにnodeを立ち上げるのはganacheやhardhat、foundryでもanvilを使えば簡単に構築できる。テストネットやメインネットへの接続やデプロイもalchemyなどのサービスを使えば簡単にできる。わざわざ自前でチェーンを構築するのはだいぶ手間かもしれないが便利なライブラリやサービスが隠蔽してくれていた部分の理解が深まりいい学習にはなった。
今回やったこと
- kindを使用しローカルに構築したkubernetesマルチノードにプライベートチェーンを構築し、コントラクトをデプロイした。
- EKSに構築したプライベートチェーンにコントラクトをデプロイし、go-ethereumクライアントを使用してコントラクトのメソッドを呼び出した。
参考
eksctl
foundryのlegacyオプションについて Error: Returned error: invalid opcode: SHR
Discussion