/*
* Author: Vlad Seryakov vseryakov@gmail.com
* backendjs 2018
*/
const modules = require(__dirname + '/modules');
const logger = require(__dirname + '/logger');
const app = require(__dirname + '/app');
const lib = require(__dirname + '/lib');
/**
* @module aws
*/
const aws = {
name: 'aws',
args: [
{ name: "key", descr: "AWS access key" },
{ name: "secret", descr: "AWS access secret" },
{ name: "token", descr: "AWS security token" },
{ name: "region", descr: "AWS region", pass: 1 },
{ name: "zone", descr: "AWS availability zone" },
{ name: "meta", type: "bool", descr: "Retrieve instance metadata, 0 to disable" },
{ name: "sdk-profile", descr: "AWS SDK profile to use when reading credentials file" },
{ name: "sns-app-arn", descr: "SNS Platform application ARN to be used for push notifications" },
{ name: "key-name", descr: "AWS instance keypair name for remote job instances or other AWS commands" },
{ name: "target-group", descr: "AWS ELB target group to be registered with on start up or other AWS commands" },
{ name: "elastic-ip", descr: "AWS Elastic IP to be associated on start" },
{ name: "host-name", type: "list", descr: "List of hosts to update in Route54 zone with the current private IP address, hosts must be in FQDN format, supports @..@ app.env placeholders" },
{ name: "iam-profile", descr: "IAM instance profile name for instances or commands" },
{ name: "image-id", descr: "AWS image id to be used for instances or commands" },
{ name: "subnet-id", descr: "AWS subnet id to be used for instances or commands" },
{ name: "vpc-id", descr: "AWS VPC id to be used for instances or commands" },
{ name: "group-id", array: 1, descr: "AWS security group(s) to be used for instances or commands" },
{ name: "public-ip", type: "bool", descr: "AWS public IP option for instances or commands" },
{ name: "ecs-cluster", descr: "AWS ECS cluster to use as default" },
{ name: "instance-type", descr: "AWS instance type to launch on demand" },
{ name: "metadata-options", type: "list", descr: "Default instance metadata options" },
{ name: "account-id", descr: "AWS account id if not running on an instance" },
{ name: "eni-id", type: "list", descr: "AWS Elastic Network Interfaces to attach on start, format is: eni[:index],eni..." },
{ name: "config-parameters", descr: "Prefix for AWS Systems Manager parameters to load and parse as config before initializing the database pools", example: "/bkjs/config/" },
{ name: "config-secrets", type: "list", descr: "AWS Secrets Manager filters to load and parse as config before initializing the database pools, supports @..@ app.env placeholders in filters", example: "production,production-@tag@,production-@role@" },
{ name: "config-s3-file", descr: "S3 url for config file to download on start, may include @placeholders@ to refer properties from app.env" },
{ name: "config-s3-interval", type: "int", descr: "Load S3 config file every specified interval in minites" },
],
meta: 1,
metaHost: "169.254.169.254",
key: process.env.AWS_ACCESS_KEY_ID,
secret: process.env.AWS_SECRET_ACCESS_KEY,
token: process.env.AWS_SESSION_TOKEN,
tokenExpiration: 0,
// Current instance details
instance: {},
tags: [],
// Known process roles that need instance metadata
roles: ["shell","web","server","worker","node"],
// Supported regions per service
regions: {
route53domains: ["us-east-1"]
},
endpoints: {
iam: "https://iam.amazonaws.com/",
"iam-us-gov-west-1": "https://iam.us-gov.amazonaws.com/",
"iam-us-gov-east-1": "https://iam.us-gov.amazonaws.com/",
},
retryCount: {
ec2: 1, ssm: 3, sqs: 1, iam: 1, sts: 1, email: 1, monitoring: 1, autoscaling: 1, elasticloadbalancing: 3, sns: 1,
},
_sigCache: { map: {}, list: [] },
};
/**
* AWS API interface, uses API directly for each service, JSON is returned as is but XML repsonses are converted
* using `fast-xml-parser` into objects.
*
* Supports local AWS SDK credentials files and sessions
*
* When AWS environment is detected the `app.env.type` is set to **aws**.
*
* @example
*
* # aws login
*
* # bin/bksh -aws-sdk-profile default
* > aws.s3Query("", "/", (err, rc) => {
* console.log(rc?.ListAllMyBucketsResult?.Buckets)
* })
*/
module.exports = aws;
// Initialization of metadata
aws.configure = function(options, callback)
{
lib.everySeries([
function(next) {
if (!this.meta || app.platform != "linux" || !lib.isFlag(this.roles, app.role)) return next();
if (process.env.AWS_EC2_METADATA_DISABLED) return next();
aws.getInstanceInfo(options, next);
},
function(next) {
if (aws.key || !aws.sdkProfile) return next();
aws.readCredentials(aws.sdkProfile, (err, creds) => {
for (const p in creds) aws[p] = creds[p];
next();
});
},
function(next) {
modules.ipc.on('config:init', aws.readConfig.bind(aws));
aws.readConfig(next);
},
], callback, true);
}
// Execute on primary server startup
aws.configureServer = function(options, callback)
{
// Make sure we are running on EC2 instance
if (app.env.type != "aws") return callback();
var opts = lib.clone(options, { retryCount: options.retryCount || 3, retryOnError: 1 });
lib.everyParallel([
function(next) {
if (!lib.isArray(aws.hostName) || !app.ipaddr) return next();
logger.info("configureServer:", aws.hostName, app.ipaddr, app.env);
lib.forEverySeries(aws.hostName, (host, next2) => {
aws.route53Change(lib.toTemplate(host, [app.env, app]), next2);
}, next, true);
},
function(next) {
if (!aws.elasticIp) return next();
aws.getInstanceDetails((err) => {
opts.subnetId = aws.SubnetId || aws.instance.subnetId;
logger.info("configureServer:", aws.elasticIp, opts);
aws.ec2AssociateAddress(app.env.id, aws.elasticIp, opts, next);
});
},
function(next) {
if (lib.isEmpty(aws.eniId)) return next();
aws.getInstanceDetails((err) => {
logger.info("configureServer:", aws.eniId);
aws.ec2AttachNetworkInterface(aws.eniId, aws.instance, options, next);
});
},
function(next) {
if (!aws.targetGroup) return next();
this.queryELB2("DescribeTargetGroups", { "TargetGroupArns.member.1": aws.targetGroup }, (err, rc) => {
var group = rc?.DescribeTargetGroupsResponse?.DescribeTargetGroupsResult?.TargetGroups?.member;
logger.info("configureServer:", aws.targetGroup, group);
if (!group?.TargetType) return next();
aws.elb2RegisterInstances(aws.targetGroup, group?.TargetType == "ip" ? app.ipaddr: app.env.id, next);
});
},
], callback, true);
}
/**
* Process AWS alarms and state notifications, if such a job is pulled from SQS queue it is handled here and never get to the jobs.
* SNS alarms or EventBridge events must use a SQS qeue as the target.
* @memberof module:aws
* @method configureJob
*/
aws.configureJob = function(options, callback)
{
// AWS SNS notifications
if (options.message.Type == "Notification" && options.message.TopicArn) {
logger.debug("configureJob:", options);
var alarm = lib.jsonParse(options.message.Message);
if (!alarm) return callback("invalid message");
alarm.alarmName = alarm.AlarmName || alarm.Trigger?.MetricName || alarm["detail-type"];
alarm.subject = options.message.Subject;
alarm.topicArn = options.message.TopicArn;
app.runMethods("awsProcessNotification", alarm, { direct: true, parallel: true }, () => {
callback({ status: 200 });
});
return;
}
// EC2/ECS instance status via EventBridge to a SQS queue
if (lib.isFlag(["aws.ec2", "aws.ecs"], options.message.source) && options.message.detail) {
logger.debug("configureJob:", options);
switch (options.message["detail-type"]) {
case "EC2 Instance State-change Notification":
options.message.state = options.message.detail.state;
options.message.instanceId = options.message.detail["instance-id"];
options.method = "awsProcessInstanceStateChange";
break;
case "ECS Task State Change":
options.message.state = options.message.detail.lastStatus;
options.message.taskId = options.message.detail.taskArn.split("/").pop();
options.method = "awsProcessTaskStateChange";
break;
default:
options.method = `awsProcess${options.message.source.split(".").pop().toUpperCase()}Event`;
break;
}
app.runMethods(options.method, options.message, { direct: true, parallel: true }, () => {
callback({ status: 200 });
});
return;
}
callback();
}
aws.shutdown = function(options, callback)
{
lib.deferShutdown(this);
lib.tryCall(callback);
}
/**
* AWS AIM API request
* @memberof module:aws
* @method queryIAM
*/
aws.queryIAM = function(action, obj, options, callback)
{
this.queryEndpoint("iam", '2010-05-08', action, obj, options, callback);
}
/**
* AWS STS API request
* @memberof module:aws
* @method querySTS
*/
aws.querySTS = function(action, obj, options, callback)
{
this.queryEndpoint("sts", '2011-06-15', action, obj, options, callback);
}
/**
* Makes a request to AWS CloudFormation API.
* @memberof module:aws
* @function queryCFN
* @param {string} action - The CloudFormation API action to perform (e.g., `DescribeStacks`, `CreateStack`).
* @param {Object} obj - API-specific parameters as an object.
* @param {Object} options - Optional configuration object
* @param {string} [options.region] - AWS region (e.g., `"us-east-1"`).
* @param {number} [options.retryTimeout] - Request timeout in milliseconds.
* @param {number} [options.retryCount] - Max request retries
* @param {Function} callback - Callback function with:
* signature `(err, data)` where:
* - `err`: Error object if request fails.
* - `data`: Response object from AWS.
* @example
* ```js
* aws.queryCFN(
* 'CreateStack',
* {StackName: 'MyStack', Body: stackTemplate}, // obj
* {region: 'us-west-2'}, // options
* (err, result) => { ... } // callback
* )
*/
aws.queryCFN = function(action, obj, options, callback)
{
this.queryEndpoint("cloudformation", '2010-05-15', action, obj, options, callback);
}
/**
* AWS Elastic Cache API request
* @memberof module:aws
* @method queryElastiCache
*/
aws.queryElastiCache = function(action, obj, options, callback)
{
this.queryEndpoint("elasticache", '2014-09-30', action, obj, options, callback);
}
/**
* AWS Autoscaling API request
* @memberof module:aws
* @method queryAS
*/
aws.queryAS = function(action, obj, options, callback)
{
this.queryEndpoint("autoscaling", '2011-01-01', action, obj, options, callback);
}
/**
* Make a request to the Rekognition service
* @memberof module:aws
* @method queryRekognition
*/
aws.queryRekognition = function(action, obj, options, callback)
{
this.queryService("rekognition", "RekognitionService", action, obj, options, callback);
}
/**
* AWS SSM API request
* @memberof module:aws
* @method querySSM
*/
aws.querySSM = function(action, obj, options, callback)
{
this.queryService("ssm", "AmazonSSM", action, obj, options, callback);
}
/**
* AWS ACM API request
* @memberof module:aws
* @method queryACM
*/
aws.queryACM = function(action, obj, options, callback)
{
this.queryService("acm", "CertificateManager", action, obj, options, callback);
}
/**
* AWS Comprehend API request
* @memberof module:aws
* @method queryComprehend
*/
aws.queryComprehend = function(action, obj, options, callback)
{
this.queryService("comprehend", "Comprehend_20171127", action, obj, options, callback);
}
/**
* AWS Transcribe API request
* @memberof module:aws
* @method queryTranscribe
*/
aws.queryTranscribe = function(action, obj, options, callback)
{
this.queryService("transcribe", "Transcribe", action, obj, options, callback);
}
/**
* AWS ECR API request
* @memberof module:aws
* @method queryECR
*/
aws.queryECR = function(action, obj, options, callback)
{
this.queryService("ecr", "AmazonEC2ContainerRegistry_V20150921", action, obj, options, callback);
}
/**
* AWS EventBridge API request
* @memberof module:aws
* @method queryEvents
*/
aws.queryEvents = function(action, obj, options, callback)
{
this.queryService("events", "AWSEvents", action, obj, options, callback);
}
/**
* AWS Secrets Manager API request
* @memberof module:aws
* @method querySecrets
*/
aws.querySecrets = function(action, obj, options, callback)
{
this.queryService("secretsmanager", "secretsmanager", action, obj, options, callback);
}
require(__dirname + "/aws/meta")
require(__dirname + "/aws/query")
require(__dirname + "/aws/cw")
require(__dirname + "/aws/dynamodb")
require(__dirname + "/aws/ec2")
require(__dirname + "/aws/ecs")
require(__dirname + "/aws/s3")
require(__dirname + "/aws/sns")
require(__dirname + "/aws/route53")
require(__dirname + "/aws/sqs")
require(__dirname + "/aws/ses")
require(__dirname + "/aws/other")