From 27183d50785b72e474b39cc6558dfdeedcb30185 Mon Sep 17 00:00:00 2001
From: Adar Nimrod <nimrod@shore.co.il>
Date: Sat, 18 Dec 2021 15:33:14 +0200
Subject: [PATCH] Send SMS messages with Twilio.

AWS' SMS delivery has been unreliable to say the least. Replace it with
Twilio but that's more complex:

- A new Lambda function to send SNS messages as SMS messages with
  Twilio.
- Add the function as a subscription to the SNS topic.
---
 requirements.txt  |   1 +
 sms-notify.tf     | 172 ++++++++++++++++++++++++++++++++++++++++++++++
 sns.tf            |   4 ++
 src/sms_notify.py |  26 +++++++
 4 files changed, 203 insertions(+)
 create mode 100644 sms-notify.tf
 create mode 100644 src/sms_notify.py

diff --git a/requirements.txt b/requirements.txt
index 5c7639c..35dd243 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,3 @@
 dnspython
 requests
+twilio
diff --git a/sms-notify.tf b/sms-notify.tf
new file mode 100644
index 0000000..62a167b
--- /dev/null
+++ b/sms-notify.tf
@@ -0,0 +1,172 @@
+variable "twilio_account_sid" {
+  description = "Twilio account SID."
+}
+
+variable "twilio_api_key" {
+  description = "Twilio API key."
+}
+
+variable "twilio_api_secret" {
+  description = "Twilio API secret."
+}
+
+# It would have been nicer to buy the phone number with Terraform and the
+# Twilio provider. unfortunately the sign up for the provider is closed right
+# now. So instead the friendly name, that's something.
+variable "twilio_from_number" {
+  default     = "AmILive"
+  description = "Twilio from phone number."
+}
+
+resource "aws_lambda_function" "sms_notify" {
+  runtime           = var.runtime
+  function_name     = "${local.function_name_prefix}-sms-notify"
+  role              = local.lambda_role_arn
+  source_code_hash  = filebase64sha256("payload.zip")
+  s3_bucket         = local.payloads_bucket_name
+  s3_key            = local.payload_object_name
+  s3_object_version = local.payload_object_version
+  package_type      = "Zip"
+  handler           = "sms_notify.handler"
+  description       = "Send SMS message notification using Twilio."
+  memory_size       = var.memory_size
+  tags              = local.common_tags
+  timeout           = var.timeout
+
+  environment {
+    variables = {
+      ENV                = local.env
+      MODULE             = local.module
+      TOPIC_ARN          = local.topic_arn
+      VERSION            = local.payload_object_version
+      TWILIO_ACCOUNT_SID = var.twilio_account_sid
+      TWILIO_API_KEY     = var.twilio_api_key
+      TWILIO_API_SECRET  = var.twilio_api_secret
+      TWILIO_FROM_NUMBER = var.twilio_from_number
+      TWILIO_TO_NUMBER   = local.my_phone_number
+    }
+  }
+
+  # Create the log group with retention before the function is created.
+  # Otherwise it's created without retention and need to be imported.
+  depends_on = [
+    aws_cloudwatch_log_group.sms_notify,
+  ]
+}
+
+locals {
+  sms_notify_function_arn     = aws_lambda_function.sms_notify.arn
+  sms_notify_function_name    = aws_lambda_function.sms_notify.function_name
+  sms_notify_function_version = aws_lambda_function.sms_notify.version
+}
+
+output "sms_notify_function_arn" {
+  description = "ARN of the SMS notification Lambda function."
+  value       = local.sms_notify_function_arn
+}
+
+output "sms_notify_function_name" {
+  description = "Name of the SMS notification Lambda function."
+  value       = local.sms_notify_function_name
+}
+
+output "sms_notify_function_version" {
+  description = "Version of the SMS notification Lambda function."
+  value       = local.sms_notify_function_version
+}
+
+resource "aws_lambda_alias" "sms_notify" {
+  name             = "${local.function_name_prefix}_${local.sms_notify_function_name}"
+  function_name    = local.sms_notify_function_arn
+  function_version = local.sms_notify_function_version
+}
+
+locals {
+  sms_notify_function_alias_arn  = aws_lambda_alias.sms_notify.arn
+  sms_notify_function_alias_name = aws_lambda_alias.sms_notify.name
+}
+
+output "sms_notify_function_alias_arn" {
+  description = "ARN of the SMS notification Lambda function alias."
+  value       = local.sms_notify_function_alias_arn
+}
+
+output "sms_notify_function_alias_name" {
+  description = "Name of the SMS notification Lambda function alias."
+  value       = local.sms_notify_function_alias_name
+}
+
+resource "aws_lambda_permission" "sms_notify" {
+  statement_id  = "AllowExecutionFromSNS"
+  action        = "lambda:InvokeFunction"
+  principal     = "sns.amazonaws.com"
+  source_arn    = local.topic_arn
+  function_name = local.sms_notify_function_name
+}
+
+resource "aws_sns_topic_subscription" "sms_notify" {
+  endpoint  = local.sms_notify_function_arn
+  protocol  = "lambda"
+  topic_arn = local.topic_arn
+  depends_on = [
+    aws_lambda_permission.sms_notify,
+  ]
+}
+resource "aws_cloudwatch_log_group" "sms_notify" {
+  name              = "/aws/lambda/${local.function_name_prefix}-sms-notify"
+  retention_in_days = var.log_retention
+  tags              = local.common_tags
+}
+
+locals {
+  sms_notify_log_group_arn  = aws_cloudwatch_log_group.sms_notify.arn
+  sms_notify_log_group_name = aws_cloudwatch_log_group.sms_notify.name
+}
+
+output "sms_notify_log_group_arn" {
+  description = "ARN of the CloudWatch log groups for the SMS notify Lambda function invocations."
+  value       = local.sms_notify_log_group_arn
+}
+
+output "sms_notify_log_group_name" {
+  description = "Name of the CloudWatch log groups for the SMS notify Lambda function invocations."
+  value       = local.sms_notify_log_group_name
+}
+
+data "aws_iam_policy_document" "sms_notify" {
+  statement {
+    effect = "Allow"
+
+    actions = [
+      "logs:CreateLogStream",
+      "logs:PutLogEvents",
+    ]
+
+    resources = [local.sms_notify_log_group_arn, ]
+  }
+}
+
+locals {
+  sms_notify_log_policy_doc = data.aws_iam_policy_document.sms_notify.json
+}
+
+resource "aws_iam_policy" "sms_notify_log" {
+  name   = "${local.module}-${local.env}-sms-notify-log"
+  policy = local.sms_notify_log_policy_doc
+  tags   = local.common_tags
+}
+
+locals {
+  sms_notify_log_policy_arn  = aws_iam_policy.log.arn
+  sms_notify_log_policy_name = aws_iam_policy.log.name
+}
+
+output "sms_notify_log_policy_arn" {
+  value       = local.sms_notify_log_policy_arn
+  description = "CloudWatch log IAM policy for SMS notifications ARN."
+}
+
+output "sms_notify_log_policy_name" {
+  value       = local.sms_notify_log_policy_name
+  description = "CloudWatch log IAM policy for SMS notifications name."
+}
diff --git a/sns.tf b/sns.tf
index 725e2e5..6ddf0d9 100644
--- a/sns.tf
+++ b/sns.tf
@@ -25,6 +25,10 @@ variable "subscriptions" {
   description = "A list of subscriptions to the SNS topic."
 }
 
