-
Notifications
You must be signed in to change notification settings - Fork 35
Add Azure resource type samples #2610
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # Go search API service + Radius.AI/search (Azure AI Search) | ||
|
|
||
| This sample provisions an Azure AI Search service through `Radius.AI/search` using an Azure Verified Module (AVM) recipe. It builds and runs a small Go HTTP API that stores documents in the search index and queries them through the Azure AI Search REST API. | ||
|
|
||
| ## What this sample shows | ||
| - **Resource type:** `Radius.AI/search` | ||
| - **Cloud backing (Azure):** Azure AI Search via the AVM `mcr.microsoft.com/bicep/avm/res/search/search-service:0.12.2` module (`env-azure.bicep`). | ||
| - **Application:** Go search API service `v1`, built from source via `Radius.Compute/containerImages` and run via `imageReference`. | ||
| - **Credential model:** The recipe exposes the admin key under nested `outputs.secrets.apiKey`. The app declares `connections.search` (Radius auto-injects the non-secret `CONNECTION_SEARCH_ENDPOINT`) and consumes the key explicitly as `CONNECTION_SEARCH_APIKEY` via a `secretKeyRef` backed by `searchService.properties.secrets`, which the Go service reads. | ||
|
|
||
| ## Files | ||
| | File | Role | | ||
| | --- | --- | | ||
| | `app.bicep` | Developer view: the Radius application, search resource, search API image build, and search API container. | | ||
| | `env-azure.bicep` | Platform-engineer view: the `recipePacks` binding the type to the AVM module plus supporting recipes, and the `environment`. | | ||
| | `bicepconfig.json` | Radius Bicep extension configuration. | | ||
| | `src/` | Go source for the search API service (stdlib only), its tests, and Dockerfile. | | ||
|
|
||
| ## Deploying | ||
| Deploy `env-azure.bicep` (the environment) first, then `app.bicep`. | ||
|
|
||
| This sample builds its container image from source with a `Radius.Compute/containerImages` recipe and pushes it to a registry you supply. When deploying `env-azure.bicep`, provide: | ||
| - `containerImagesRegistry` — an OCI registry you can push to (e.g. `ghcr.io/your-org`). | ||
| - `containerImagesRegistrySecretName` — the name of a Kubernetes Secret (`username`/`password`) in the environment namespace, used to authenticate the BuildKit **push** of the built image. Create it before deploying. | ||
|
|
||
| Image **pull** auth is separate: the `containerImages` recipe does not configure kubelet pull credentials, so the built image repository must be publicly readable by the cluster, or you must configure a Kubernetes image-pull secret (e.g. a `kubernetes.io/dockerconfigjson` `imagePullSecret` on the namespace's `default` ServiceAccount) before deploying `app.bicep` — otherwise the app pods fail with `ImagePullBackOff`. | ||
|
|
||
| ## Notes | ||
| The containerImages recipe builds from `git::https://github.com/radius-project/samples.git//samples/azure-search-api/src?ref=edge`; this source lands in this repo on merge. The service uses only the Go standard library, calls Azure AI Search REST API version `2024-07-01`, ensures an index on startup, and exposes `GET /healthz`, `POST /documents`, and `GET /search?q=...` on port 8080. | ||
|
|
||
| The default index name is `radius-sample`, overridable with `SEARCH_INDEX_NAME`. `POST /documents` accepts JSON documents with `id` and `content`, and `/search` returns the upstream Azure AI Search JSON response. | ||
|
|
||
| Additional deployment details: | ||
| - The Azure AI Search service uses the Basic SKU with one replica and one partition. | ||
| - The container image builds with the Dockerfile under `src/`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| extension radius | ||
|
|
||
| @description('The ID of your Radius Environment. Set automatically by the rad CLI.') | ||
| param environment string | ||
|
|
||
| resource app 'Radius.Core/applications@2025-08-01-preview' = { | ||
| name: 'search-azure-app-test' | ||
| properties: { | ||
| environment: environment | ||
| } | ||
| } | ||
|
|
||
| resource searchService 'Radius.AI/search@2025-08-01-preview' = { | ||
| name: 'search' | ||
| properties: { | ||
| environment: environment | ||
| application: app.id | ||
| } | ||
| } | ||
|
|
||
| resource searchApiImage 'Radius.Compute/containerImages@2025-08-01-preview' = { | ||
| name: 'search-api-image' | ||
| properties: { | ||
| environment: environment | ||
| application: app.id | ||
| tag: 'v1' | ||
| build: { | ||
| source: 'git::https://github.com/radius-project/samples.git//samples/azure-search-api/src?ref=edge' | ||
| } | ||
| } | ||
| } | ||
|
|
||
| resource searchapictr 'Radius.Compute/containers@2025-08-01-preview' = { | ||
| name: 'searchapictr' | ||
| properties: { | ||
| environment: environment | ||
| application: app.id | ||
| containers: { | ||
| searchapi: { | ||
| image: searchApiImage.properties.imageReference | ||
| ports: { | ||
| web: { | ||
| containerPort: 8080 | ||
| } | ||
| } | ||
| env: { | ||
| CONNECTION_SEARCH_APIKEY: { | ||
| valueFrom: { | ||
| secretKeyRef: { | ||
| secretName: searchService.properties.secrets.name | ||
| key: 'apiKey' | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| connections: { | ||
| search: { | ||
| source: searchService.id | ||
| } | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "experimentalFeaturesEnabled": { | ||
| "extensibility": true | ||
| }, | ||
| "extensions": { | ||
| "radius": "br:biceptypes.azurecr.io/radius:latest" | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| extension radius | ||
|
|
||
| @description('Azure subscription ID the environment provisions resources into.') | ||
| param azureSubscriptionId string | ||
|
|
||
| @description('Azure resource group the environment provisions resources into. Must already exist.') | ||
| param azureResourceGroup string | ||
|
|
||
| @description('Globally-unique Azure AI Search service name (2-60 lowercase letters, digits, or hyphens; cannot start or end with a hyphen). The workflow generates a unique value per run.') | ||
| param searchServiceName string | ||
|
|
||
| @description('OCI ref for the Radius.Compute/containers recipe. Override to a custom build if needed; defaults to the released public recipe.') | ||
| param containersRecipe string = 'ghcr.io/radius-project/kube-recipes/containers:latest' | ||
|
|
||
| @description('OCI registry the containerImages recipe builds and pushes the search-api image to (e.g. `ghcr.io/myorg`).') | ||
| param containerImagesRegistry string | ||
|
|
||
| @description('Kubernetes Secret (in the environment namespace) holding the push registry `username`/`password`.') | ||
| param containerImagesRegistrySecretName string | ||
|
|
||
| resource recipes 'Radius.Core/recipePacks@2025-08-01-preview' = { | ||
| name: 'search-azure-avm' | ||
| properties: { | ||
| recipes: { | ||
| 'Radius.AI/search': { | ||
| kind: 'bicep' | ||
|
|
||
| source: 'mcr.microsoft.com/bicep/avm/res/search/search-service:0.12.2' | ||
| parameters: { | ||
| name: searchServiceName | ||
| sku: 'basic' | ||
| disableLocalAuth: false | ||
| replicaCount: 1 | ||
| partitionCount: 1 | ||
|
|
||
| enableTelemetry: false | ||
| } | ||
|
|
||
| outputs: { | ||
| endpoint: 'endpoint' | ||
| secrets: { | ||
| apiKey: 'primaryKey' | ||
| } | ||
| } | ||
| } | ||
|
|
||
| 'Radius.Compute/containers': { | ||
| kind: 'bicep' | ||
| source: containersRecipe | ||
| } | ||
|
|
||
| 'Radius.Security/secrets': { | ||
| kind: 'bicep' | ||
| source: 'ghcr.io/radius-project/kube-recipes/secrets:latest' | ||
| } | ||
|
|
||
| 'Radius.Compute/containerImages': { | ||
| kind: 'terraform' | ||
| source: 'git::https://github.com/radius-project/resource-types-contrib.git//Compute/containerImages/recipes/kubernetes/terraform?ref=b9e0fad536a53349b98f94c5be961db84845e1b7' | ||
| parameters: { | ||
| registry: containerImagesRegistry | ||
| registrySecretName: containerImagesRegistrySecretName | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| resource env 'Radius.Core/environments@2025-08-01-preview' = { | ||
| name: 'azure' | ||
| properties: { | ||
| providers: { | ||
| azure: { | ||
| subscriptionId: azureSubscriptionId | ||
| resourceGroupName: azureResourceGroup | ||
| } | ||
|
|
||
| kubernetes: { | ||
| namespace: 'default' | ||
| } | ||
| } | ||
| recipePacks: [ | ||
| recipes.id | ||
| ] | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| FROM golang:1.23-alpine AS build | ||
| WORKDIR /src | ||
| COPY go.mod ./ | ||
| COPY *.go ./ | ||
| RUN CGO_ENABLED=0 GOOS=linux go build -o /out/azure-search-api . | ||
|
|
||
| FROM gcr.io/distroless/static-debian12 | ||
| COPY --from=build /out/azure-search-api /azure-search-api | ||
| EXPOSE 8080 | ||
| USER nonroot:nonroot | ||
| ENTRYPOINT ["/azure-search-api"] | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| module github.com/radius-project/samples/azure-search-api | ||
|
|
||
| go 1.23 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "log" | ||
| "net/http" | ||
| ) | ||
|
|
||
| type Server struct { | ||
| client *SearchClient | ||
| logger *log.Logger | ||
| } | ||
|
|
||
| func NewServer(client *SearchClient, logger *log.Logger) http.Handler { | ||
| if logger == nil { | ||
| logger = log.Default() | ||
| } | ||
| server := &Server{client: client, logger: logger} | ||
|
|
||
| mux := http.NewServeMux() | ||
| mux.HandleFunc("/healthz", server.healthz) | ||
| mux.HandleFunc("/documents", server.documents) | ||
| mux.HandleFunc("/search", server.search) | ||
| return mux | ||
| } | ||
|
|
||
| func (s *Server) healthz(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodGet { | ||
| writeError(w, http.StatusMethodNotAllowed, "method not allowed") | ||
| return | ||
| } | ||
| w.Header().Set("content-type", "application/json") | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(`{"status":"ok"}`)) | ||
| } | ||
|
|
||
| func (s *Server) documents(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodPost { | ||
| writeError(w, http.StatusMethodNotAllowed, "method not allowed") | ||
| return | ||
| } | ||
| defer r.Body.Close() | ||
|
|
||
| var doc Document | ||
| decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)) | ||
| decoder.DisallowUnknownFields() | ||
| if err := decoder.Decode(&doc); err != nil { | ||
| writeError(w, http.StatusBadRequest, "invalid JSON document") | ||
| return | ||
| } | ||
| if doc.ID == "" || doc.Content == "" { | ||
| writeError(w, http.StatusBadRequest, "id and content are required") | ||
| return | ||
| } | ||
|
|
||
| if err := s.client.UploadDocument(r.Context(), doc); err != nil { | ||
| s.logger.Printf("document upload failed: %v", err) | ||
| writeError(w, http.StatusBadGateway, "search service request failed") | ||
| return | ||
| } | ||
|
|
||
| w.Header().Set("content-type", "application/json") | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write([]byte(`{"status":"ok"}`)) | ||
| } | ||
|
|
||
| func (s *Server) search(w http.ResponseWriter, r *http.Request) { | ||
| if r.Method != http.MethodGet { | ||
| writeError(w, http.StatusMethodNotAllowed, "method not allowed") | ||
| return | ||
| } | ||
|
|
||
| query := r.URL.Query().Get("q") | ||
| if query == "" { | ||
| writeError(w, http.StatusBadRequest, "q query parameter is required") | ||
| return | ||
| } | ||
|
|
||
| body, err := s.client.Search(r.Context(), query) | ||
| if err != nil { | ||
| s.logger.Printf("search failed: %v", err) | ||
| writeError(w, http.StatusBadGateway, "search service request failed") | ||
| return | ||
| } | ||
|
|
||
| w.Header().Set("content-type", "application/json") | ||
| w.WriteHeader(http.StatusOK) | ||
| _, _ = w.Write(body) | ||
| } | ||
|
|
||
| func writeError(w http.ResponseWriter, status int, message string) { | ||
| w.Header().Set("content-type", "application/json") | ||
| w.WriteHeader(status) | ||
| _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "log" | ||
| "net/http" | ||
| "os" | ||
| "time" | ||
| ) | ||
|
|
||
| const ( | ||
| defaultPort = "8080" | ||
| defaultIndexName = "radius-sample" | ||
| ) | ||
|
|
||
| func main() { | ||
| logger := log.New(os.Stdout, "azure-search-api: ", log.LstdFlags) | ||
|
|
||
| port := getenvDefault("PORT", defaultPort) | ||
| indexName := getenvDefault("SEARCH_INDEX_NAME", defaultIndexName) | ||
| client := NewSearchClient( | ||
| os.Getenv("CONNECTION_SEARCH_ENDPOINT"), | ||
| os.Getenv("CONNECTION_SEARCH_APIKEY"), | ||
| indexName, | ||
| &http.Client{Timeout: 30 * time.Second}, | ||
| ) | ||
|
|
||
| ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) | ||
| if err := client.EnsureIndex(ctx); err != nil { | ||
| logger.Printf("warning: unable to ensure search index %q at startup: %v", indexName, err) | ||
| } else { | ||
| logger.Printf("search index %q is ready", indexName) | ||
| } | ||
| cancel() | ||
|
|
||
| addr := ":" + port | ||
| logger.Printf("listening on %s", addr) | ||
| if err := http.ListenAndServe(addr, NewServer(client, logger)); err != nil { | ||
| logger.Fatalf("server stopped: %v", err) | ||
| } | ||
| } | ||
|
|
||
| func getenvDefault(name, fallback string) string { | ||
| if value := os.Getenv(name); value != "" { | ||
| return value | ||
| } | ||
| return fallback | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.