Skip to content

Commit 2e1229a

Browse files
committed
Etcd cluster.
1 parent 2100228 commit 2e1229a

4 files changed

Lines changed: 235 additions & 19 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
create-stack:
22
aws cloudformation create-stack --stack-name asyncdb --template-body file://cloudformation.json --capabilities CAPABILITY_NAMED_IAM
33

4+
update-stack:
5+
aws cloudformation update-stack --stack-name asyncdb --template-body file://cloudformation.json --capabilities CAPABILITY_NAMED_IAM
6+
47
delete-stack:
58
aws cloudformation delete-stack --stack-name asyncdb
69

cloudformation.json

Lines changed: 177 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
"Type": "AWS::EC2::Subnet",
3535
"Properties": {
3636
"VpcId": { "Ref": "VPC" },
37-
"CidrBlock": "10.0.1.0/24",
37+
"CidrBlock": "10.0.0.0/24",
3838
"AvailabilityZone": { "Fn::Select": [ 0, { "Fn::GetAZs": "" } ] },
3939
"MapPublicIpOnLaunch": true
4040
}
@@ -43,7 +43,7 @@
4343
"Type": "AWS::EC2::Subnet",
4444
"Properties": {
4545
"VpcId": { "Ref": "VPC" },
46-
"CidrBlock": "10.0.2.0/24",
46+
"CidrBlock": "10.0.1.0/24",
4747
"AvailabilityZone": { "Fn::Select": [ 1, { "Fn::GetAZs": "" } ] },
4848
"MapPublicIpOnLaunch": true
4949
}
@@ -52,7 +52,7 @@
5252
"Type": "AWS::EC2::Subnet",
5353
"Properties": {
5454
"VpcId": { "Ref": "VPC" },
55-
"CidrBlock": "10.0.3.0/24",
55+
"CidrBlock": "10.0.2.0/24",
5656
"AvailabilityZone": { "Fn::Select": [ 2, { "Fn::GetAZs": "" } ] },
5757
"MapPublicIpOnLaunch": true
5858
}
@@ -118,8 +118,14 @@
118118
"Type": "AWS::EC2::SecurityGroup",
119119
"Properties": {
120120
"VpcId": { "Ref": "VPC" },
121-
"GroupDescription": "Allow inbound traffic from application load balancer",
121+
"GroupDescription": "Allow inbound traffic from application load balancer",
122122
"SecurityGroupIngress": [
123+
{
124+
"IpProtocol": "tcp",
125+
"FromPort": 22,
126+
"ToPort": 22,
127+
"CidrIp": "0.0.0.0/0"
128+
},
123129
{
124130
"IpProtocol": "tcp",
125131
"FromPort": 80,
@@ -135,6 +141,33 @@
135141
]
136142
}
137143
},
144+
"EtcdSecurityGroup": {
145+
"Type": "AWS::EC2::SecurityGroup",
146+
"Properties": {
147+
"VpcId": { "Ref": "VPC" },
148+
"GroupDescription": "Allow etcd traffic",
149+
"SecurityGroupIngress": [
150+
{
151+
"IpProtocol": "tcp",
152+
"FromPort": 22,
153+
"ToPort": 22,
154+
"CidrIp": "0.0.0.0/0"
155+
},
156+
{
157+
"IpProtocol": "tcp",
158+
"FromPort": 2379,
159+
"ToPort": 2380,
160+
"CidrIp": "0.0.0.0/0"
161+
}
162+
],
163+
"SecurityGroupEgress": [
164+
{
165+
"IpProtocol": "-1",
166+
"CidrIp": "0.0.0.0/0"
167+
}
168+
]
169+
}
170+
},
138171
"InstanceRole": {
139172
"Type": "AWS::IAM::Role",
140173
"Properties": {
@@ -170,23 +203,24 @@
170203
"Name": { "Ref": "InstanceProfile" }
171204
},
172205
"SecurityGroupIds": [ { "Ref": "InstanceSecurityGroup" } ],
206+
"KeyName": "asyncdb",
173207
"UserData": {
174208
"Fn::Base64": {
175209
"Fn::Join": [
176-
"",
210+
"\n",
177211
[
178-
"#! /bin/bash\n",
179-
"sudo yum update\n",
180-
"sudo yum -y install unzip\n",
181-
"curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"\n",
182-
"unzip awscliv2.zip\n",
183-
"./aws/install\n",
184-
"REGISTRY_URL=332187735950.dkr.ecr.eu-west-2.amazonaws.com\n",
185-
"VERSION=0.0.2\n",
186-
"IMAGE=$REGISTRY_URL/asyncdb:$VERSION\n",
187-
"aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin $REGISTRY_URL\n",
188-
"docker pull $IMAGE\n",
189-
"docker run -d -p 80:80 $IMAGE\n"
212+
"#! /bin/bash",
213+
"sudo yum update",
214+
"sudo yum -y install unzip",
215+
"curl \"https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip\" -o \"awscliv2.zip\"",
216+
"unzip awscliv2.zip",
217+
"./aws/install",
218+
"REGISTRY_URL=332187735950.dkr.ecr.eu-west-2.amazonaws.com",
219+
"VERSION=0.0.2",
220+
"IMAGE=$REGISTRY_URL/asyncdb:$VERSION",
221+
"aws ecr get-login-password --region eu-west-2 | docker login --username AWS --password-stdin $REGISTRY_URL",
222+
"docker pull $IMAGE",
223+
"docker run -d -p 80:80 $IMAGE"
190224
]
191225
]
192226
}
@@ -199,7 +233,11 @@
199233
"Properties": {
200234
"Name": "ClusterALB",
201235
"Scheme": "internet-facing",
202-
"Subnets": [{ "Ref": "PublicSubnet1" }, { "Ref": "PublicSubnet2" }, { "Ref": "PublicSubnet3" }],
236+
"Subnets": [
237+
{ "Ref": "PublicSubnet1" },
238+
{ "Ref": "PublicSubnet2" },
239+
{ "Ref": "PublicSubnet3" }
240+
],
203241
"SecurityGroups": [{ "Ref": "ALBSecurityGroup" }]
204242
}
205243
},
@@ -230,7 +268,11 @@
230268
"AutoScalingGroup": {
231269
"Type": "AWS::AutoScaling::AutoScalingGroup",
232270
"Properties": {
233-
"VPCZoneIdentifier": [ { "Ref": "PublicSubnet1" }, { "Ref": "PublicSubnet2" }, { "Ref": "PublicSubnet3" } ],
271+
"VPCZoneIdentifier": [
272+
{ "Ref": "PublicSubnet1" },
273+
{ "Ref": "PublicSubnet2" },
274+
{ "Ref": "PublicSubnet3" }
275+
],
234276
"LaunchTemplate": {
235277
"LaunchTemplateId": { "Ref": "LaunchTemplate" },
236278
"Version": { "Fn::GetAtt": [ "LaunchTemplate", "LatestVersionNumber" ] }
@@ -240,6 +282,122 @@
240282
"MaxSize": "4",
241283
"TargetGroupARNs":[{ "Ref":"ALBTargetGroup" } ]
242284
}
285+
},
286+
"DiscoveryTokenLambdaRole": {
287+
"Type": "AWS::IAM::Role",
288+
"Properties": {
289+
"AssumeRolePolicyDocument": {
290+
"Version": "2012-10-17",
291+
"Statement": [
292+
{
293+
"Effect": "Allow",
294+
"Principal": {
295+
"Service": "lambda.amazonaws.com"
296+
},
297+
"Action": "sts:AssumeRole"
298+
}
299+
]
300+
},
301+
"ManagedPolicyArns": [
302+
"arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
303+
]
304+
}
305+
},
306+
"DiscoveryTokenLambda": {
307+
"Type": "AWS::Lambda::Function",
308+
"Properties": {
309+
"Handler": "index.handler",
310+
"Role": { "Fn::GetAtt": ["DiscoveryTokenLambdaRole", "Arn"] },
311+
"Runtime": "python3.9",
312+
"Timeout": 30,
313+
"Code": {
314+
"ZipFile": {
315+
"Fn::Join": [
316+
"\n",
317+
[
318+
"import urllib.request",
319+
"import cfnresponse",
320+
"def handler(event, context):",
321+
" try:",
322+
" if event['RequestType'] in ('Create','Update'):",
323+
" url = \"https://discovery.etcd.io/new?size=3\"",
324+
" token = urllib.request.urlopen(url).read().decode().strip()",
325+
" cfnresponse.send(event, context, cfnresponse.SUCCESS,",
326+
" { 'DiscoveryURL': token })",
327+
" else:",
328+
" cfnresponse.send(event, context, cfnresponse.SUCCESS, {})",
329+
" except Exception as e:",
330+
" print(\"Error:\", e)",
331+
" cfnresponse.send(event, context, cfnresponse.FAILED, {})"
332+
]
333+
]
334+
}
335+
}
336+
}
337+
},
338+
"DiscoveryTokenCustomResource": {
339+
"Type": "Custom::EtcdDiscovery",
340+
"Properties": {
341+
"ServiceToken": { "Fn::GetAtt": ["DiscoveryTokenLambda", "Arn"] }
342+
}
343+
},
344+
"EtcdLaunchTemplate": {
345+
"Type": "AWS::EC2::LaunchTemplate",
346+
"Properties": {
347+
"LaunchTemplateData": {
348+
"ImageId": { "Ref": "ECSAMI" },
349+
"InstanceType": { "Ref": "InstanceType" },
350+
"SecurityGroupIds": [{ "Ref": "EtcdSecurityGroup" }],
351+
"KeyName": "asyncdb",
352+
"UserData": {
353+
"Fn::Base64": {
354+
"Fn::Join": [
355+
"\n",
356+
[
357+
"#! /bin/bash",
358+
"PRIVATE_IP=$(curl -s http://169.254.169.254/latest/meta-data/local-ipv4)",
359+
"PUBLIC_IP=$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4)",
360+
"INSTANCE=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)",
361+
{ "Fn::Sub": ["DISCOVERY_URL='${DiscoveryTokenCustomResource.DiscoveryURL}'", {}] },
362+
"docker run -d -v /usr/share/ca-certificates/:/etc/ssl/certs -p 4001:4001 -p 2380:2380 -p 2379:2379 \\",
363+
" --name etcd quay.io/coreos/etcd:v2.3.8 \\",
364+
" -name ${INSTANCE} \\",
365+
" -advertise-client-urls http://${PUBLIC_IP}:2379,http://${PUBLIC_IP}:4001 \\",
366+
" -listen-client-urls http://0.0.0.0:2379,http://0.0.0.0:4001 \\",
367+
" -initial-advertise-peer-urls http://${PUBLIC_IP}:2380 \\",
368+
" -listen-peer-urls http://0.0.0.0:2380 \\",
369+
" -discovery ${DISCOVERY_URL} \\"
370+
]
371+
]
372+
}
373+
}
374+
}
375+
}
376+
},
377+
"EtcdAutoScalingGroup": {
378+
"Type": "AWS::AutoScaling::AutoScalingGroup",
379+
"Properties": {
380+
"VPCZoneIdentifier": [
381+
{ "Ref": "PublicSubnet1" },
382+
{ "Ref": "PublicSubnet2" },
383+
{ "Ref": "PublicSubnet3" }
384+
],
385+
"LaunchTemplate": {
386+
"LaunchTemplateId": { "Ref": "EtcdLaunchTemplate" },
387+
"Version": { "Fn::GetAtt": ["EtcdLaunchTemplate", "LatestVersionNumber"] }
388+
},
389+
"MinSize": "3",
390+
"MaxSize": "3",
391+
"DesiredCapacity": "3",
392+
"HealthCheckType": "EC2",
393+
"Tags": [
394+
{
395+
"Key": "Name",
396+
"Value": "etcd-node",
397+
"PropagateAtLaunch": true
398+
}
399+
]
400+
}
243401
}
244402
}
245403
}