+locals {
+  my_phone_number = var.subscriptions[0][0]
+}
+
 output "subscriptions" {
   description = "A list of subscriptions to the SNS topic."
   value       = var.subscriptions
diff --git a/src/sms_notify.py b/src/sms_notify.py
new file mode 100644
index 0000000..4564f2d
--- /dev/null
+++ b/src/sms_notify.py
@@ -0,0 +1,26 @@
+import os
+import twilio.rest  # pylint: disable=import-error
+
+
+TWILIO_ACCOUNT_SID = os.environ["TWILIO_ACCOUNT_SID"]
+TWILIO_API_KEY = os.environ["TWILIO_API_KEY"]
+TWILIO_API_SECRET = os.environ["TWILIO_API_SECRET"]
+TWILIO_FROM_NUMBER = os.environ["TWILIO_FROM_NUMBER"]
+TWILIO_TO_NUMBER = os.environ["TWILIO_TO_NUMBER"]
+
+
+# pylint: disable=unused-argument
+def handler(event, context):
+    message = event["Records"][0]["Sns"]["Message"]
+    client = twilio.rest.Client(
+        TWILIO_API_KEY, TWILIO_API_SECRET, TWILIO_ACCOUNT_SID
+    )
+    client.messages.create(
+        body=message,
+        from_=TWILIO_FROM_NUMBER,
+        to=TWILIO_TO_NUMBER,
+    )
+
+
+if __name__ == "__main__":
+    handler({"Records": [{"Sns": {"Message": "foo"}}]}, "context")
-- 
GitLab