Implementing a Fully Functional API Gateway in Rust: Part II — Dynamic Service Registry

Hey there, Rustaceans! 🦀

Implementing a Fully Functional API Gateway in Rust: Part II — Dynamic Service Registry

Implementing a Fully Functional API Gateway in Rust: Part 2— Dynamic Service Registry

Hey there, Rustaceans! 🦀

Remember when we embarked on that thrilling journey of crafting an API Gateway in Rust? If you missed it, don’t worry! You can catch up with the first part of our adventure right here. It laid the groundwork, diving into the core concepts and constructing the initial functionalities of our gateway.

Ready for round two? This time, we’re leveling up by introducing a dynamic service registry. No more hardcoded addresses; we’re talking about flexibility, scalability, and all the jazz that comes with dynamically discovering and routing to services. 🚀

So, tighten up those coding gloves, grab a cup of your favorite brew, and let’s jump right into part two!

Why Do We Need Dynamic Service Registration?

In microservices-heavy ecosystems, services come and go. Some are scaled up in response to high loads, while others might be momentarily down for maintenance. Manually keeping track of which service runs where becomes a Herculean task. Enter dynamic service registration: a system where services autonomously register their location, making the act of discovery and routing fluid and automated.

Breathing Life into the Dynamic Service Registry

Structuring the Service Registry

Let’s start by encapsulating services and their locations:

struct ServiceRegistry { 
    services: Arc<RwLock<HashMap<String, String>>>,  // Service Name -> Service Address (URL/URI) 
} 
 
impl ServiceRegistry { 
    fn new() -> Self { 
        ServiceRegistry { 
            services: Arc::new(RwLock::new(HashMap::new())), 
        } 
    } 
 
    fn register(&self, name: String, address: String) { 
        let mut services = self.services.write().unwrap(); 
        services.insert(name, address); 
    } 
 
    fn deregister(&self, name: &str) { 
        let mut services = self.services.write().unwrap(); 
        services.remove(name); 
    } 
 
    fn get_address(&self, name: &str) -> Option<String> { 
        let services = self.services.read().unwrap(); 
        services.get(name).cloned() 
    } 
}

Crafting Registration Endpoints

Our API Gateway needs to expose endpoints allowing services to announce or rescind their presence:

async fn register_service(req: Request<Body>, registry: Arc<ServiceRegistry>) -> Result<Response<Body>, hyper::Error> { 
    // ... parsing and registration logic ... 
 
    Ok(Response::new(Body::from("Service registered successfully"))) 
} 
 
async fn deregister_service(req: Request<Body>, registry: Arc<ServiceRegistry>) -> Result<Response<Body>, hyper::Error> { 
    // ... parsing and deregistration logic ... 
 
    Ok(Response::new(Body::from("Service deregistered successfully"))) 
}

Main Function Tweaks

The main function needs to be aware of these new endpoints:

let make_svc = make_service_fn(move |_conn| { 
    let registry = Arc::clone(&registry); 
    async move { 
        Ok::<_, hyper::Error>(service_fn(move |req| { 
            let path = req.uri().path(); 
 
            if path == "/register_service" { 
                return register_service(req, Arc::clone(&registry)); 
            } 
 
            if path == "/deregister_service" { 
                return deregister_service(req, Arc::clone(&registry)); 
            } 
 
            // ... other routes ... 
        })) 
    } 
});

Testing Our New Service Registry with a Hello Service

Introducing the HelloService

Our HelloService is a basic Rust microservice. Its primary tasks are:

  1. Register itself with the API Gateway upon startup.
  2. Respond with a friendly “Hello from the Service!” when accessed.

Crafting the HelloService

1. The Basics

For starters, let’s get our dependencies in order. In our Cargo.toml, we'll have. We’re relying on `hyper` for our HTTP server and client functionalities, and `tokio` for asynchronous operations:

[dependencies] 
hyper = "0.14" 
tokio = { version = "1", features = ["full"] }

2. Service Logic

Our service will have a simple endpoint that replies with a welcoming JSON message:

