Re-engineering Alexa Skills in Rust: Because AWS Lambda is Overkill

More Adventures in AI Coding

I created a sprawling set of legacy PHP scripts to build a couple Alexa skills for private use. Every time I asked Alexa for the local tide tracking info or my home solar storage status, the request would rattle through Apache, fire up mod_php, parse some local JSON files or hit an external API, and spit back a response. It worked fine for a long time. It was customizable, not locked down, and certainly not idiot-proof. That’s great if you want the ability to set up your home automation network the way you want. That’s me.

It worked as expected.

So, next obvious question—what can I do to modernize this setup? Can I make it faster? Can I drop the old script footprints completely and rewrite the backend in Rust? Can I integrate it cleanly into my existing self-hosted home server without succumbing to the modern trap of moving everything to an expensive, long-running background web server daemon or cloud-vendor locked AWS Lambda functions?

Let’s look at how to get that running.

The CGI Approach: Stripping Out the Overhead

The standard modern advice for building an Alexa skill in Rust is to reach for a heavy, asynchronous web server framework like Actix, Axum, or Rocket. But running a persistent background daemon that constantly eats system memory just to wait around for an occasional voice command feels sloppy.

Instead, I decided to go old-school: the Common Gateway Interface (CGI).

When an Alexa request hits my server, Apache spins up a tiny, short-lived, highly optimized Rust binary. The binary reads the request data from standard input (stdin), runs its calculations, prints an Alexa-compliant JSON response straight to standard output (stdout), and instantly exits.

The baseline efficiency of this setup is wild:

  • Idle System Memory Footprint: Exactly 0 megabytes.
  • Total Executable Size: Under 300 Kilobytes (fully stripped).
  • Cold Start Latency: Under 2 milliseconds.

To keep things modular and reusable, I built a lightweight, zero-dependency core framework library crate called alexa_cgi_core. It encapsulates the raw environment parsing, handles state validation, and exposes a clean interface. Here is how you can use it to drive a custom skill. It’s available here https://github.com/schettj/alexa_cgi_core


🛠️ Step 1: Writing the Skill Handler

First, configure your project’s manifest file to pull in the core framework dependency. We also apply aggressive release profiles to optimize the output binary size:

Cargo.toml

[package]
name = "alexa_home_assistant"
version = "0.1.0"
edition = "2021"
[dependencies]
# Reference the public library crate core framework directly alexa_cgi_core = { git = "git@github.com:schettj/alexa_cgi_core.git", tag = "v0.1.0" }
[profile.release]
opt-level = "z" # Optimize strictly for minimal binary size footprint lto = true # Enable Link-Time Optimization codegen-units = 1 # Maximize compiler optimization passes 
panic = "abort" 
# Strip out diagnostic stack unwinding structures

Next, create your skill handler. By implementing the generic AlexaSkill trait, we completely decouple our custom intent parsing from the underlying web protocols. You can return your explicit Amazon Skill ID here to automatically drop rogue, unauthorized internet scraping traffic before your data loops execute:

src/alexa.rs

use alexa_cgi_core::AlexaSkill;

pub struct AssistantSkillHandler;

impl AlexaSkill for AssistantSkillHandler {
    fn skill_id(&self) -> Option<&str> {
        Some("amzn1.echo-api.skill.your-unique-skill-id-token")
    }

    fn handle_launch(&self) -> String {
        String::from("Home information system online. You can ask for system status.")
    }

    fn handle_intent(&self, intent_name: &str) -> String {
        match intent_name {
            "StatusIntent" => String::from("All systems are completely balanced and standing by."),
            _ => String::from("I didn't quite catch that. Please try again.")
        }
    }

    fn handle_fallback(&self) -> String {
        String::from("System fallback matched. Try rephrasing your question.")
    }
}

Now, your application’s main entry point collapses into a single clean initialization sequence:

src/main.rs

mod alexa;
use alexa::AssistantSkillHandler;

fn main() {
    let skill = AssistantSkillHandler;

    // Pass control straight to the core library CGI execution engine
    alexa_cgi_core::run_cgi_skill(skill);
}

🌐 Step 2: Compiling and Server Deployment

Compile the binary using your release flags and copy the executable asset straight into your local web server’s CGI directory:

cargo build --release
sudo cp target/release/alexa_home_assistant /usr/lib/cgi-bin/service1.cgi
sudo chmod +x /usr/lib/cgi-bin/service1.cgi

