File

shared/common/aws-service.ts

Description

AWS related service

Index

Properties
Methods

Constructor

constructor(debugLogsAllowed)

Constructor

@param debugLogAllower {boolean = false} - enable or disable additional logs

Parameters :
Name Optional
debugLogsAllowed No

Properties

Private Readonly debugLogsAllowed
Type : boolean
Default value : true

flag for additional debug logs

Private Readonly logger
Default value : new Logger(AwsService.name)

logger

Methods

Async allowCronRun
allowCronRun(forceCronRun)

Checks if cron run is allowed

@param forceCronRun - allow some methods to still be run on non cron running environments.

@return {Promise} - ec2 instance is lead

Parameters :
Name Optional Default value Description
forceCronRun No false
  • allow some methods to still be run on non cron running environments.
Returns : unknown
  • ec2 instance is lead
Async allowLeadRoutines
allowLeadRoutines()

Checks if lead tasks are allowed

@return {Promise} - ec2 instance is lead

Returns : Promise<boolean>
  • ec2 instance is lead
Private Async getLeadInstanceIdFromBeanstalk
getLeadInstanceIdFromBeanstalk()

Gets the current lead instance using the Elastic Beanstalk API.

But this API has a very low rate limiting, with an unknown specific limit. I (Ain) was unable to figure out what the rate limit was, but we were throttled using this way of querying.

See getLeadInstanceIdFromEc2()

Returns : Promise<string>
Private Async getLeadInstanceIdFromEc2
getLeadInstanceIdFromEc2()

Gets the lead instance from the EC2 API.

This API 100 requests per second limit for our bucket (all servers in our region)

See https://docs.aws.amazon.com/AWSEC2/latest/APIReference/throttling.html#throttling-limits See getLeadInstanceIdFromBeanstalk()

Returns : Promise<string>
Async getRunningInstanceId
getRunningInstanceId()

Gets running ec2 instance id

@return {Promise} - ec2 instance id

Returns : Promise<string>
  • ec2 instance id
Private Async isCurrentServerTheLeadInstance
isCurrentServerTheLeadInstance()

Checks if current ec2 instance is lead (can do cron/gameSave tasks)

@return {Promise} - ec2 instance is lead

Returns : Promise<boolean>
  • ec2 instance is lead
Async publishMetrics
publishMetrics(metrics: AwsCustomMetrics)

Publish metrics data

@param {AwsCustomMetrics} metrics - metrics to publish

Parameters :
Name Type Optional Description
metrics AwsCustomMetrics No
  • metrics to publish
Returns : Promise<void>
ready
ready()

ready checker

Returns : boolean
import { Logger } from '@nestjs/common';
import * as AWS from 'aws-sdk';
import { AwsCustomMetrics } from '../interfaces/aws';

/**
 *  Method to check if environment configured to run cron tasks (env.RUN_CRON !== 'no')
 *
 *  @return {boolean}
 */
export const isCronRunningEnvironment = (): boolean => {
  if (!!process.env.WEBSOCKET_NO_REDIS) return true
  return process.env.RUN_CRON !== 'no';
};

AWS.config.update({
  region: process.env.AWS_REGION,
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
});

const MATH99_APP = process.env.AWS_ENV_NAME || 'math99';

/**
 *  AWS related service
 */
export class AwsService {
  /** flag for additional debug logs */
  private readonly debugLogsAllowed: boolean = true;
  /** logger */
  private readonly logger = new Logger(AwsService.name);

  /**
   *  Constructor
   *
   *  @param debugLogAllower {boolean = false} - enable or disable additional logs
   */
  constructor(debugLogsAllowed = false) {
    this.debugLogsAllowed = debugLogsAllowed;
  }

  /**
   *  ready checker
   */
  ready(): boolean {
    const  { AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY }
      = process.env
    return !!AWS_REGION && !!AWS_ACCESS_KEY_ID && !!AWS_SECRET_ACCESS_KEY;
  }

  /**
   *  Publish metrics data
   *
   *  @param {AwsCustomMetrics} metrics - metrics to publish
   */
  async publishMetrics(metrics: AwsCustomMetrics): Promise<void> {
    if (!this.ready()) return;
    const cloudwatch = new AWS.CloudWatch();
    const now = new Date();
    const data = Object.keys(metrics)
      .filter(m => m !== 'instanceId')
      .map(m => ({
        MetricName: m,
        Value: metrics[m],
        Dimensions: [{
          Name: 'AWS_REGION',
          Value: process.env.AWS_REGION,
        }, {
          Name: 'AWS_INSTANCE_ID',
          Value: metrics.instanceId,
        }, {
          Name: 'MATH99_REGION',
          Value: process.env.MATH99_REGION,
        }, {
          Name: 'MATH99_APP',
          Value: MATH99_APP,
        }],
        StorageResolution: 60,
        Timestamp: now,
        Unit: 'Count',
      }))
    const params = {
      Namespace: 'MATH99',
      MetricData: data,
    };
    return new Promise((resolve) => {
      cloudwatch.putMetricData(params, (err, data) => {
        if (!err) {
          this.logger.log(`AWS METRICS published ${JSON.stringify(metrics)}`);
          return
        }
        this.logger.error(`AWS METRICS error ${JSON.stringify(metrics)}: ${err}`);
      });
    });
  }
  /**
   *  Gets running ec2 instance id
   *
   *  @return {Promise<string>} - ec2 instance id
   */
  async getRunningInstanceId(): Promise<string> {
    if (!this.ready()) return '';
    const meta = new AWS.MetadataService();
    return new Promise((resolve, reject) => {
      meta.request('/latest/meta-data/instance-id', (err, data) => {
        if (err) {
          this.logger.error(
            `Error requesting instance meta data: ${err.stack}`,
          );
          reject(err);
        } else {
          if (this.debugLogsAllowed) {
            this.logger.log(`Instance meta data: ${data}`);
          }
          resolve(data);
        }
      });
    });
  }