async fn handle_hello(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> { 
    Ok(Response::new(Body::from(r#"{"message": "Hello from the Service!"}"#))) 
}

3. Registration Magic

Upon startup, the HelloService reaches out to our API Gateway, announcing its presence:

let client = Client::new(); 
let req_body = format!("{},{}", SERVICE_NAME, SERVICE_ADDRESS); 
 
let request = Request::builder() 
    .method("POST") 
    .uri(API_GATEWAY_ADDRESS) 
    .body(Body::from(req_body)) 
    .unwrap(); 
 
if let Err(e) = client.request(request).await { 
    eprintln!("Failed to register the service: {}", e); 
}

4. Bringing It All Together

Combine the service logic and registration magic under a Tokio runtime:

#[tokio::main] 
async fn main() { 
    // Register with the API Gateway 
    // ... [registration code from above] ... 
 
    // Start the Hello Service 
    let make_svc = make_service_fn(|_conn| { 
        async { Ok::<_, hyper::Error>(service_fn(handle_hello)) } 
    }); 
 
    let addr = ([127, 0, 0, 1], 9090).into(); // Service runs on port 9090 
    let server = Server::bind(&addr).serve(make_svc); 
 
    println!("Hello Service running on http://{}", addr); 
 
    if let Err(e) = server.await { 
        eprintln!("server error: {}", e); 
    } 
}

Taking It for a Spin

  1. Start your API Gateway (ensure it’s listening for service registrations).
  2. Fire up the HelloService. It should automatically announce itself to the API Gateway.

Download Now!


Implementing a client for testing the API gateway

We’ll create a Rust-based client to test the API Gateway’s authentication and other features. This client will:

  1. Generate a JWT token for authentication.
  2. Make requests to the API Gateway with and without the JWT token.

Dependencies

Add the following dependencies to your Cargo.toml:

jsonwebtoken = "7.2" 
hyper = "0.14" 
hyper-tls = "0.5" 
tokio = { version = "1", features = ["full"] }

Rust-based Client Implementation:

use std::time::{Duration, SystemTime}; 
use jsonwebtoken::{encode, Header, EncodingKey}; 
use hyper::{Client, Request}; 
use hyper_tls::HttpsConnector; 
use serde::{Deserialize, Serialize}; 
 
const SECRET_KEY: &'static str = "secret_key";  // Must match the secret in the API Gateway 
 
#[derive(Debug, Serialize, Deserialize)] 
struct Claims { 
    sub: String, 
    iss: String, 
    exp: usize, 
} 
 
#[tokio::main] 
async fn main() { 
    let claims = Claims { 
        sub: "1234567890".to_string(), 
        iss: "my_issuer".to_string(), 
        exp: (SystemTime::now() + Duration::from_secs(3600)) 
            .duration_since(SystemTime::UNIX_EPOCH) 
            .unwrap() 
            .as_secs() as usize, // Expires in 1 hour 
    }; 
 
    let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(SECRET_KEY.as_ref())).unwrap(); 
    println!("Token: {}", token); 
 
    let client = { 
        let https = HttpsConnector::new(); 
        Client::builder().build::<_, hyper::Body>(https) 
    }; 
 
    let request = Request::builder() 
        .method("GET") 
        .uri("http://127.0.0.1:8080/hello_service") 
        .header("Authorization", token) 
        .body(hyper::Body::empty()) 
        .expect("Request builder failed."); 
 
    let response = client.request(request).await.expect("Request failed."); 
    println!("Response: {:?}", response.status()); 
 
    let bytes = hyper::body::to_bytes(response.into_body()).await.expect("Failed to read response."); 
    let string = String::from_utf8_lossy(&bytes); 
    println!("Response Body: {}", string); 
}

This client will:

  1. Generate a JWT token with hardcoded claims.
  2. Use the hyper library to make an HTTP request to our API Gateway's /hello_service endpoint.
  3. Attach the generated JWT token in the “Authorization” header.
  4. Print the response status and body.

When you run this client, it should successfully authenticate with the API Gateway and receive a response from the /hello_service endpoint.

To test the authentication failure, you can comment out the line that sets the “Authorization” header or modify the JWT token’s content or signature.

Wrapping up

Alright, fellow Rustaceans, that brings us to the end of the second part of our journey into crafting a robust API Gateway in Rust. I hope you found this exploration of dynamic service registry as exhilarating as the previous leg of our voyage. If you happened to miss the first part, where we laid down the foundation of our API Gateway, don’t worry! You can catch up and delve into the basics right here.

Check out our update GitHub repository at https://github.com/luishsr/rust-api-gateway to find everything we’ve implemented so far and play with it!

Having now embedded the dynamic service registry into our API Gateway, we’ve unlocked a more agile, adaptable, and scalable approach to managing microservices.

As the landscape of our services evolves — scaling, migrating, or even reincarnating from failures — our API Gateway stands ready to discover and route with grace.

Check out some interesting hands-on Rust articles:

🌟 Developing a Fully Functional API Gateway in Rust — Discover how to set up a robust and scalable gateway that stands as the frontline for your microservices.

🌟 Implementing a Network Traffic Analyzer — Ever wondered about the data packets zooming through your network? Unravel their mysteries with this deep dive into network analysis.

🌟 Building an Application Container in Rust — Join us in creating a lightweight, performant, and secure container from scratch! Docker’s got nothing on this. 😉

🌟 Crafting a Secure Server-to-Server Handshake with Rust & OpenSSL — 
If you’ve been itching to give your servers a unique secret handshake that only they understand, you’ve come to the right place. Today, we’re venturing into the world of secure server-to-server handshakes, using the powerful combo of Rust and OpenSSL.

🌟Building a Function-as-a-Service (FaaS) in Rust: If you’ve been exploring cloud computing, you’ve likely come across FaaS platforms like AWS Lambda or Google Cloud Functions. In this article, we’ll be creating our own simple FaaS platform using Rust.

🌟 Rusting Up Your Own Self-Signed Certificate Generator — Let’s walk through the process of crafting your very own self-signed certificate generator, all using the versatile Rust programming language and the rust-openssl crate.

Stay with us for the next parts, where we’ll uncover even more exciting features and delve deeper into the vast world of microservices with Rust.

Happy coding, and keep those Rust gears turning! 🦀


Read more articles about Rust in my Rust Programming Library!


Visit my Blog for more articles, news, and software engineering stuff!

Follow me on Medium, LinkedIn, and Twitter.

Leave a comment, and drop me a message!

All the best,

Luis Soares

CTO | Tech Lead | Senior Software Engineer | Cloud Solutions Architect | Rust 🦀 | Golang | Java | ML AI & Statistics | Web3 & Blockchain

Read more