🔒 Step 3: Handling Cryptographic Signatures Generically

For an Alexa skill to pass Amazon’s strict production security checks, it must validate incoming cryptographic request signatures. To keep the Rust binaries fast and unburdened by heavy cryptographic crates, we can offload this validation entirely to an Apache proxy layer using a generic Python WSGI script.

Install the required validation modules on your host machine:

sudo apt-get install libapache2-mod-wsgi-py3 python3-pip
sudo pip3 install ask-sdk-webservice-support requests
sudo a2enmod wsgi

Next, place this wrapper script in your CGI directory. It acts as a generic gatekeeper, validating the Amazon certificate chain signatures before streaming the raw payload down to our target compiled Rust binary via environment keys:

/usr/lib/cgi-bin/alexa_verify.py

import os
import subprocess
from ask_sdk_webservice_support.verifier import RequestVerifier, VerificationException

def application(environ, start_response):
    try:
        # Pull the target executable path injected dynamically by Apache
        target_binary = environ.get('TARGET_CGI_BIN', '')
        if not target_binary or not os.path.exists(target_binary):
            start_response('500 Internal Server Error', [('Content-Type', 'text/plain')])
            return [f"Configuration Error: Target path missing.".encode('utf-8')]

        # Extract mandatory validation headers
        signature_url = environ.get('HTTP_SIGNATURECERTCHAINURL', '')
        signature = environ.get('HTTP_SIGNATURE', '')

        content_length = int(environ.get('CONTENT_LENGTH', 0))
        request_body = environ['wsgi.input'].read(content_length)

        # Enforce strict cryptographic validation check via official Amazon SDK
        verifier = RequestVerifier()
        verifier.verify(request_body.decode('utf-8'), signature, signature_url)

        # Signature is valid! Forward body payload stream to our target generic binary
        proc = subprocess.Popen(
            [target_binary],
            stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            env=os.environ.copy()
        )
        stdout_data, _ = proc.communicate(input=request_body)

        start_response('200 OK', [('Content-Type', 'application/json;charset=UTF-8')])
        return [stdout_data]

    except VerificationException as e:
        start_response('400 Bad Request', [('Content-Type', 'text/plain')])
        return [b"Cryptographic validation failure."]
    except Exception as e:
        start_response('500 Internal Server Error', [('Content-Type', 'text/plain')])
        return [str(e).encode('utf-8')]

🎛️ Step 4: Configuring Apache SSL Virtual Hosts

Amazon mandates secure HTTPS communication. Open your site’s secure configuration file (e.g., /etc/apache2/sites-enabled/000-default-le-ssl.conf) inside your port 443 VirtualHost block.

By leveraging the generic Python script with SetEnv, you can expose multiple distinct skills on your server through clean, isolated endpoints without changing a single line of your framework code:

<VirtualHost *:443>
    ServerName yourdomain.com

    # -------------------------------------------------------------
    # Skill A Mapping (Service Gateway 1)
    # -------------------------------------------------------------
    WSGIScriptAlias /skill1 /usr/lib/cgi-bin/alexa_verify.py
    <Location /skill1>
        SetEnv TARGET_CGI_BIN /usr/lib/cgi-bin/service1.cgi
    </Location>

    # -------------------------------------------------------------
    # Skill B Mapping (Service Gateway 2)
    # -------------------------------------------------------------
    WSGIScriptAlias /skill2 /usr/lib/cgi-bin/alexa_verify.py
    <Location /skill2>
        SetEnv TARGET_CGI_BIN /usr/lib/cgi-bin/service2.cgi
    </Location>

    <Directory "/usr/lib/cgi-bin">
        Options +ExecCGI
        Require all granted
    </Directory>
</VirtualHost>

Test your configuration syntax layout and restart Apache to bring your new secure pipeline online:

sudo apache2ctl configtest
sudo systemctl restart apache2

Wrapping Up

By stepping away from long-running, resource-heavy web daemons and leaning into the simplicity of Rust-backed CGI pipelines, you get an elite, self-hosted voice automation backend. It costs nothing to run, consumes zero idle resources, and responds with the sub-millisecond execution speeds that only a bare-metal compiled language can provide.

The framework source code is fully open-source and ready to deploy. Give it a spin, wire it up to your server, and see how much faster your smart home feels.


Comments

Leave a Reply