DEV Community

Karl Weinmeister for Google Cloud

Posted on • Originally published at Medium on

Getting started with Rust on Google Cloud

This post will guide you through deploying a simple “Hello, World!” application on Cloud Run. You’ll then extend the application by showing how to integrate with Google Cloud services with experimental Rust client libraries.

I’ll cover the necessary code, Dockerfile configuration, and deployment steps. I’ll also recommend a robust and scalable stack for building web services, especially when combined with Google Cloud’s serverless platform, Cloud Run.

Why Rust and Axum?

Rust has gained significant traction in backend development, earning the title of most-admired language in the StackOverflow 2024 Developer Survey. This popularity stems from its core strengths: performance, memory safety, and reliability. Rust’s low-level control and zero-cost abstractions enable highly performant applications. Its ownership system prevents common programming errors like data races and null pointer dereferences. In addition, Rust’s strong type system and compile-time checks catch errors early in the development process, leading to more reliable software.

The Rust web framework ecosystem is vibrant and evolving. Popular choices include Axum, Rocket, and Actix. In this post, I’ll showcase Axum, but you can apply what you’ve learned here to other Rust web frameworks. Axum’s API is clear and composable, making it easy to build web services. Its modular architecture allows developers to select only the necessary components. Axum is built on Tokio, a popular asynchronous runtime for Rust, which allows it to handle concurrency and I/O operations efficiently.

Hello World Application

Let’s start by exploring a basic “Hello, World!” example from the official Axum repository. In each section of this blog post, you will enhance the example to leverage Google Cloud capabilities. You can access the final code sample in the cloud-rust-example repository.

First, the Cargo.toml manifest file defines the project’s metadata and dependencies:

[package]
name = "example-hello-world"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
axum = { path = "../../axum" }
tokio = { version = "1.0", features = ["full"] }
Enter fullscreen mode Exit fullscreen mode

Within this file, you see:

  • [package]: Contains basic project information like name, version, and the Rust edition. publish = false prevents accidental publication.
  • [dependencies]: Lists the project’s dependencies — axum for the web framework and tokio for asynchronous capabilities.

Now, let’s examine the core application code, src/main.rs:

use axum::{response::Html, routing::get, Router};

#[tokio::main]
async fn main() {
    // build our application with a route
    let app = Router::new().route("/", get(handler));

    // run it
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
        .await
        .unwrap();
    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}

async fn handler() -> Html<&'static str> {
    Html("<h1>Hello, World!</h1>")
}
Enter fullscreen mode Exit fullscreen mode

This code sets up a minimal web server using Axum and Tokio. The #[tokio::main] macro enables asynchronous execution. The main function creates a Router to handle requests, defines a single route / that responds with “Hello, World!”, binds the server to 127.0.0.1:3000, and starts the server. The handler function generates the HTML response for the root route.

Enhancements for Cloud Run

The basic example above works well for local development, but let’s make some improvements for deploying to Cloud Run. The official example notably does not include a Dockerfile, which is required for Cloud Run.

1. Standalone Deployment: To make the example standalone and deployable, modify the Cargo.toml file. Change the axum dependency from axum = { path = “../../axum” } to axum = “0.8” to use the published version of Axum from crates.io instead of the local path.

2. Dynamic Port Configuration:

Cloud Run dynamically assigns a port to your application, which is provided through the PORT environment variable. The original example hardcodes the port to 3000. To make our application Cloud Run-compatible, modify the main function to read the PORT environment variable and use it if available, falling back to a default port such as 8080 if the variable is not set.

The address should also be changed to 0.0.0.0 to listen on all network interfaces, which is generally preferred for containerized applications.

Here’s the modified main function:

#[tokio::main]
async fn main() {
    // Get the port from the environment, defaulting to 8080
    let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());
    let addr = format!("0.0.0.0:{}", port);

    // build our application with a route
    let app = Router::new().route("/", get(handler));

    // run it
    let listener = tokio::net::TcpListener::bind(addr)
        .await
        .unwrap();
    println!("listening on {}", listener.local_addr().unwrap());
    axum::serve(listener, app).await.unwrap();
}
Enter fullscreen mode Exit fullscreen mode

