Skip to content

Commit 391ad6d

Browse files
authored
Addition of function to calculate rally point instance in AWS Auto Scaling Groups (#33)
* Dockerfiles have been updated to use Python 3 due to multiple issues with running under Python 2. * A new function, aws_wrapper_get_asg_rally_point, has been added. This allows you to calculate a single 'rally point' instance that is part of an auto-scaling group, useful for executing bootstrapping commands once per ASG. * Refactored new unit tests to include a custom setup function for each set, and split out public vs private hostname call scenarios to be consistent with other tests. * The Dockerfiles for tests have been updated to include setting some encoding environment variables, which resolves some issues when running tests, as well as removing some un-necessary 'sudo' calls. * Steps to install some new dependencies have been moved to the 'Install basic dependencies' section of the test Dockerfiles per comments from maintainer. * The comment for the new function has been updated per maintainer comment. * Variable usage and whitespace have been updated per comments from the maintainer. * Additional tests have been added for the 'aws_wrapper_get_asg_rally_point' function to ensure that the function fails when the ASG does not contain the requisite number of instances after the given retry / wait period has elapsed. * The Dockerfiles for the tests have been updated to include better comments / details around the use of the locale environment variables.
1 parent 1f45461 commit 391ad6d

File tree

4 files changed

+342
-3
lines changed

4 files changed

+342
-3
lines changed

Dockerfile.ubuntu16.04.bats

+11-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ MAINTAINER Gruntwork <info@gruntwork.io>
33

44
# Install basic dependencies
55
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
6-
apt-get install -y vim python-pip jq sudo curl
6+
apt-get install -y vim python-pip jq sudo curl libffi-dev python3-dev && \
7+
update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \
8+
update-alternatives --config python && \
9+
curl https://bootstrap.pypa.io/pip/3.5/get-pip.py -o /tmp/get-pip.py && \
10+
python /tmp/get-pip.py
711

812
# Install Bats
913
RUN apt-get install -y software-properties-common && \
@@ -25,3 +29,9 @@ RUN apt-get install -y net-tools iptables
2529

2630
# Copy mock AWS CLI into the PATH
2731
COPY ./.circleci/aws-local.sh /usr/local/bin/aws
32+
33+
# These have been added to resolve some encoding error issues with the tests. These were introduced during the upgrade to Python 3.6,
34+
# which is known to have some sensitivity around locale issues. These variables should correct that, per examples like this SO thread:
35+
# https://stackoverflow.com/questions/51026315/how-to-solve-unicodedecodeerror-in-python-3-6/51027262#51027262.
36+
ENV LC_ALL=C.UTF-8
37+
ENV LANG=C.UTF-8

Dockerfile.ubuntu18.04.bats

+13-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ MAINTAINER Gruntwork <info@gruntwork.io>
33

44
# Install basic dependencies
55
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
6-
apt-get install -y vim python-pip jq sudo curl
6+
apt-get install -y vim python-pip jq sudo curl libffi-dev python3-dev
77

88
# Install Bats
99
RUN apt-get install -y software-properties-common && \
@@ -12,7 +12,12 @@ RUN apt-get install -y software-properties-common && \
1212
apt-get install -y bats
1313

1414
# Install AWS CLI
15-
RUN pip install awscli --upgrade --user
15+
RUN apt install python3-distutils python3-apt -y && \
16+
update-alternatives --install /usr/bin/python python /usr/bin/python3 1 && \
17+
update-alternatives --config python && \
18+
curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py && \
19+
python /tmp/get-pip.py && \
20+
pip install awscli --upgrade --user
1621

1722
# Install moto: https://github.com/spulec/moto
1823
# Lock cfn-lint and pysistent to last known working versions
@@ -23,3 +28,9 @@ RUN apt-get install -y net-tools iptables
2328

2429
# Copy mock AWS CLI into the PATH
2530
COPY ./.circleci/aws-local.sh /usr/local/bin/aws
31+
32+
# These have been added to resolve some encoding error issues with the tests. These were introduced during the upgrade to Python 3.6,
33+
# which is known to have some sensitivity around locale issues. These variables should correct that, per examples like this SO thread:
34+
# https://stackoverflow.com/questions/51026315/how-to-solve-unicodedecodeerror-in-python-3-6/51027262#51027262.
35+
ENV LC_ALL=C.UTF-8
36+
ENV LANG=C.UTF-8

modules/bash-commons/src/aws-wrapper.sh

+33
Original file line numberDiff line numberDiff line change
@@ -215,3 +215,36 @@ function aws_wrapper_get_hostname {
215215
aws_get_instance_private_hostname
216216
fi
217217
}
218+
219+
# Calculates a "rally point" instance in an ASG and returns its hostname. This is a deterministic way for the instances in an ASG to all pick the same single instance to perform some action: e.g., this instance could become the leader in a cluster or run some initialization script that should only be run once for the entire ASG. Under the hood, this method picks the instance in the ASG with the earliest launch time; in the case of ties, the instance with the earliest instance ID (lexicographically) is returned. This method assumes jq is installed.
220+
function aws_wrapper_get_asg_rally_point {
221+
local -r asg_name="$1"
222+
local -r aws_region="$2"
223+
local -r use_public_hostname="$3"
224+
local -r retries="${4:-60}"
225+
local -r sleep_between_retries="${5:-5}"
226+
227+
log_info "Calculating rally point for ASG $asg_name in $aws_region"
228+
229+
local instances
230+
log_info "Waiting for all instances to be available..."
231+
instances=$(aws_wrapper_wait_for_instances_in_asg $asg_name $aws_region $retries $sleep_between_retries)
232+
assert_not_empty_or_null "$instances" "Wait for instances in ASG $asg_name in $aws_region"
233+
234+
local rally_point
235+
rally_point=$(echo "$instances" | jq -r '[.Reservations[].Instances[]] | sort_by(.LaunchTime, .InstanceId) | .[0]')
236+
assert_not_empty_or_null "$rally_point" "Select rally point server in ASG $asg_name"
237+
238+
local hostname_field=".PrivateDnsName"
239+
if [[ "$use_public_hostname" == "true" ]]; then
240+
hostname_field=".PublicDnsName"
241+
fi
242+
243+
log_info "Hostname field is $hostname_field"
244+
245+
local hostname
246+
hostname=$(echo "$rally_point" | jq -r "$hostname_field")
247+
assert_not_empty_or_null "$hostname" "Get hostname from field $hostname_field for rally point in $asg_name: $rally_point"
248+
249+
echo -n "$hostname"
250+
}

test/aws-wrapper.bats

+285
Original file line numberDiff line numberDiff line change
@@ -1311,4 +1311,289 @@ END_HEREDOC
13111311

13121312
assert_success
13131313
assert_equal "ip-10-251-50-12.ec2.internal" "$out"
1314+
}
1315+
1316+
function setup_rally_point_by_instance {
1317+
local -r asg_name="$1"
1318+
local -r aws_region="$2"
1319+
local -r size=3
1320+
1321+
local -r asg=$(cat <<END_HEREDOC
1322+
{
1323+
"AutoScalingGroups": [
1324+
{
1325+
"AutoScalingGroupARN": "arn:aws:autoscaling:us-west-2:123456789012:autoScalingGroup:930d940e-891e-4781-a11a-7b0acd480f03:autoScalingGroupName/$asg_name",
1326+
"DesiredCapacity": $size,
1327+
"AutoScalingGroupName": "$asg_name",
1328+
"MinSize": 0,
1329+
"MaxSize": 10,
1330+
"LaunchConfigurationName": "my-launch-config",
1331+
"CreatedTime": "2013-08-19T20:53:25.584Z",
1332+
"AvailabilityZones": [
1333+
"${aws_region}c"
1334+
]
1335+
}
1336+
]
1337+
}
1338+
END_HEREDOC
1339+
)
1340+
1341+
local -r instances=$(cat <<END_HEREDOC
1342+
{
1343+
"Reservations": [
1344+
{
1345+
"Instances": [
1346+
{
1347+
"LaunchTime": "2013-08-19T20:53:25.584Z",
1348+
"InstanceId": "i-1234567890abcdef0",
1349+
"PublicIpAddress": "55.66.77.88",
1350+
"PrivateIpAddress": "11.22.33.44",
1351+
"PrivateDnsName": "ip-10-251-50-12.ec2.internal",
1352+
"PublicDnsName": "ec2-203-0-113-25.compute-1.amazonaws.com",
1353+
"Tags": [
1354+
{
1355+
"Value": "$asg_name",
1356+
"Key": "aws:autoscaling:groupName"
1357+
}
1358+
]
1359+
}
1360+
]
1361+
},
1362+
{
1363+
"Instances": [
1364+
{
1365+
"LaunchTime": "2013-08-19T20:53:25.584Z",
1366+
"InstanceId": "i-1234567890abcdef1",
1367+
"PublicIpAddress": "55.66.77.881",
1368+
"PrivateIpAddress": "11.22.33.441",
1369+
"PrivateDnsName": "ip-10-251-50-121.ec2.internal",
1370+
"PublicDnsName": "ec2-203-0-113-252.compute-1.amazonaws.com",
1371+
"Tags": [
1372+
{
1373+
"Value": "$asg_name",
1374+
"Key": "aws:autoscaling:groupName"
1375+
}
1376+
]
1377+
},
1378+
{
1379+
"LaunchTime": "2013-08-19T20:53:25.584Z",
1380+
"InstanceId": "i-1234567890abcdef2",
1381+
"PublicIpAddress": "55.66.77.882",
1382+
"PrivateIpAddress": "11.22.33.442",
1383+
"PrivateDnsName": "ip-10-251-50-122.ec2.internal",
1384+
"PublicDnsName": "ec2-203-0-113-253.compute-1.amazonaws.com",
1385+
"Tags": [
1386+
{
1387+
"Value": "$asg_name",
1388+
"Key": "aws:autoscaling:groupName"
1389+
}
1390+
]
1391+
}
1392+
]
1393+
}
1394+
]
1395+
}
1396+
END_HEREDOC
1397+
)
1398+
1399+
load_aws_mock "" "$asg" "$instances"
1400+
}
1401+
1402+
@test "aws_wrapper_get_asg_rally_point by instance id, private" {
1403+
local -r asg_name="foo"
1404+
local -r aws_region="us-west-2"
1405+
1406+
setup_rally_point_by_instance $asg_name $aws_region
1407+
1408+
local out
1409+
1410+
out=$(aws_wrapper_get_asg_rally_point $asg_name $aws_region)
1411+
assert_success
1412+
assert_equal "$out" "ip-10-251-50-12.ec2.internal"
1413+
}
1414+
1415+
@test "aws_wrapper_get_asg_rally_point by instance id, public" {
1416+
local -r asg_name="foo"
1417+
local -r aws_region="us-west-2"
1418+
1419+
setup_rally_point_by_instance $asg_name $aws_region
1420+
1421+
local out
1422+
1423+
out=$(aws_wrapper_get_asg_rally_point $asg_name $aws_region true)
1424+
assert_success
1425+
assert_equal "$out" "ec2-203-0-113-25.compute-1.amazonaws.com"
1426+
}
1427+
1428+
function setup_rally_point_by_launch_time {
1429+
local -r asg_name="$1"
1430+
local -r aws_region="$2"
1431+
local -r size=3
1432+
1433+
local -r asg=$(cat <<END_HEREDOC
1434+
{
1435+
"AutoScalingGroups": [
1436+
{
1437+
"AutoScalingGroupARN": "arn:aws:autoscaling:us-west-2:123456789012:autoScalingGroup:930d940e-891e-4781-a11a-7b0acd480f03:autoScalingGroupName/$asg_name",
1438+
"DesiredCapacity": $size,
1439+
"AutoScalingGroupName": "$asg_name",
1440+
"MinSize": 0,
1441+
"MaxSize": 10,
1442+
"LaunchConfigurationName": "my-launch-config",
1443+
"CreatedTime": "2013-08-19T20:53:25.584Z",
1444+
"AvailabilityZones": [
1445+
"${aws_region}c"
1446+
]
1447+
}
1448+
]
1449+
}
1450+
END_HEREDOC
1451+
)
1452+
1453+
local -r instances=$(cat <<END_HEREDOC
1454+
{
1455+
"Reservations": [
1456+
{
1457+
"Instances": [
1458+
{
1459+
"LaunchTime": "2013-08-19T20:53:25.584Z",
1460+
"InstanceId": "i-1234567890abcdef0",
1461+
"PublicIpAddress": "55.66.77.88",
1462+
"PrivateIpAddress": "11.22.33.44",
1463+
"PrivateDnsName": "ip-10-251-50-12.ec2.internal",
1464+
"PublicDnsName": "ec2-203-0-113-25.compute-1.amazonaws.com",
1465+
"Tags": [
1466+
{
1467+
"Value": "$asg_name",
1468+
"Key": "aws:autoscaling:groupName"
1469+
}
1470+
]
1471+
}
1472+
]
1473+
},
1474+
{
1475+
"Instances": [
1476+
{
1477+
"LaunchTime": "2013-08-19T20:53:25.584Z",
1478+
"InstanceId": "i-1234567890abcdef1",
1479+
"PublicIpAddress": "55.66.77.881",
1480+
"PrivateIpAddress": "11.22.33.441",
1481+
"PrivateDnsName": "ip-10-251-50-121.ec2.internal",
1482+
"PublicDnsName": "ec2-203-0-113-252.compute-1.amazonaws.com",
1483+
"Tags": [
1484+
{
1485+
"Value": "$asg_name",
1486+
"Key": "aws:autoscaling:groupName"
1487+
}
1488+
]
1489+
},
1490+
{
1491+
"LaunchTime": "2013-08-19T20:53:24.584Z",
1492+
"InstanceId": "i-1234567890abcdef2",
1493+
"PublicIpAddress": "55.66.77.882",
1494+
"PrivateIpAddress": "11.22.33.442",
1495+
"PrivateDnsName": "ip-10-251-50-122.ec2.internal",
1496+
"PublicDnsName": "ec2-203-0-113-253.compute-1.amazonaws.com",
1497+
"Tags": [
1498+
{
1499+
"Value": "$asg_name",
1500+
"Key": "aws:autoscaling:groupName"
1501+
}
1502+
]
1503+
}
1504+
]
1505+
}
1506+
]
1507+
}
1508+
END_HEREDOC
1509+
)
1510+
1511+
load_aws_mock "" "$asg" "$instances"
1512+
}
1513+
1514+
@test "aws_wrapper_get_asg_rally_point by launch time, private" {
1515+
local -r asg_name="foo"
1516+
local -r aws_region="us-west-2"
1517+
1518+
setup_rally_point_by_launch_time $asg_name $aws_region
1519+
1520+
local out
1521+
1522+
out=$(aws_wrapper_get_asg_rally_point $asg_name $aws_region)
1523+
assert_success
1524+
assert_equal "$out" "ip-10-251-50-122.ec2.internal"
1525+
}
1526+
1527+
@test "aws_wrapper_get_asg_rally_point by launch time, public" {
1528+
local -r asg_name="foo"
1529+
local -r aws_region="us-west-2"
1530+
1531+
setup_rally_point_by_launch_time $asg_name $aws_region
1532+
1533+
local out
1534+
1535+
out=$(aws_wrapper_get_asg_rally_point $asg_name $aws_region true)
1536+
assert_success
1537+
assert_equal "$out" "ec2-203-0-113-253.compute-1.amazonaws.com"
1538+
}
1539+
1540+
function setup_rally_point_empty {
1541+
local -r asg_name="$1"
1542+
local -r aws_region="$2"
1543+
local -r size=3
1544+
1545+
local -r asg=$(cat <<END_HEREDOC
1546+
{
1547+
"AutoScalingGroups": [
1548+
{
1549+
"AutoScalingGroupARN": "arn:aws:autoscaling:us-west-2:123456789012:autoScalingGroup:930d940e-891e-4781-a11a-7b0acd480f03:autoScalingGroupName/$asg_name",
1550+
"DesiredCapacity": $size,
1551+
"AutoScalingGroupName": "$asg_name",
1552+
"MinSize": 0,
1553+
"MaxSize": 10,
1554+
"LaunchConfigurationName": "my-launch-config",
1555+
"CreatedTime": "2013-08-19T20:53:25.584Z",
1556+
"AvailabilityZones": [
1557+
"${aws_region}c"
1558+
]
1559+
}
1560+
]
1561+
}
1562+
END_HEREDOC
1563+
)
1564+
1565+
local -r instances=$(cat <<END_HEREDOC
1566+
{
1567+
"Reservations": []
1568+
}
1569+
END_HEREDOC
1570+
)
1571+
1572+
load_aws_mock "" "$asg" "$instances"
1573+
}
1574+
1575+
@test "aws_wrapper_get_asg_rally_point empty asg, private" {
1576+
local -r asg_name="foo"
1577+
local -r aws_region="us-west-2"
1578+
local -r use_public_hostname="false"
1579+
local -r retries=5
1580+
local -r sleep_between_retries=2
1581+
1582+
setup_rally_point_empty $asg_name $aws_region
1583+
1584+
run aws_wrapper_get_asg_rally_point $asg_name $aws_region $use_public_hostname $retries $sleep_between_retries
1585+
assert_failure
1586+
}
1587+
1588+
@test "aws_wrapper_get_asg_rally_point empty asg, public" {
1589+
local -r asg_name="foo"
1590+
local -r aws_region="us-west-2"
1591+
local -r use_public_hostname="true"
1592+
local -r retries=5
1593+
local -r sleep_between_retries=2
1594+
1595+
setup_rally_point_empty $asg_name $aws_region
1596+
1597+
run aws_wrapper_get_asg_rally_point $asg_name $aws_region $use_public_hostname $retries $sleep_between_retries
1598+
assert_failure
13141599
}

0 commit comments

Comments
 (0)