DEV Community

Youngho Andrew Chaa
Youngho Andrew Chaa

Posted on

Securely Send SMS with Twilio, AWS Lambda, and Terraform

In modern applications, automated notifications are essential for user engagement. Sending an SMS is a direct and effective way to reach your customers. This guide will walk you through a secure, automated, and serverless solution for sending SMS messages using Twilio, running the code on AWS Lambda, and managing the infrastructure with Terraform.

We'll stick to a key security principle: never hardcode your credentials. We'll see how to pass sensitive information like your Twilio accountSid and authToken securely from GitHub Secrets all the way to your Lambda function's environment.

The AWS Lambda Function

First, let's look at the Node.js/TypeScript code that will run on AWS Lambda. This function is responsible for the actual sending of the SMS.

The magic happens in this simple script which uses the official twilio SDK.

import twilio from 'twilio';

const accountSid = process.env.TWILIO_ACCOUNT_SID;
const authToken = process.env.TWILIO_AUTH_TOKEN;
const from = process.env.TWILIO_FROM; // Your Twilio phone number

// A crucial check to ensure the function has the credentials it needs
if (!accountSid || !authToken || !from) {
  throw new Error('Twilio configuration is missing in environment variables.');
}

const client = twilio(accountSid, authToken);

export async function sendTextMessage(recipient: string, message: string) {
  try {
    const response = await client.messages.create({
      body: message,
      from: from,
      to: recipient,
    });
    console.log('Message sent successfully:', response.sid);
    return response;

  } catch (error) {
    console.error('Failed to send message:', error);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Takeaways:

  • No Hardcoded Secrets: Notice how accountSid, authToken, and the from number are retrieved from process.env. This is the standard way to access environment variables in Node.js. It keeps your sensitive data out of your source code.
  • Error Handling: The code gracefully handles potential failures by wrapping the API call in a try...catch block.
  • Initialization: It performs a startup check to ensure the necessary environment variables are present. If not, the function will fail fast, which is better than failing silently later.

The Infrastructure: Defining the Lambda with Terraform

Now, how do we get those environment variables into our Lambda function? We define them using Infrastructure as Code (IaC) with Terraform. This allows us to version control our cloud setup.

The Terraform configuration below defines the Lambda function and, most importantly, injects our variables into its runtime environment.

module "lambda_function_send_notification" {
  source = "./lambda-function"

  function_name                = "${var.component}_${var.env}_send_notification"
  role_arn                     = aws_iam_role.iam_lambda_role.arn
  env_vars                     = local.env_vars # This is where the magic happens!
  # ... other configurations
}

locals {
  env_vars = {
    TWILIO_ACCOUNT_SID = var.TWILIO_ACCOUNT_SID # Mapping Terraform var to Lambda env var
    TWILIO_AUTH_TOKEN  = var.TWILIO_AUTH_TOKEN  # Mapping Terraform var to Lambda env var
    TWILIO_FROM        = var.TWILIO_FROM      # Add your Twilio 'from' number here
    # ... other variables
  }
}

# Variable definitions to receive values from the CI/CD pipeline
variable "TWILIO_ACCOUNT_SID" {
  type = string
}

variable "TWILIO_AUTH_TOKEN" {
  type = string
}

variable "TWILIO_FROM" {
  type = string
}
Enter fullscreen mode Exit fullscreen mode

Takeaways:

  • env_vars Block: The locals.env_vars map is the crucial link. It defines the key-value pairs that will become the environment variables inside the AWS Lambda function.
  • Variable Mapping: We map the incoming Terraform variables (e.g., var.TWILIO_ACCOUNT_SID) to the names our JavaScript code expects (e.g., TWILIO_ACCOUNT_SID). This keeps the code clean and decouples it from the naming conventions used in your infrastructure or CI/CD system.

The Automation: CI/CD with GitHub Actions

Finally, let's tie it all together. The GitHub Actions workflow automates the deployment. This is where we securely pull the actual secret values from GitHub Secrets and pass them to our Terraform plan.

name: Deploy

on:
  push:
  workflow_dispatch:

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    env:
      # These TF_VAR_ prefixes tell Terraform to populate variables
      TF_VAR_TWILIO_ACCOUNT_SID: ${{ secrets.TWILIO_ACCOUNT_SID }}
      TF_VAR_TWILIO_AUTH_TOKEN: ${{ secrets.TWILIO_AUTH_TOKEN }}
      TF_VAR_TWILIO_FROM: ${{ secrets.NAVIEN_TWILIO_FROM }} # Added the from number
      # ... other environment variables
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # ... build and test steps ...

      - name: Set up Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        working-directory: ./terraforms
        run: terraform init

      - name: Terraform Plan
        working-directory: ./terraforms
        run: terraform plan -input=false

      - name: Terraform Apply
        working-directory: ./terraforms
        if: github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve -input=false
Enter fullscreen mode Exit fullscreen mode

Takeaways:

  • GitHub Secrets: The actual credentials (${{ secrets.TWILIO_ACCOUNT_SID }}) are stored securely in your GitHub repository's settings. They are never printed in logs.
  • TF_VAR_ Prefix: The TF_VAR_ prefix is a special convention used by Terraform. When GitHub Actions sets an environment variable like TF_VAR_TWILIO_ACCOUNT_SID, Terraform automatically uses its value for the TWILIO_ACCOUNT_SID input variable in your .tf files.

The Complete Secure Flow

Here’s the full journey of your secret credentials, from storage to execution, without ever being exposed in your code:

  1. Storage: Your Twilio Account SID, Auth Token, and phone number are stored as GitHub Secrets.
  2. CI/CD Pipeline: The GitHub Actions workflow reads the secrets and exports them as environment variables prefixed with TF_VAR_.
  3. Infrastructure as Code: Terraform reads the TF_VAR_ variables and uses them to populate the variable blocks in its configuration.
  4. Lambda Environment: Terraform then passes these values into the environment variables section of the AWS Lambda function resource.
  5. Execution: When the Lambda function runs, the Node.js code accesses these variables via process.env to securely authenticate with the Twilio API.

By following this pattern, you build a system that is not only automated and scalable but also secure by design. ✨

Top comments (0)