test-etcd.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import grpc
2+
import socket
3+
from etcd3.etcdrpc import KVStub, RangeRequest
4+
5+
def test_etcd(elb_hostname: str):
6+
# Resolve ELB hostname to IPv4 manually
7+
ip = socket.gethostbyname(elb_hostname)
8+
target = f"{ip}:2379"
9+
print(f"Connecting to {target}")
10+
11+
# Connect directly to the IP
12+
channel = grpc.insecure_channel(target)
13+
14+
kv = KVStub(channel)
15+
request = RangeRequest(key=b"test")
16+
response = kv.Range(request, timeout=5)
17+
18+
print("Cluster responded OK")
19+
print("KVs returned:", len(response.kvs))
20+
21+
if __name__ == "__main__":
22+
elb = "asyncd-EtcdN-On7N9ya3YCJL-caba21a201f8acff.elb.eu-west-2.amazonaws.com"
23+
test_etcd(elb)
24+

test-etcd.sh

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env bash
2+
# test-etcd-cluster.sh
3+
# Usage: ./test-etcd-cluster.sh <LOAD_BALANCER_DNS_NAME>
4+
5+
LB_DNS=$1
6+
7+
if [ -z "$LB_DNS" ]; then
8+
echo "Usage: $0 <LOAD_BALANCER_DNS_NAME>"
9+
exit 1
10+
fi
11+
12+
echo "Checking etcd cluster via Load Balancer: $LB_DNS"
13+
14+
# 1. Check health
15+
echo "==> Checking cluster health..."
16+
curl -s http://$LB_DNS:2379/health | jq .
17+
18+
# 2. Write a test key
19+
echo "==> Writing test key..."
20+
curl -s http://$LB_DNS:2379/v2/keys/testkey -XPUT -d value="hello-etcd" | jq .
21+
22+
# 3. Read the test key
23+
echo "==> Reading test key..."
24+
curl -s http://$LB_DNS:2379/v2/keys/testkey | jq .
25+
26+
# 4. Delete the test key
27+
echo "==> Deleting test key..."
28+
curl -s http://$LB_DNS:2379/v2/keys/testkey -XDELETE | jq .
29+
30+
echo "Done."
31+

0 commit comments

Comments
 (0)