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