Watch Aspire live streamsDocumentationEssayer Aspire
Watch Aspire live streamsDocumentationEssayer

Connect to Rust apps

Ce contenu n’est pas encore disponible dans votre langue.

Rust logo

This article describes how the Rust apps in your Aspire solution connect to other resources, in both directions. When another resource references a Rust app, Aspire injects the Rust app’s URL into that resource, so apps written in C#, Go, Python, TypeScript, or Rust can call it without hard-coded addresses. When a Rust app references other resources — such as a database, a cache, or another service — Aspire injects their URLs and connection details into the app’s environment, where your code reads them with std::env::var. It also describes how Rust apps send telemetry to the Aspire dashboard and trust the development certificate.

For the AppHost APIs that add and configure Rust apps, see Set up Rust apps in the AppHost. If you’re new to the Rust integration, start with Get started with the Rust integration.

The examples in this article use the following AppHost. The orders Rust app references a PostgreSQL database and the catalog Rust app, and a Node.js storefront app references orders:

apphost.mts
import {
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
} from './.aspire/modules/aspire.mjs';
const
const builder: IDistributedApplicationBuilder
builder
= await
function createBuilder(): IDistributedApplicationBuilder

Creates a new distributed application builder

createBuilder
();
const
const postgres: PostgresServerResource
postgres
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addPostgres(name: string, options?: {
userName?: string | ParameterResource;
password?: string | ParameterResource;
port?: number;
}): PostgresServerResource (+1 overload)

Adds a PostgreSQL resource to the application model. A container is used for local development.

addPostgres
('postgres');
const
const ordersDb: PostgresDatabaseResource
ordersDb
= await
const postgres: PostgresServerResource
postgres
.
PostgresServerResource.addDatabase(name: string, options?: {
databaseName?: string;
} | undefined): PostgresDatabaseResource (+1 overload)

Adds a PostgreSQL database to the application model.

addDatabase
('ordersdb');
const
const catalog: RustAppResource
catalog
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addRustApp(name: string, appDirectory: string): RustAppResource (+2 overloads)

Adds a Rust application to the application model.

addRustApp
('catalog', '../catalog');
await
const catalog: RustAppResource
catalog
.
ExecutableResource.withHttpEndpoint(options?: {
port?: number;
targetPort?: number;
name?: string;
env?: string;
isProxied?: boolean;
} | undefined): RustAppResource (+1 overload)

Adds an HTTP endpoint

withHttpEndpoint
({
env?: string | undefined
env
: 'PORT' });
await
const catalog: RustAppResource
catalog
.
ExecutableResource.withHttpHealthCheck(options?: {
path?: string;
statusCode?: number;
endpointName?: string;
} | undefined): RustAppResource (+1 overload)

Adds a health check to the resource which is mapped to a specific endpoint.

withHttpHealthCheck
({
path?: string | undefined
path
: '/health' });
const
const orders: RustAppResource
orders
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addRustApp(name: string, appDirectory: string): RustAppResource (+2 overloads)

Adds a Rust application to the application model.

addRustApp
('orders', '../orders');
await
const orders: RustAppResource
orders
.
ExecutableResource.withHttpEndpoint(options?: {
port?: number;
targetPort?: number;
name?: string;
env?: string;
isProxied?: boolean;
} | undefined): RustAppResource (+1 overload)

Adds an HTTP endpoint

withHttpEndpoint
({
env?: string | undefined
env
: 'PORT' });
await
const orders: RustAppResource
orders
.
ExecutableResource.withHttpHealthCheck(options?: {
path?: string;
statusCode?: number;
endpointName?: string;
} | undefined): RustAppResource (+1 overload)

Adds a health check to the resource which is mapped to a specific endpoint.

withHttpHealthCheck
({
path?: string | undefined
path
: '/health' });
await
const orders: RustAppResource
orders
.
ExecutableResource.withReference(source: IResource | EndpointReference | string | uri, options?: {
connectionName?: string;
optional?: boolean;
name?: string;
} | undefined): RustAppResource (+1 overload)

Adds a reference to another resource

