Skip to content

Commit 035ace2

Browse files
committed
Base. Added Instance Store volume option for storage
1 parent aff0693 commit 035ace2

15 files changed

+597
-34
lines changed

lib/base/README.md

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ npx cdk deploy base-common
122122
> cdk bootstrap aws://ACCOUNT-NUMBER/REGION
123123
> ```
124124
125-
### From your Cloud9: Deploy Single Node
125+
### Option 1: Deploy Single Node
126126
127127
1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example:
128128
@@ -163,12 +163,48 @@ aws ssm start-session --target $INSTANCE_ID --region $AWS_REGION
163163
curl -s -X POST -H "Content-Type: application/json" --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' http://localhost:8545
164164
```
165165

166+
### Option 2: Highly Available RPC Nodes
167+
168+
1. For L1 node you you can set your own URLs in `BASE_L1_EXECUTION_ENDPOINT` and `BASE_L1_CONSENSUS_ENDPOINT` properties of `.env` file. It can be one of [the providers recommended by Base](https://docs.base.org/tools/node-providers) or you can run your own Ethereum node [with Node Runner Ethereum blueprint](https://aws-samples.github.io/aws-blockchain-node-runners/docs/Blueprints/Ethereum) (tested with geth-lighthouse combination). For example:
169+
170+
```bash
171+
#For Sepolia:
172+
BASE_L1_EXECUTION_ENDPOINT="https://ethereum-sepolia-rpc.publicnode.com"
173+
BASE_L1_CONSENSUS_ENDPOINT="https://ethereum-sepolia-beacon-api.publicnode.com"
174+
```
175+
176+
2. Deploy Base RPC Node and wait for it to sync. For Mainnet it might a day when using snapshots or about a week if syncing from block 0. You can use snapshots provided by the Base team by setting `BASE_RESTORE_FROM_SNAPSHOT="true"` in `.env` file.
177+
178+
```bash
179+
pwd
180+
# Make sure you are in aws-blockchain-node-runners/lib/base
181+
npx cdk deploy base-ha-nodes --json --outputs-file ha-nodes-deploy.json
182+
```
183+
184+
2. Give the new RPC **full** nodes about 2-3 hours (24 hours for **archive** node) to initialize and then run the following query against the load balancer behind the RPC node created
185+
186+
```bash
187+
export RPC_ALB_URL=$(cat ha-nodes-deploy.json | jq -r '..|.alburl? | select(. != null)')
188+
echo $RPC_ALB_URL
189+
```
190+
191+
Periodically check [Geth Syncing Status](https://geth.ethereum.org/docs/fundamentals/logs#syncing). Run the following query from within the same VPC and against the private IP of the load balancer fronting your nodes:
192+
193+
```bash
194+
curl http://$RPC_ALB_URL:8545 -X POST -H "Content-Type: application/json" \
195+
--data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
196+
```
197+
198+
**NOTE:** By default and for security reasons the load balancer is available only from within the default VPC in the region where it is deployed. It is not available from the Internet and is not open for external connections. Before opening it up please make sure you protect your RPC APIs.
199+
166200
### Monitoring
167-
Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes buehind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics:
201+
Every 5 minutes a script on the Base node publishes to CloudWatch service the metrics for current block for L1/L2 clients as well as blocks behind metric for L1 and minutes behind for L2. When the node is fully synced the blocks behind metric should get to 4 and minutes behind should get down to 0. To see the metrics for **single node only**:
168202

169203
- Navigate to CloudWatch service (make sure you are in the region you have specified for AWS_REGION)
170204
- Open Dashboards and select `base-single-node-<network>-<your_ec2_instance_id>` from the list of dashboards.
171205

206+
Metrics for **ha nodes** configuration is not yet implemented (contributions are welcome!)
207+
172208
## From your Cloud9: Clear up and undeploy everything
173209

174210
1. Undeploy all Nodes and Common stacks
@@ -184,6 +220,9 @@ pwd
184220
# Undeploy Single Node
185221
npx cdk destroy base-single-node
186222

223+
# Undeploy HA Nodes
224+
npx cdk destroy base-ha-nodes
225+
187226
# Delete all common components like IAM role and Security Group
188227
npx cdk destroy base-common
189228
```

lib/base/app.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import * as cdk from 'aws-cdk-lib';
55
import * as config from "./lib/config/baseConfig";
66
import {BaseCommonStack} from "./lib/common-stack";
77
import {BaseSingleNodeStack} from "./lib/single-node-stack";
8+
import {BaseHANodesStack} from "./lib/ha-nodes-stack";
89

910
const app = new cdk.App();
1011
cdk.Tags.of(app).add("Project", "AWSBase");
@@ -28,3 +29,22 @@ new BaseSingleNodeStack(app, "base-single-node", {
2829
snapshotUrl: config.baseNodeConfig.snapshotUrl,
2930
dataVolume: config.baseNodeConfig.dataVolume,
3031
});
32+
33+
new BaseHANodesStack(app, "base-ha-nodes", {
34+
stackName: `base-ha-nodes-${config.baseNodeConfig.baseNodeConfiguration}-${config.baseNodeConfig.baseNetworkId}`,
35+
env: { account: config.baseConfig.accountId, region: config.baseConfig.region },
36+
37+
instanceType: config.baseNodeConfig.instanceType,
38+
instanceCpuType: config.baseNodeConfig.instanceCpuType,
39+
baseNetworkId: config.baseNodeConfig.baseNetworkId,
40+
baseNodeConfiguration: config.baseNodeConfig.baseNodeConfiguration,
41+
restoreFromSnapshot: config.baseNodeConfig.restoreFromSnapshot,
42+
l1ExecutionEndpoint: config.baseNodeConfig.l1ExecutionEndpoint,
43+
l1ConsensusEndpoint: config.baseNodeConfig.l1ConsensusEndpoint,
44+
snapshotUrl: config.baseNodeConfig.snapshotUrl,
45+
dataVolume: config.baseNodeConfig.dataVolume,
46+
47+
albHealthCheckGracePeriodMin: config.haNodeConfig.albHealthCheckGracePeriodMin,
48+
heartBeatDelayMin: config.haNodeConfig.heartBeatDelayMin,
49+
numberOfNodes: config.haNodeConfig.numberOfNodes
50+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
#!/bin/bash
2+
3+
source /etc/environment
4+
5+
if [[ "$DATA_VOLUME_TYPE" == "instance-store" ]]; then
6+
echo "Data volume type is instance store"
7+
export DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk 'max < $4 {max = $4; vol = $1} END {print vol}')
8+
fi
9+
10+
if [ -n "$DATA_VOLUME_ID" ]; then
11+
if [ $(df --output=target | grep -c "/data") -lt 1 ]; then
12+
echo "Checking fstab for Data volume"
13+
14+
mkfs.ext4 $DATA_VOLUME_ID
15+
echo "Data volume formatted. Mounting..."
16+
# Waiting wihtouht using sleep as it sometimes just hangs....
17+
coproc read -t 10 && wait "$!" || true
18+
DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID)
19+
DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2"
20+
echo "DATA_VOLUME_ID="$DATA_VOLUME_ID
21+
echo "DATA_VOLUME_UUID="$DATA_VOLUME_UUID
22+
echo "DATA_VOLUME_FSTAB_CONF="$DATA_VOLUME_FSTAB_CONF
23+
24+
# Check if data disc is already in fstab and replace the line if it is with the new disc UUID
25+
if [ $(grep -c "data" /etc/fstab) -gt 0 ]; then
26+
SED_REPLACEMENT_STRING="$(grep -n "/data" /etc/fstab | cut -d: -f1)s#.*#$DATA_VOLUME_FSTAB_CONF#"
27+
cp /etc/fstab /etc/fstab.bak
28+
sed -i "$SED_REPLACEMENT_STRING" /etc/fstab
29+
else
30+
echo $DATA_VOLUME_FSTAB_CONF | sudo tee -a /etc/fstab
31+
fi
32+
33+
sudo mount -a
34+
35+
chown bcuser:bcuser -R /data
36+
else
37+
echo "Data volume is mounted, nothing changed"
38+
fi
39+
fi

lib/base/lib/assets/user-data/node.sh

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ LIFECYCLE_HOOK_NAME=${_LIFECYCLE_HOOK_NAME_}
66
AUTOSCALING_GROUP_NAME=${_AUTOSCALING_GROUP_NAME_}
77
RESOURCE_ID=${_NODE_CF_LOGICAL_ID_}
88
ASSETS_S3_PATH=${_ASSETS_S3_PATH_}
9+
DATA_VOLUME_TYPE=${_DATA_VOLUME_TYPE_}
910
{
1011
echo "LIFECYCLE_HOOK_NAME=$LIFECYCLE_HOOK_NAME"
1112
echo "AUTOSCALING_GROUP_NAME=$AUTOSCALING_GROUP_NAME"
1213
echo "ASSETS_S3_PATH=$ASSETS_S3_PATH"
14+
echo "DATA_VOLUME_TYPE=$DATA_VOLUME_TYPE"
1315
} >> /etc/environment
1416

1517
arch=$(uname -m)
@@ -34,6 +36,7 @@ yum -y install amazon-cloudwatch-agent collectd jq yq gcc ncurses-devel aws-cfn-
3436
wget $YQ_URI -O /usr/bin/yq && chmod +x /usr/bin/yq
3537

3638
# install aria2 a p2p downloader
39+
cd /tmp
3740

3841
if [ "$arch" == "x86_64" ]; then
3942
wget https://github.com/q3aql/aria2-static-builds/releases/download/v1.36.0/aria2-1.36.0-linux-gnu-64bit-build1.tar.bz2
@@ -235,37 +238,48 @@ ExecStart=/home/bcuser/bin/node.sh
235238
WantedBy=multi-user.target
236239
EOF'
237240

238-
echo "Signaling completion to CloudFormation to continue with volume mount"
239-
/opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION
241+
if [[ "$LIFECYCLE_HOOK_NAME" == "none" ]]; then
242+
echo "We run single node setup. Signaling completion to CloudFormation to continue with volume mount"
243+
/opt/aws/bin/cfn-signal --stack $STACK_NAME --resource $RESOURCE_ID --region $REGION
244+
fi
240245

241246
echo "Preparing data volume"
242247

243-
echo "Wait for one minute for the volume to be available"
244-
sleep 60
245-
246-
if $(lsblk | grep -q nvme1n1); then
247-
echo "nvme1n1 is found. Configuring attached storage"
248+
echo "Wait for one minute for the volume to become available"
249+
sleep 60s
248250

249-
if [ "$FORMAT_DISK" == "false" ]; then
250-
echo "Not creating a new filesystem in the disk. Existing data might be present!!"
251-
else
252-
mkfs -t ext4 /dev/nvme1n1
253-
fi
251+
if [[ "$DATA_VOLUME_TYPE" == "instance-store" ]]; then
252+
echo "Data volume type is instance store"
254253

255-
sleep 10
256-
# Define the line to add to fstab
257-
uuid=$(lsblk -n -o UUID /dev/nvme1n1)
258-
line="UUID=$uuid /data ext4 defaults 0 2"
254+
cd /opt
255+
chmod +x /opt/setup-instance-store-volumes.sh
259256

260-
# Write the line to fstab
261-
echo $line | sudo tee -a /etc/fstab
257+
(crontab -l; echo "@reboot /opt/setup-instance-store-volumes.sh >/tmp/setup-instance-store-volumes.log 2>&1") | crontab -
258+
crontab -l
262259

263-
mount -a
260+
DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk 'max < $4 {max = $4; vol = $1} END {print vol}')
264261

265262
else
266-
echo "nvme1n1 is not found. Not doing anything"
263+
echo "Data volume type is EBS"
264+
265+
DATA_VOLUME_ID=/dev/$(lsblk -lnb | awk -v VOLUME_SIZE_BYTES="$DATA_VOLUME_SIZE" '{if ($4== VOLUME_SIZE_BYTES) {print $1}}')
267266
fi
268267

268+
mkfs -t ext4 $DATA_VOLUME_ID
269+
echo "waiting for volume to get UUID"
270+
OUTPUT=0;
271+
while [ "$OUTPUT" = 0 ]; do
272+
DATA_VOLUME_UUID=$(lsblk -fn -o UUID $DATA_VOLUME_ID)
273+
OUTPUT=$(echo $DATA_VOLUME_UUID | grep -c - $2)
274+
echo $OUTPUT
275+
done
276+
DATA_VOLUME_FSTAB_CONF="UUID=$DATA_VOLUME_UUID /data ext4 defaults 0 2"
277+
echo "DATA_VOLUME_ID="$DATA_VOLUME_ID
278+
echo "DATA_VOLUME_UUID="$DATA_VOLUME_UUID
279+
echo "DATA_VOLUME_FSTAB_CONF="$DATA_VOLUME_FSTAB_CONF
280+
echo $DATA_VOLUME_FSTAB_CONF | tee -a /etc/fstab
281+
mount -a
282+
269283
lsblk -d
270284

271285
chown -R bcuser:bcuser /data

lib/base/lib/config/baseConfig.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,9 @@ export interface BaseBaseNodeConfig extends configTypes.BaseNodeConfig {
2323
l1ConsensusEndpoint: string;
2424
snapshotUrl: string;
2525
}
26+
27+
export interface BaseHAConfig {
28+
albHealthCheckGracePeriodMin: number;
29+
heartBeatDelayMin: number;
30+
numberOfNodes: number;
31+
}

lib/base/lib/config/baseConfig.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,9 @@ export const baseNodeConfig: configTypes.BaseBaseNodeConfig = {
3939
throughput: process.env.BASE_DATA_VOL_THROUGHPUT ? parseInt(process.env.BASE_DATA_VOL_THROUGHPUT): 700,
4040
},
4141
};
42+
43+
export const haNodeConfig: configTypes.BaseHAConfig = {
44+
albHealthCheckGracePeriodMin: process.env.BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN ? parseInt(process.env.BASE_HA_ALB_HEALTHCHECK_GRACE_PERIOD_MIN) : 10,
45+
heartBeatDelayMin: process.env.BASE_HA_NODES_HEARTBEAT_DELAY_MIN ? parseInt(process.env.BASE_HA_NODES_HEARTBEAT_DELAY_MIN) : 40,
46+
numberOfNodes: process.env.BASE_HA_NUMBER_OF_NODES ? parseInt(process.env.BASE_HA_NUMBER_OF_NODES) : 2
47+
};

lib/base/lib/ha-nodes-stack.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import * as cdkConstructs from "constructs";
3+
import * as ec2 from "aws-cdk-lib/aws-ec2";
4+
import * as iam from "aws-cdk-lib/aws-iam";
5+
import { AmazonLinuxGeneration, AmazonLinuxImage } from "aws-cdk-lib/aws-ec2";
6+
import * as s3Assets from "aws-cdk-lib/aws-s3-assets";
7+
import * as configTypes from "./config/baseConfig.interface";
8+
import { BaseNodeSecurityGroupConstruct } from "./constructs/base-node-security-group";
9+
import * as fs from "fs";
10+
import * as path from "path";
11+
import * as constants from "../../constructs/constants";
12+
import { HANodesConstruct } from "../../constructs/ha-rpc-nodes-with-alb";
13+
import * as nag from "cdk-nag";
14+
15+
export interface BaseHANodesStackProps extends cdk.StackProps {
16+
instanceType: ec2.InstanceType;
17+
instanceCpuType: ec2.AmazonLinuxCpuType;
18+
baseNetworkId: configTypes.BaseNetworkId;
19+
baseNodeConfiguration: configTypes.BaseNodeConfiguration;
20+
restoreFromSnapshot: boolean;
21+
l1ExecutionEndpoint: string,
22+
l1ConsensusEndpoint: string,
23+
snapshotUrl: string,
24+
dataVolume: configTypes.BaseDataVolumeConfig;
25+
albHealthCheckGracePeriodMin: number;
26+
heartBeatDelayMin: number;
27+
numberOfNodes: number;
28+
}
29+
30+
export class BaseHANodesStack extends cdk.Stack {
31+
constructor(scope: cdkConstructs.Construct, id: string, props: BaseHANodesStackProps) {
32+
super(scope, id, props);
33+
34+
const REGION = cdk.Stack.of(this).region;
35+
const STACK_NAME = cdk.Stack.of(this).stackName;
36+
const lifecycleHookName = STACK_NAME;
37+
const autoScalingGroupName = STACK_NAME;
38+
39+
const {
40+
instanceType,
41+
instanceCpuType,
42+
baseNetworkId,
43+
baseNodeConfiguration,
44+
restoreFromSnapshot,
45+
l1ExecutionEndpoint,
46+
l1ConsensusEndpoint,
47+
dataVolume,
48+
albHealthCheckGracePeriodMin,
49+
heartBeatDelayMin,
50+
numberOfNodes
51+
} = props;
52+
53+
// using default vpc
54+
const vpc = ec2.Vpc.fromLookup(this, "vpc", { isDefault: true });
55+
56+
// setting up the security group for the node from BSC-specific construct
57+
const instanceSG = new BaseNodeSecurityGroupConstruct(this, "security-group", { vpc: vpc });
58+
59+
// getting the IAM Role ARM from the common stack
60+
const importedInstanceRoleArn = cdk.Fn.importValue("BaseNodeInstanceRoleArn");
61+
62+
const instanceRole = iam.Role.fromRoleArn(this, "iam-role", importedInstanceRoleArn);
63+
64+
// making our scripts and configs from the local "assets" directory available for instance to download
65+
const asset = new s3Assets.Asset(this, "assets", {
66+
path: path.join(__dirname, "assets")
67+
});
68+
69+
asset.bucket.grantRead(instanceRole);
70+
71+
// parsing user data script and injecting necessary variables
72+
const nodeScript = fs.readFileSync(path.join(__dirname, "assets", "user-data", "node.sh")).toString();
73+
const dataVolumeSizeBytes = dataVolume.sizeGiB * constants.GibibytesToBytesConversionCoefficient;
74+
75+
const modifiedInitNodeScript = cdk.Fn.sub(nodeScript, {
76+
_REGION_: REGION,
77+
_ASSETS_S3_PATH_: `s3://${asset.s3BucketName}/${asset.s3ObjectKey}`,
78+
_STACK_NAME_: STACK_NAME,
79+
_NODE_CF_LOGICAL_ID_: constants.NoneValue,
80+
_DATA_VOLUME_TYPE_: dataVolume.type,
81+
_DATA_VOLUME_SIZE_: dataVolumeSizeBytes.toString(),
82+
_NETWORK_ID_: baseNetworkId,
83+
_NODE_CONFIG_: baseNodeConfiguration,
84+
_LIFECYCLE_HOOK_NAME_: lifecycleHookName,
85+
_AUTOSCALING_GROUP_NAME_: autoScalingGroupName,
86+
_RESTORE_FROM_SNAPSHOT_: restoreFromSnapshot.toString(),
87+
_FORMAT_DISK_: "true",
88+
_L1_EXECUTION_ENDPOINT_: l1ExecutionEndpoint,
89+
_L1_CONSENSUS_ENDPOINT_: l1ConsensusEndpoint,
90+
_SNAPSHOT_URL_: props.snapshotUrl,
91+
});
92+
93+
const rpcNodes = new HANodesConstruct(this, "rpc-nodes", {
94+
instanceType,
95+
dataVolumes: [dataVolume],
96+
machineImage: new ec2.AmazonLinuxImage({
97+
generation: AmazonLinuxGeneration.AMAZON_LINUX_2,
98+
kernel:ec2.AmazonLinuxKernel.KERNEL5_X,
99+
cpuType: instanceCpuType
100+
}),
101+
role: instanceRole,
102+
vpc,
103+
securityGroup: instanceSG.securityGroup,
104+
userData: modifiedInitNodeScript,
105+
numberOfNodes,
106+
rpcPortForALB: 8545,
107+
albHealthCheckGracePeriodMin,
108+
heartBeatDelayMin,
109+
lifecycleHookName: lifecycleHookName,
110+
autoScalingGroupName: autoScalingGroupName
111+
});
112+
113+
114+
115+
new cdk.CfnOutput(this, "alb-url", { value: rpcNodes.loadBalancerDnsName });
116+
117+
// Adding suppressions to the stack
118+
nag.NagSuppressions.addResourceSuppressions(
119+
this,
120+
[
121+
{
122+
id: "AwsSolutions-AS3",
123+
reason: "No notifications needed"
124+
},
125+
{
126+
id: "AwsSolutions-S1",
127+
reason: "No access log needed for ALB logs bucket"
128+
},
129+
{
130+
id: "AwsSolutions-EC28",
131+
reason: "Using basic monitoring to save costs"
132+
},
133+
{
134+
id: "AwsSolutions-IAM5",
135+
reason: "Need read access to the S3 bucket with assets"
136+
}
137+
],
138+
true
139+
);
140+
}
141+
}

0 commit comments

Comments
 (0)