  /**
   * Gets the lead instance from the EC2 API.
   *
   * This API 100 requests per second limit for our bucket (all servers in our region)
   *
   * @see https://docs.aws.amazon.com/AWSEC2/latest/APIReference/throttling.html#throttling-limits
   * @see getLeadInstanceIdFromBeanstalk()
   * @private
   */
  private async getLeadInstanceIdFromEc2(): Promise<string> {
    const ec2 = new AWS.EC2();
    return new Promise((resolve, reject) => {
      try {
        ec2.describeInstances(
          {
            Filters: [
              {
                Name: 'tag:elasticbeanstalk:environment-name',
                Values: [process.env.AWS_ENV_NAME],
              },
              {
                Name: 'instance-state-name',
                Values: ['running']
              },
            ],
            MaxResults: 100,
          },
          (err, data) => {
            if (err) {
              this.logger.error(
                `Error describing environment resources: ${err.stack}`,
              );
              reject(err);
            } else {
              const leadId = data.Reservations[0].Instances[0].InstanceId;
              if (this.debugLogsAllowed) {
                const allIds = data.Reservations.map(
                  (reservation) => reservation.Instances[0].InstanceId,
                ).join(', ');
                this.logger.log(`All instances: ${allIds}`);
                this.logger.log(`Lead instance ID: ${leadId}`);
              }
              resolve(leadId);
            }
          },
        );
      } catch (error) {
        reject(error);
      }
    });
  }

  /**
   * Gets the current lead instance using the Elastic Beanstalk API.
   *
   * But this API has a very low rate limiting, with an unknown specific limit.
   * I (Ain) was unable to figure out what the rate limit was, but we were
   * throttled using this way of querying.
   *
   * @see getLeadInstanceIdFromEc2()
   * @deprecated
   * @private
   */
  private async getLeadInstanceIdFromBeanstalk(): Promise<string> {
    const newClient = new AWS.ElasticBeanstalk();
    try {
      return new Promise((resolve, reject) => {
        // eslint-disable-next-line
        // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ElasticBeanstalk.html#describeEnvironmentResources-property
        newClient.describeEnvironmentResources(
          {
            EnvironmentName: process.env.AWS_ENV_NAME,
          },
          (err, data) => {
            if (err) {
              this.logger.error(
                'Error describing environment resources:',
                err.stack,
              );
              reject(err);
            } else {
              resolve(data.EnvironmentResources.Instances[0].Id);
            }
          },
        );
      });
    } catch (error) {
      this.logger.log('Error getting lead instance ID:', error);
      return null;
    }
  }

  /**
   *  Checks if current ec2 instance is lead (can do cron/gameSave tasks)
   *
   *  @return {Promise<boolean>} - ec2 instance is lead
   */
  private async isCurrentServerTheLeadInstance(): Promise<boolean> {
    if (process.env.NODE_ENV === 'development') return true;
    if (!!process.env.WEBSOCKET_NO_REDIS) return true
    let runningInstanceId;
    let leadInstanceId;

    try {
      runningInstanceId = await this.getRunningInstanceId();
    } catch (e) {
      this.logger.error(`Error getting running instance ID: ${e}`);
      return null;
    }

    try {
      leadInstanceId = await this.getLeadInstanceIdFromEc2();
    } catch (e) {
      this.logger.error(`Error getting lead instance ID: ${e}`);
      return null;
    }

    if (this.debugLogsAllowed) {
      this.logger.log(
        `Is current server lead? ${runningInstanceId === leadInstanceId}`,
      );
    }
    return `${runningInstanceId}` === `${leadInstanceId}`;
  }

  /**
   *  Checks if cron run is allowed 
   *
   *  @param forceCronRun - allow some methods to still be run on non cron running environments.
   *
   *  @return {Promise<boolean>} - ec2 instance is lead
   */
  async allowCronRun(forceCronRun = false) {
    if (process.env.NODE_ENV === 'development') {
      return true;
    }

    if (!forceCronRun && isCronRunningEnvironment() === false) {
      return false;
    }

    // Check it everytime because the list of servers changes constantly,
    // eg the previous lead instance might be killed and now this server
    // is the new lead.
    return await this.isCurrentServerTheLeadInstance();
  }

  /**
   *  Checks if lead tasks are allowed
   *
   *  @return {Promise<boolean>} - ec2 instance is lead
   */
  async allowLeadRoutines(): Promise<boolean> {
    if (process.env.NODE_ENV === 'development') return true;
    if (!!process.env.WEBSOCKET_NO_REDIS) return true
    // allow if aws not configured
    if (!this.ready()) return true;
    return await this.isCurrentServerTheLeadInstance();
  }
}

results matching ""

    No results matching ""