withReference
(
const ordersDb: PostgresDatabaseResource
ordersDb
);
await
const orders: RustAppResource
orders
.
ExecutableResource.withReference(source: IResource | EndpointReference | string | uri, options?: {
connectionName?: string;
optional?: boolean;
name?: string;
} | undefined): RustAppResource (+1 overload)

Adds a reference to another resource

withReference
(
const catalog: RustAppResource
catalog
);
await
const orders: RustAppResource
orders
.
ExecutableResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): RustAppResource

Waits for another resource to be ready

waitFor
(
const ordersDb: PostgresDatabaseResource
ordersDb
);
await
const orders: RustAppResource
orders
.
ExecutableResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): RustAppResource

Waits for another resource to be ready

waitFor
(
const catalog: RustAppResource
catalog
);
const
const storefront: NodeAppResource
storefront
= await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.addNodeApp(name: string, appDirectory: string, scriptPath: string): NodeAppResource

Adds a node application to the application model. Node should be available on the PATH.

addNodeApp
('storefront', '../storefront', 'server.js');
await
const storefront: NodeAppResource
storefront
.
ExecutableResource.withReference(source: IResource | EndpointReference | string | uri, options?: {
connectionName?: string;
optional?: boolean;
name?: string;
} | undefined): NodeAppResource (+1 overload)

Adds a reference to another resource

withReference
(
const orders: RustAppResource
orders
);
await
const storefront: NodeAppResource
storefront
.
ExecutableResource.waitFor(dependency: IResource | IResourceWithConnectionString, waitBehavior?: WaitBehavior): NodeAppResource

Waits for another resource to be ready

waitFor
(
const orders: RustAppResource
orders
);
await
const builder: IDistributedApplicationBuilder
builder
.
IDistributedApplicationBuilder.build(): DistributedApplication

Builds the distributed application

build
().
DistributedApplication.run(cancellationToken?: cancellationToken): void

Runs the distributed application

run
();

WithReferencewithReference injects the referenced resource’s connection information into the referencing resource, and WaitForwaitFor delays the referencing resource until the referenced one is running — or healthy, when it has a health check. A Rust app can be on either side of a reference, and the environment variables are the same whatever language the other app uses.

AddRustAppaddRustApp doesn’t add an endpoint, so each Rust app in the preceding AppHost gets one from WithHttpEndpointwithHttpEndpoint, which passes the app its port in the PORT environment variable. Each app also gets a health check from WithHttpHealthCheckwithHttpHealthCheck. Without it, orders would start as soon as Cargo starts building catalog, rather than when catalog is ready for requests.

Aspire passes connection information to apps as environment variables. A Rust app is on both sides of that exchange: it exposes endpoints that other apps call, and it receives variables of its own.

Variables for apps that reference a Rust app

Section titled “Variables for apps that reference a Rust app”

When a resource references a Rust app, Aspire injects the URL of each of the Rust app’s endpoints. In the preceding AppHost, storefront receives the following variables for the orders app:

Environment variableDescription
ORDERS_HTTPThe URL of the orders app’s http endpoint, named {RESOURCE}_{ENDPOINT}. Read this variable from apps that don’t use .NET service discovery.
services__orders__http__0The same URL, in the format that .NET service discovery reads.

WithHttpEndpoint names its endpoint http unless you pass a name. When you add an endpoint with a different name to a Rust app, the variables use that name instead — for example, an endpoint named admin produces ORDERS_ADMIN. For the naming rules, see Endpoint URLs and Service discovery variables.

A Rust app receives the variables of every resource that it references. In the preceding AppHost, orders receives CATALOG_HTTP and services__catalog__http__0 for the catalog app, and the connection properties of the ordersdb database, which include:

Environment variableDescription
ORDERSDB_URIThe connection URI, in the format postgresql://{Username}:{Password}@{Host}:{Port}/{DatabaseName}. Rust database crates such as sqlx accept it as-is.
ORDERSDB_USERNAMEThe user name for authentication.
ORDERSDB_PASSWORDThe password for authentication.
ORDERSDB_HOSTThe host name of the PostgreSQL server.
ORDERSDB_PORTThe port of the PostgreSQL server.
ORDERSDB_DATABASENAMEThe name of the database.

