How to deploy your Rust microservices with Minikube in 10 minutes

Aug 11, 2024 · 5 minute read

Introduction

If you ever wanted to run your local Kubernetes, build and configure the Rust web microservices and see how it all works together, this 10 minutes guide is for you.

Install minikube

First, install minikube on your machine:

brew install minikube

Install registry addon

Initialize the minikube images registry:

minikube addons enable registry

docker run --rm -it --network=host alpine ash -c "apk add socat && socat TCP-LISTEN:5000,reuseaddr,fork TCP:$(minikube ip):5000”

Start minikube

Start minikube and check its status:

minikube start

minikube status

Next, make yourself aware of Kubernetes contexts on your machine, maybe you are already working within your company Kubernetes context:

kubectl config get-contexts

Make sure you use the newly created minikube context:

kubectl config use-context minikube

Build and push your Rust application images

Now you are ready to build and deploy your Rust web services.

Service A

Let’s create our first service, run the following commands:

cargo new service_a
cd service_a

Add the actix_web and reqwest dependencies:

cargo add actix_web reqwest

Your final Cargo.toml should look like this:

[package]
name = "service_a"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.8.0"
reqwest = "0.12.5"

In your src/main.rs put the following code:

use actix_web::{get, App, HttpServer, Responder};
use reqwest::Client;

#[get("/")]
async fn index() -> impl Responder {
    "Hello from Service A"
}

#[get("/call-service-b")]
async fn call_service_b() -> impl Responder {
    let client = Client::new();
    match client.get("http://service-b").send().await {
        Ok(response) => response.text().await.unwrap_or_else(|_| "Failed to get response".to_string()),
        Err(_) => "Failed to call service-b".to_string(),
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
            App::new()
                .service(index)
                .service(call_service_b)
        })
        .bind("0.0.0.0:8080")?
        .run()
        .await
}

Create Dockerfile in the root folder and add the following content to it:

# Builder stage
FROM rust:1.72 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /app/target/release/service_a service_a
ENTRYPOINT ["./service_a"]

Service B

Now create your second service, run the following commands:

cargo new service_b
cd service_b

Add the actix_web dependency:

cargo add actix_web

Your final Cargo.toml should look like this:

[package]
name = "service_b"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4.8.0"

In your src/main.rs put the following code:

use actix_web::{get, App, HttpServer, Responder};

#[get("/")]
async fn index() -> impl Responder {
    "Hello from Service B"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(index))
        .bind("0.0.0.0:8080")?
        .run()
        .await
}

Create Dockerfile in the root folder and add the following content to it (the same as for our service A):

# Builder stage
FROM rust:1.72 as builder
WORKDIR /app
COPY . .
RUN cargo build --release

# Runtime stage
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /app/target/release/service_b service_b
ENTRYPOINT ["./service_b"]

Build, tag and push images

Now we are ready to build, tag and push images to the minikube images repository:

cd service_a
docker build -t localhost:5000/service-a:latest -f Dockerfile .
docker push localhost:5000/service-a:latest

cd ../service_b
docker build -t localhost:5000/service-b:latest -f Dockerfile .
docker push localhost:5000/service-b:latest

Apply Kubernetes configurations

Now, we are ready to finally deploy the images on minikube cluster.

Create a separate folder k8s.

Inside create a file service-a-deployment.yaml with the content:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-a
spec:
  replicas: 3
  selector:
    matchLabels:
      app: service-a
  template:
    metadata:
      labels:
        app: service-a
    spec:
      containers:
        - name: service-a
          image: localhost:5000/service-a:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080

Next, create a file service-a-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: service-a
spec:
  selector:
    app: service-a
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

Now, the same for the service B. Inside the same folder k8s create a file service-b-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: service-b
spec:
  replicas: 3
  selector:
    matchLabels:
      app: service-b
  template:
    metadata:
      labels:
        app: service-b
    spec:
      containers:
        - name: service-b
          image: localhost:5000/service-b:latest
          imagePullPolicy: Always
          ports:
            - containerPort: 8080

And a file service-b-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: service-b
spec:
  selector:
    app: service-b
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080

Now, apply all the deployment and service Kubernetes configurations:

kubectl apply -f ./service-a-deployment.yaml
kubectl apply -f ./service-a-service.yaml

kubectl apply -f ./service-b-deployment.yaml
kubectl apply -f ./service-b-service.yaml

Check the status of your pods:

kubectl get pods

All of them should be running without any errors. You should see the status “Running” for each of them. Example output you might see:

NAME                         READY   STATUS    RESTARTS   AGE
service-a-85d9755db-h5c8q    1/1     Running   0          42h
service-a-85d9755db-lvf9k    1/1     Running   0          42h
service-a-85d9755db-t5w2m    1/1     Running   0          42h
service-b-667f9dc5d7-5t6tw   1/1     Running   0          42h
service-b-667f9dc5d7-fl799   1/1     Running   0          42h
service-b-667f9dc5d7-pglcd   1/1     Running   0          42h

Making changes

When you have any new code changes, rebuild the image, publish it and restart the Kubernetes. For example, if you made any changes to the Service A:

docker build -t localhost:5000/service-a:latest -f Dockerfile .
docker push localhost:5000/service-a:latest 

kubectl rollout restart deployment/service-a

Now you can see the list of services running in minikube:

minikube service list

And finally open our service A via this command:

minikube service service-a

You will be redirected to a browser page. You should see a response from service A. And following the URL /call-service-b you will see that the Service A can successfully access the Service B.

You can also see a beautiful dashboard by running this command:

minikube dashboard

You should see a nice dashboard similar to the one below Minkube dashboard

Final

Following this short guide you are now able to deploy your Rust microservices locally. In the next article we will cover a production ready solution. We will deploy a Kubernetes cluster on a Ubuntu VPS, using the kubeadm tool. Also we will add TLS and automated certificates renewal to secure our services. Also, we will add more functionality to our Rust microservices to cover more use cases. Stay tuned and see you next time.