Category: Cloud Infrastructure

  • How to add alert notifications in your app, the easy way.

    How to add alert notifications in your app, the easy way.

    It’s such a beautifully, useful and simple thing that I had to write about it…

    Sooner or later your app needs to tell you something: a CRON job failed, some queue is backing up, a quota is almost gone, a big customer just signed up. You don’t need a paid service, an SDK, or the joy of debugging email deliverability for this. A Slack Incoming Webhook is a secret URL you POST JSON to, and that’s the whole trick.

    Step 1 – Create the webhook

    In Slack: create an app (api.slack.com/apps > Create New App > From scratch), turn on Incoming Webhooks, then Add New Webhook to Workspace and pick a channel. You get back a URL like:

    https://hooks.slack.com/services/T00000000/B00000000/xxxxxxxxxxxxxxxxxxxxxxxx

    That URL is the credential. You don’t need anything else, no secrets, no IDs, nothing – those are for other Slack stuff. For “post a message to this channel”, the URL is enough.

    Step 2 – Store it as a secret

    Drop it in your environment, never in the repo:

    # .env  (git-ignored)
    SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/xxxx"

    Step 3 – A tiny reusable helper

    A few lines of code, no deps, and – importantly – best-effort: meaning a notification that fails will not take your app down with it.

    // notify.ts
    
    type NotifyOpts = { 
      title?: string; 
      level?: "info" | "warn" | "error" 
    };
    
    const EMOJI = { 
      info: "ℹ️", 
      warn: "⚠️", 
      error: "🚨" 
    } as const;
    
    export async function notify(
      text: string, 
      opts: NotifyOpts = {}
    ): Promise<void> {
      const url = process.env.SLACK_WEBHOOK_URL;
      if (!url) return; // no-op, don't blow up
    
      const emoji = EMOJI[opts.level ?? "info"];
      const heading = opts.title 
        ? `${emoji} *${opts.title}*\n`
        : "";
    
      try {
        await fetch(url, {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ text: heading + text }),
        });
      } catch (err) {
        console.error("notify failed (non-fatal):", err);
      }
    }

    Use it anywhere

    await notify("Mr. Dude, stuff happened! ✅");
    
    await notify(
      `DB usage is at ${percent}% of the plan cap.`, {
        title: "Quota warning",
        level: "warn",
      });

    That’s a working alert system. Really.

    Want it to look nicer?

    Slack is really good at this stuff; the messages can carry Block Kit blocks for headings and compact key/value grids. Keep the top-level text too, it’s the fallback used in push notifications:

    body: JSON.stringify({
      text: "Quota warning", // fallback
      blocks: [
        { 
          type: "section", 
          text: { 
            type: "mrkdwn", 
            text: "🚨 *Quota warning*" 
          } 
        },
        { type: "section", fields: [
          { type: "mrkdwn", text: "*Used:*\n84%" },
          { type: "mrkdwn", text: "*Plan:*\nScaler" },
        ]},
      ],
    })

    Two rules, and you’re done

    1. Best-effort, always. Wrap the send in try/catch and do a no-op when the URL is missing. Your alerting is the least important thing in any request; it must never be the thing that breaks it.
    2. The URL is a secret. Anyone who has it can post to your channel. Keep it out of git, and if it leaks, regenerate it in Slack (Incoming Webhooks > remove/add); no code changes, just swap the environment variable.

    BTW, same pattern works for Discord and Telegram if Slack isn’t your thing; a different URL and a slightly different JSON body, basically same code; or use Python if you’re a guru :-).

    I’m honestly wondering if there’s an easier way. I’d probably use it.

    That’s it.
    Enjoy!


    Slack logo rendered with Krbn.

  • A CloudFormation recipe for scheduling Lambda functions with 1-minute frequency


    Among the plethora of tools which Amazon has given us there’s AWS CloudFormation. IMO, this proves that AWS is (still) in a class of it’s own. It’s the embodiment of the “infrastructure as code” concept. And, it’s battle-tested, it works! So well, in fact, that they ElasticBeanstalk right on-top of it.

    Automation not supported

    However, once you gain some experience with this beast, you start realizing that it also has faults. In fact, they tend to pile up; that’s one reason we now have some promissing tools like Terraform to play with. One such issue is the fact that CloudFormation doesn’t support the creation of scheduled (recurring) Lambda functions. And that’s a big problem, because you’re forced to do it by hand, by creating a scheduled event source using the AWS CLI, APIs or the Web Console.

    Striving for 1-minute rate

    Moreover, even if you do schedule a Lambda function by hand, the fastest rate at which you can invoke it is 5 minutes, no less.

    Recently, I needed an automatically created Lambda function to be periodically invoked at 1-minute intervals. No more, no less. And, eventually, I found a way.

    Strategy

    The trick is to use a custom metric called Tick (published by our lambda) which can be either 0(zero) or 1(one). We set two CloudWatch alarms which trigger on each state, respectively: TickState0 triggers when Tick == 0 for 1+ minutes and TickState1 triggers when Tick == 1 for 1+ minutes.

    {
      "TickState0": {
        "Type": "AWS::CloudWatch::Alarm",
        "Properties": {
          "AlarmName": "TickState0",
          "Namespace": "Tick",
          "MetricName": "Tick",
          "Statistic": "Average",
          "EvaluationPeriods": 1,
          "Period": 60,
          "Threshold": 0,
          "ComparisonOperator": "LessThanOrEqualToThreshold",
          "AlarmActions": [{ "Ref": "TickTopic" }]
        }
      },
    
      "TickState1": {
        "Type": "AWS::CloudWatch::Alarm",
        "Properties": {
          "AlarmName": "TickState1",
          "Namespace": "Tick",
          "MetricName": "Tick",
          "Statistic": "Average",
          "EvaluationPeriods": 1,
          "Period": 60,
          "Threshold": 1,
          "ComparisonOperator": "GreaterThanOrEqualToThreshold",
          "AlarmActions": [{ "Ref": "TickTopic" }]
        }
      }
    }

    Each Alarm triggers the Lambda (via an SNS topic) and the Lambda toggles the Tick metric (which will trigger the opposite alarm after ~1 minute).

    # Get message body(stack update or alarm).
    message = json.loads(event['Records'][0]['Sns']['message'])
    
    # Detect source event.
    source = message.get('RequestType', 'Alarm')
    
    # Confirm stack deletion.
    if source == 'Delete':
      return send(message, context, SUCCESS)
    
    # Set / toggle state.
    if source == 'Alarm':
      state = int(not float(message['Trigger']['threshold']))
    else :
      state = 0
    
    # Set metric to state.
    cw = boto3.client('cloudwatch')
    cw.put_metric_data(
      Namespace = 'Tick',
      MetricData = [{
        'MetricName': 'Tick',
        'Value': state
      }]
    )

    What’s left is to use a CF custom resource to ensure the Lambda is called on stack operations (create/update/delete) so the whole setup is self-managed.

    {
      "StartTick": {
        "Type": "Custom::StartTick",
        "DependsOn": ["TickState0", "TickState1"],
        "Properties": {
          "ServiceToken": { "Ref": "TickTopic" }
        }
      }
    }

    Test Drive!

    • Clone the code (see below) and use it to launch a CF stack. You’ll have to upload the lambda function (the tick.zip file) on S3 somewhere (and update it’s URI in the CF JSON file).
    • After it started, you should observe in the CloudWatch -> Logs section that a log message is published every minute by the Lambda function (see screenshot below).

    Some considerations

    • Please note that the actual Lambda has additional code and is prepared to be invoked by either the CloudWatch Alarms or the custom resource events (i.e. stack updates).
    • Expecting a shorter trigger period using this method is not realistic, since AWS CloudWatch will always aggregate metrics inside a 1-minute time period.
    • In practice, however, you will observe that this actually tends to be triggered faster than on 1-minute intervals; this is influenced by the different check-times of each CloudWatch alarm (check times differ).

    See complete code on GitHub

    That’s it, enjoy! 🙂