Each resource type documents its own connection properties, such as those in Connect to PostgreSQL. For the naming rules, see Resource properties.

Aspire also sets the following variables on the Rust app itself:

Environment variableSet onDescription
PORT, or the variable that you name with the env parameter of WithHttpEndpointRust apps with an endpoint that sets envThe port that the app must listen on.
OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_SERVICE_NAME, and the other OTEL_* variablesAll Rust resourcesWhere and how to export telemetry. For details, see OpenTelemetry environment variables.
SSL_CERT_DIR, and SSL_CERT_FILE with the System trust scopeRust apps, when you run the AppHostThe locations of the certificates that the app trusts, including the development certificate. For details, see Trust the development certificate.

Apps call a Rust app at the URL that Aspire injects. Each example calls the orders app from the storefront app in the preceding AppHost. Only the language differs.

In a project that uses the Aspire service defaults, .NET service discovery resolves the orders name from the services__orders__http__0 variable:

Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHttpClient("orders", client =>
{
client.BaseAddress = new Uri("https+http://orders");
});
var app = builder.Build();
app.MapGet("/orders", async (IHttpClientFactory factory) =>
{
var client = factory.CreateClient("orders");
return await client.GetStringAsync("/orders");
});
app.Run();

The https+http scheme prefers an HTTPS endpoint and falls back to HTTP. Without service discovery, read the URL from the ORDERS_HTTP configuration value instead.

Read Aspire configuration in your Rust app

Section titled “Read Aspire configuration in your Rust app”

There’s no Aspire client crate for Rust. A Rust app reads the variables that Aspire injects with std::env::var, and passes them to the crates that it already uses. The following example configures the orders app from the preceding AppHost to listen on its port, call the catalog app, and use the ordersdb database. It uses axum for the web server, reqwest for the HTTP client, and sqlx for the database. Add them to the app:

Terminal
cargo add axum reqwest
cargo add tokio --features macros,rt-multi-thread
cargo add sqlx --features postgres,runtime-tokio

Then read the variables when the app starts:

src/main.rs
use std::{env, net::SocketAddr};
use axum::{extract::State, http::StatusCode, routing::get, Router};
use sqlx::PgPool;
#[derive(Clone)]
struct AppState {
catalog_url: String,
http: reqwest::Client,
db: PgPool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let catalog_url =
env::var("CATALOG_HTTP").unwrap_or_else(|_| "http://localhost:8080".to_string());
let database_url = env::var("ORDERSDB_URI")?;
let state = AppState {
catalog_url,
http: reqwest::Client::new(),
db: PgPool::connect(&database_url).await?,
};
let app = Router::new()
.route("/health", get(|| async { "healthy" }))
.route("/orders", get(order_count))
.route("/products", get(products))
.with_state(state);
let port = env::var("PORT")
.ok()
.and_then(|value| value.parse::<u16>().ok())
.unwrap_or(8081);
let listener = tokio::net::TcpListener::bind(SocketAddr::from(([0, 0, 0, 0], port))).await?;
axum::serve(listener, app).await?;
Ok(())
}
async fn order_count(State(state): State<AppState>) -> Result<String, StatusCode> {
let count: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM orders")
.fetch_one(&state.db)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(format!("{count} orders"))
}
async fn products(State(state): State<AppState>) -> Result<String, StatusCode> {
let response = state
.http
.get(format!("{}/products", state.catalog_url))
.send()
.await
.map_err(|_| StatusCode::BAD_GATEWAY)?;
response.text().await.map_err(|_| StatusCode::BAD_GATEWAY)
}

The defaults for CATALOG_HTTP and PORT keep the app runnable outside Aspire, alongside a catalog app that listens on port 8080. Reading ORDERSDB_URI with the ? operator makes the app exit with an error when the variable isn’t set.