3. Dockerfile:

To deploy to Cloud Run, you’ll need a Dockerfile. Here’s a simple one that works well for this example:

FROM rust:1.85.1
WORKDIR /app
COPY . .
RUN cargo build --release
EXPOSE 8080
CMD ["./target/release/example-hello-world"]
Enter fullscreen mode Exit fullscreen mode

This Dockerfile uses the official Rust image as a base, copies the project files, builds the application in release mode, exposes port 8080 (the default port), and sets the command to run the compiled executable. You can upgrade to the latest Rust image if you’d like.

4. .gcloudignore file:

You can also add a .gcloudignore file to the project root to exclude unnecessary files (like the target directory containing build artifacts) from the deployment:

.git/
.gitignore
target/
Enter fullscreen mode Exit fullscreen mode

Deploying to Cloud Run

Before deploying, ensure you have the Google Cloud SDK installed and configured, and you have enabled the Cloud Run API in your Google Cloud project. You’ll also need to be in the root directory of your Axum project (where the Cargo.toml file is located).

Before attempting your deployment, you can check the local package and deployment for errors:

cargo check
Enter fullscreen mode Exit fullscreen mode

To deploy directly to Cloud Run from source, use the following command:

gcloud run deploy cloud-rust-example \
    --source . \
    --region us-central1 \
    --allow-unauthenticated
Enter fullscreen mode Exit fullscreen mode

Here’s what each part of the command means:

  • gcloud run deploy cloud-rust-example: This is the base command to deploy a service to Cloud Run. cloud-rust-example is the name we’re giving to our service. You can choose a different name.
  • —-source .: This flag tells Cloud Run where to find the source code for your application. The . indicates the current directory. Cloud Run will use the Dockerfile in this directory to build a container image.
  • —-region us-central1: This specifies the Google Cloud region where your service will be deployed. In this case, we’re using us-central1. You can choose a region closer to your users for lower latency.
  • —-allow-unauthenticated: This flag makes your deployed service publicly accessible without requiring authentication. This is convenient for initial testing and simple public services. For production applications, you should remove this flag and implement proper authentication and authorization.

Cloud Run will automatically build and deploy your application. You will be provided with a service URL in the output. Accessing this URL in your browser will display the “Hello, World!” message.

Hello world output from / route

Integrating with Google Cloud Services

Let’s now show how to integrate our application with Google Cloud services. I’ve selected a straightforward scenario that doesn’t require any project configuration to work. You’ll add a new application route /project that will display information about your project.

To implement this, you’ll use the google-cloud-rust library to interact with the Resource Manager API and retrieve information about your Google Cloud project.

Note: The google-cloud-rust library is currently experimental. APIs may change, and it’s important to stay updated with the latest releases and documentation.

Add Dependencies

First, add the Resource Manager v3 API and reqwest HTTP client to your Cargo.toml file:

cargo add google-cloud-resourcemanager-v3 reqwest
Enter fullscreen mode Exit fullscreen mode

Implement the handler

There are four key changes we’ll need to make in src/main.rs:

  • Add /project Route: A new route /project will display project information, implemented by project_handler()
async fn main() {
    // ...
    let app = Router::new()
        .route("/", get(handler))
        .route("/project", get(project_handler))
        .layer(Extension(std::sync::Arc::new(client)));
    // ...
}
Enter fullscreen mode Exit fullscreen mode
  • project_handler function: The project handler will call get_project() to fetch project details. Finally, it formats the project information into an HTML response. Error handling is included to display any errors that occur during the API call.
