Cloud
banshee/cloud-collection/cloud/aws/https/
ec2-instances
The state and the two status checks of each EC2 instance of an AWS region, by the Amazon EC2 API.
by banshee0 installations1 version
Published on October 27, 2026. First version.
// The EC2 instances of an AWS region, by the Amazon EC2 API with an access key: for each instance, its state
// and the results of its two status checks, the one of the AWS systems it runs on and its own.
// Reads GET https://ec2.<region>.amazonaws.com/?Action=DescribeInstanceStatus, page after page, signed by
// Signature Version 4.
need('protocol:http');
need('protocol:aws');
$region = variable('aws:region', 'text', array(
'label' => 'Region',
'help' => 'The AWS region of the instances, as AWS writes it in an API: eu-west-3, us-east-1.',
));
per('instance', 'Instances');
output('state', 'text', array('label' => 'State', 'per' => 'instance'));
output('system-status', 'text', array('label' => 'System status check', 'per' => 'instance'));
output('instance-status', 'text', array('label' => 'Instance status check', 'per' => 'instance'));
watch.expect('not-running', 'Instance not running', 'state', array('critical' => array('running'), 'for' => '2 runs'));
watch.state('system-impaired', 'System status check failed', 'system-status', array('critical' => array('impaired')));
watch.state('instance-impaired', 'Instance status check failed', 'instance-status', array('critical' => array('impaired')));
watch.missing('missing', 'Instance missing', 'state', array('enabled' => false));
// One signed GET on an AWS API of the Query protocol (EC2, RDS, CloudWatch): the parameters in the address, the
// answer in XML. aws.sign gives the headers that carry the signature: the keys never leave. A refusal is told
// by the error code AWS writes in its answer; a throttled request or a fault of AWS is not one.
function aws_get($service, $region, $query, $what) {
$url = 'https://' . $service . '.' . $region . '.amazonaws.com/?' . url.query($query);
$answer = http.get($url, headers=aws.sign('GET', $url, array(), '', $region, $service));
if ($answer['status'] != 200) {
$code = '';
$found = text.match($answer['body'], '<Code>([^<]+)</Code>');
if (list.count($found) > 0) {
$code = $found[1];
}
if ($code == 'Throttling' || $code == 'RequestLimitExceeded' || $answer['status'] >= 500) {
fail('unreachable', 'AWS answers the reading of ' . $what . ' with code ' . $answer['status'] . ' ' . $code);
}
if ($answer['status'] == 400 || $answer['status'] == 401 || $answer['status'] == 403) {
fail('misconfigured', 'AWS refuses to read ' . $what . ', code ' . $answer['status'] . ' ' . $code,
'Check the access key, the permissions of its user or role, and the region.');
}
fail('unreachable', 'AWS answers the reading of ' . $what . ' with code ' . $answer['status']);
}
return xml.read($answer['body']);
}
// The first child of that name, array() when it is absent or empty. An empty element, such as a list without
// members written <name/>, reads as the text '' and not as an array.
function element($parent, $name) {
$children = list.get($parent, $name, array());
if (list.count($children) == 0 || !list.is($children[0])) {
return array();
}
return $children[0];
}
// The text of the first child of that name, '' when it is absent.
function child_text($parent, $name) {
$children = list.get($parent, $name, array());
if (list.count($children) == 0) {
return '';
}
return $children[0];
}
// The region goes into the name of the host: it must be a name.
if (list.count(text.match($region, '^[a-z0-9-]+$')) == 0) {
fail('misconfigured', 'The region holds other characters than lower-case letters, digits and hyphens');
}
// <DescribeInstanceStatusResponse><requestId>…</requestId><instanceStatusSet><item><instanceId>i-0123…</instanceId>
// <instanceState><code>16</code><name>running</name></instanceState><systemStatus><status>ok</status>…
// </systemStatus><instanceStatus><status>ok</status>…</instanceStatus></item></instanceStatusSet>
// <nextToken>…</nextToken></DescribeInstanceStatusResponse>. IncludeAllInstances asks for the stopped
// instances too, which AWS leaves out by default.
$token = '';
while (true) {
$query = array('Action' => 'DescribeInstanceStatus', 'Version' => '2016-11-15', 'IncludeAllInstances' => 'true');
if ($token != '') {
$query['NextToken'] = $token;
}
$answer = element(aws_get('ec2', $region, $query, 'the EC2 instances'), 'DescribeInstanceStatusResponse');
foreach (list.get(element($answer, 'instanceStatusSet'), 'item', array()) as $item) {
$name = child_text($item, 'instanceId');
value('state', child_text(element($item, 'instanceState'), 'name'), $name);
value('system-status', child_text(element($item, 'systemStatus'), 'status'), $name);
value('instance-status', child_text(element($item, 'instanceStatus'), 'status'), $name);
}
$token = child_text($answer, 'nextToken');
if ($token == '') {
break;
}
}