Every Rust resource receives the standard OpenTelemetry environment variables, but a Rust app doesn’t export telemetry by itself, and Rust has no automatic instrumentation agent. To send telemetry to the dashboard, configure the OpenTelemetry SDK for Rust with the OTLP exporter from the opentelemetry-otlp crate. The exporter reads the dashboard’s endpoint and the headers that authenticate with it from the OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS variables, and the SDK reads the app’s name from OTEL_SERVICE_NAME. Configure the exporter as follows:

  • Use the gRPC transport. Aspire points OTEL_EXPORTER_OTLP_ENDPOINT at the dashboard’s gRPC endpoint whenever the dashboard has one, and sets OTEL_EXPORTER_OTLP_PROTOCOL to grpc. Select the gRPC transport explicitly with with_tonic(), because only the gRPC exporter builder accepts the TLS configuration that the next item describes. If the dashboard has only an OTLP/HTTP endpoint, Aspire sets OTEL_EXPORTER_OTLP_PROTOCOL to http/protobuf instead. In that case, build the exporter with with_http(), and configure certificate trust on the HTTP client that it uses. For the dashboard’s endpoint settings, see Dashboard configuration.
  • Trust the native certificate roots. When you run the AppHost, the dashboard’s endpoint typically uses HTTPS with the development certificate. Configure the exporter’s TLS with ClientTlsConfig::new().with_native_roots(), which loads certificates from the SSL_CERT_DIR directory that Aspire sets.
  • Select one TLS provider for both crates. Enable the same rustls crypto provider — tls-aws-lc or tls-ring — on opentelemetry-otlp and on tonic. Cargo features are additive, so selecting different providers compiles both of them, rather than replacing one with the other.

The following dependencies export traces over gRPC with AWS-LC as the TLS provider, as the Aspire Rust playground does. Building AWS-LC requires native build tools, such as a C compiler. To use ring instead, replace tls-aws-lc with tls-ring for both crates. Keep the tonic version in step with the one that opentelemetry-otlp uses, because the exporter’s with_tls_config method takes tonic’s ClientTlsConfig type.

Cargo.toml
[dependencies]
opentelemetry = "0.33"
opentelemetry_sdk = { version = "0.33", features = ["rt-tokio"] }
opentelemetry-otlp = { version = "0.33", features = ["grpc-tonic", "tls-roots", "tls-aws-lc"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tonic = { version = "0.14", default-features = false, features = ["transport", "tls-native-roots", "tls-aws-lc"] }

Create the exporter and a tracer provider when the app starts:

src/telemetry.rs
use opentelemetry::global;
use opentelemetry_otlp::{SpanExporter, WithTonicConfig};
use opentelemetry_sdk::trace::SdkTracerProvider;
use tonic::transport::ClientTlsConfig;
pub fn init_tracing() -> Result<SdkTracerProvider, Box<dyn std::error::Error>> {
let exporter = SpanExporter::builder()
.with_tonic()
.with_tls_config(ClientTlsConfig::new().with_native_roots())
.build()?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.build();
global::set_tracer_provider(provider.clone());
Ok(provider)
}

Call it from inside the Tokio runtime, and shut down the provider before the app exits, so that the exporter sends the spans that it has buffered:

src/main.rs
mod telemetry;
use opentelemetry::{global, trace::Tracer};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let tracer_provider = telemetry::init_tracing()?;
let result = run().await;
if let Err(error) = tracer_provider.shutdown() {
eprintln!("failed to shut down the tracer provider: {error}");
}
result
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
global::tracer("orders").in_span("load-orders", |_cx| {
// Work that the span measures.
});
Ok(())
}

The dashboard shows the spans under the app’s resource name, which Aspire passes to the SDK in OTEL_SERVICE_NAME. To export metrics and logs as well, and to connect the spans and events of the tracing crate to OpenTelemetry, see the telemetry.rs file of the Aspire Rust playground. An app that doesn’t configure the SDK doesn’t send telemetry to the dashboard, but its console output still appears in the dashboard’s console logs.

When you run the AppHost, Aspire sets SSL_CERT_DIR on each Rust app to a directory that contains the development certificate. OpenSSL, and clients that load their trusted certificates with the rustls-native-certs crate — such as tonic with with_native_roots() — read that variable, so they trust HTTPS endpoints that use the development certificate without any code changes. On Windows and macOS, those clients then typically trust only the development certificate, rather than public certificate authorities, unless you set the app’s trust scope to System. Clients that use the operating system’s certificate store, and clients that use the built-in certificates of the webpki-roots crate, behave differently. For details, see Configure certificate trust.