async fn project_handler(Extension(client): Extension<Arc<Projects>>) -> Html<String> {
    let project_id = PROJECT_ID.get().expect("Project ID not initialized");
    let project_name = format!("projects/{}", project_id);

    match client.get_project(project_name).send().await {
        Ok(project) => {
            let project_number = project.name.strip_prefix("projects/").unwrap_or("Unknown");

            Html(format!(
                "<h1>Project Info</h1><ul><li>Name: <code>{}</code></li><li>ID: <code>{}</code></li><li>Number: <code>{}</code></li></ul>",
                &project.display_name,
                project_id,
                project_number
            ))
        }
        Err(e) => Html(format!("<h1>Error getting project info: {}</h1>", e)),
    }
}
Enter fullscreen mode Exit fullscreen mode
  • Share client with handler: For best performance, any one-time configuration should not reside in the handler. The Projects client can be initialized in main() and then shared with the handler with Axum’s Extension.
  • Add helper function for project metadata : To find out the project ID the container is running in, you’ll need to access the metadata key. That project ID will then be used to call the Resource Manager API to get more information about the project, including its display name and creation time. You can use LazyLock to initialize the project only once.
static PROJECT_ID: OnceLock<String> = OnceLock::new();

async fn main() {
    // ...

    let project_id = get_project_id().await.expect("Failed to get project ID");
    PROJECT_ID.set(project_id).expect("Failed to set PROJECT_ID");

    // ...
}

async fn get_project_id() -> Result<String, String> {
    if let Ok(project_id) = var("GOOGLE_CLOUD_PROJECT") {
        return Ok(project_id);
    }

    let client = reqwest::Client::new();
    let url = "http://metadata.google.internal/computeMetadata/v1/project/project-id";

    let response = client
        .get(url)
        .header("Metadata-Flavor", "Google")
        .send()
        .await;

    match response {
        Ok(res) => {
            if res.status().is_success() {
                Ok(res.text().await.map_err(|e| e.to_string())?)
            } else {
                Err(format!("Metadata server returned error: {}", res.status()))
            }
        }
        Err(e) => Err(format!("Error querying metadata server: {}", e)),
    }
}
Enter fullscreen mode Exit fullscreen mode

Set GOOGLE_CLOUD_PROJECT Environment Variable (Locally)

For local testing, you’ll need to set the GOOGLE_CLOUD_PROJECT environment variable to your Google Cloud project ID. You can do this in your terminal before running the application:

export GOOGLE_CLOUD_PROJECT=your-project-id
Enter fullscreen mode Exit fullscreen mode

Replace your-project-id with your actual project ID. Cloud Run will automatically set this environment variable when deployed.

Enable the Resource Manager API

If you haven’t already, make sure to enable the Resource Manager API within your Google Cloud project.

Provide Resource Manager IAM access

You will need to provide the resourcemanager.projects.get role to the appropriate Cloud Run service account. The instructions here use the Compute Engine default service account. If you are running locally, you’ll also need to provide these permissions to your account.

Redeploy to Cloud Run

Use the same gcloud run deploy command as before to redeploy your updated application:

gcloud run deploy cloud-rust-example \
    --source . \
    --region us-central1 \
    --allow-unauthenticated
Enter fullscreen mode Exit fullscreen mode

Now, when you visit the service URL provided by Cloud Run and navigate to the /project path, you should see information about your Google Cloud project.

Project information output from /project route

Conclusion

This guide demonstrates the process of deploying a Rust Axum application on Cloud Run. I started with a basic “Hello, World!” example from the Axum repository, explained its code, and then showed how to enhance it for Cloud Run compatibility by dynamically configuring the port and creating a Dockerfile. By combining Rust and Axum with Cloud Run’s serverless simplicity, you can efficiently build and deploy robust web services. The sample source code is available in the cloud-rust-example repository.

For more information about Cloud Run, I recommend the quickstart for building and deploying a web application in the documentation. Also, check out this video for a video walkthrough of running Rust on Cloud Run. Feel free to connect on LinkedIn, X, and Bluesky to continue the discussion!


Top comments (0)

Image of Quadratic

The best Excel alternative with Python built-in

Quadratic is the all-in-one, browser-based AI spreadsheet that goes beyond traditional formulas for powerful visualizations and fast analysis.

Try Quadratic free