Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,10 @@ FodyWeavers.xsd
*.msix
*.msm
*.msp

# Compiled ARM JSON from `az bicep build` / `az bicep build-params` -- generated from the .bicep
# / .bicepparam sources under deployment/, never hand-edited or meant to be committed.
deployment/**/*.json

# Local copies of example .bicepparam files with real values filled in.
deployment/**/*.local.bicepparam
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RiftCache is an open-source, Redis-inspired distributed cache built for containe
- **Self-hosters**: one container, one API key, no cloud lock-in required.
- **Enterprise teams**: multi-tenancy, pluggable secrets/persistence providers, OpenTelemetry (traces + metrics), and Azure reference providers (Key Vault, Blob Storage — see [docs/providers/azure.md](docs/providers/azure.md)) today. AWS and GCP implementations of the same interfaces are designed for (see [ARCHITECTURE.md](ARCHITECTURE.md)) but not built yet — contributions welcome, see [CONTRIBUTING.md](CONTRIBUTING.md).

> Reference deployment today: **Docker / Podman** — see [Building From Source](#building-from-source-docker--podman) below. Azure Container Apps, AWS (Fargate/ECS), and GCP (Cloud Run) reference deployments are planned, not built yet. Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).
> Reference deployments today: **Docker / Podman** — see [Building From Source](#building-from-source-docker--podman) below — and **Azure Container Apps** (Bicep) — see [deployment/azure/bicep](deployment/azure/bicep). AWS (Fargate/ECS) and GCP (Cloud Run) reference deployments are planned, not built yet. Contributions welcome — see [CONTRIBUTING.md](CONTRIBUTING.md).

---

Expand Down Expand Up @@ -167,6 +167,7 @@ provider. Observability goes through OpenTelemetry directly, no custom abstracti
|---|---|
| [ARCHITECTURE.md](ARCHITECTURE.md) | Provider abstraction pattern, deployment layout, multi-tenancy design |
| [docs/providers/azure.md](docs/providers/azure.md) | Configuring `AzureKeyVaultSecretProvider` and `AzureBlobPersistenceProvider` |
| [deployment/azure/bicep](deployment/azure/bicep) | Azure Container Apps reference deployment (Bicep) |
| [ENTERPRISE_DEPLOYMENT.md](ENTERPRISE_DEPLOYMENT.md) | Running RiftCache as shared, centrally-owned infrastructure for multiple teams |
| [ROADMAP.md](ROADMAP.md) | Phased plan — core, Azure reference, AWS/GCP providers, enterprise features |
| [CONTRIBUTING.md](CONTRIBUTING.md) | How to add a cloud provider, submit PRs, project conventions |
Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
- [x] Default providers: `EnvironmentSecretProvider`, `NullPersistenceProvider` (self-host path)
- [x] Azure reference provider: Key Vault (`ISecretProvider`) — see [docs/providers/azure.md](docs/providers/azure.md)
- [x] Azure reference provider: Blob Storage (`IPersistenceProvider`) — see [docs/providers/azure.md](docs/providers/azure.md)
- [ ] Azure Container Apps reference deployment (Bicep) + Docker Compose for local dev
- [x] Azure Container Apps reference deployment (Bicep) + Docker Compose for local dev — see [deployment/azure/bicep](deployment/azure/bicep); memory-only (`NullPersistenceProvider`) in this template
- [ ] Wire `AzureBlobPersistenceProvider` into the Azure Container Apps reference deployment — needs `Program.cs` extracted into reusable `AddRiftCacheCore()`/`MapRiftCacheCore()` extensions first, plus a second composed entry point + Dockerfile that layers the Azure providers on top of the default
- [x] Single-tenant mode as default; multi-tenant mode as opt-in config
- [x] v0.1.0 release

Expand Down
79 changes: 79 additions & 0 deletions deployment/azure/bicep/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Azure Container Apps Reference Deployment

Deploys the core RiftCache image behind Azure Container Apps — no code changes, the exact same
image `deployment/docker/Dockerfile` builds. Key Vault backs `RIFTCACHE_API_KEY` via Container
Apps' native Key-Vault-backed secret injection: the platform resolves the secret using the app's
managed identity before the container ever starts, so the app itself never needs
`AzureKeyVaultSecretProvider` code for this.

**Persistence is memory-only in this template** (`NullPersistenceProvider`, the core service's
default). Wiring `AzureBlobPersistenceProvider` into a live deployment needs `Program.cs` extracted
into reusable `AddRiftCacheCore()`/`MapRiftCacheCore()` extensions first, plus a second composed
entry point that layers the Azure providers on top — real follow-up work, tracked in
[ROADMAP.md](../../../ROADMAP.md), not done here.

## What gets created

- Log Analytics Workspace (Container Apps Environment requires one for logging)
- Container Apps Environment (Consumption plan)
- A user-assigned Managed Identity
- Key Vault, with the identity granted **Key Vault Secrets User** (read-only)
- A Key Vault secret holding `RIFTCACHE_API_KEY`
- The Container App itself — external ingress on port 8080, 1–3 replicas by default

## Prerequisites

- An Azure subscription and a resource group to deploy into.
- The [Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli), logged in
(`az login`) with that subscription selected.
- **A container image already built and pushed somewhere you control.** Nothing is published
anywhere yet (see the main [README](../../../README.md)), so this template takes the image
reference as a parameter — build `deployment/docker/Dockerfile` and push it to your own registry
(Azure Container Registry, Docker Hub, GHCR, etc.) first.

## Deploy

```bash
cp main.bicepparam main.local.bicepparam
# edit main.local.bicepparam: set containerImage and riftCacheApiKey at minimum

az deployment group create \
--resource-group <your-resource-group> \
--template-file main.bicep \
--parameters main.local.bicepparam
```

## Verify

```bash
fqdn=$(az deployment group show \
--resource-group <your-resource-group> \
--name main \
--query properties.outputs.containerAppFqdn.value -o tsv)

curl "https://$fqdn/healthz"
curl -H "X-RiftCache-Api-Key: <the key you set>" "https://$fqdn/api/v1/cache/hello"
```

## Cost

Consumption-plan Container Apps, Key Vault, and Log Analytics are all pay-per-use with no fixed
minimum, but this isn't free to leave running indefinitely — `minReplicas` defaults to `1` (a cache
with zero warm replicas defeats its own purpose, unlike a typical stateless web app where
scale-to-zero makes sense), so there's always at least one replica running. Delete the resource
group when you're done evaluating this template:

```bash
az group delete --name <your-resource-group>
```

## Local validation without deploying

```bash
az bicep build --file main.bicep
az bicep build-params --file main.bicepparam
```

Both check the templates compile and every resource type/API version/property is valid against
Azure's real schemas — no subscription or credentials needed, since this is pure local
compilation.
159 changes: 159 additions & 0 deletions deployment/azure/bicep/main.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Azure Container Apps reference deployment for RiftCache.
//
// Deploys the core RiftCache image (Azure-SDK-free, no code changes) behind Azure Container
// Apps. Key Vault backs RIFTCACHE_API_KEY via Container Apps' native Key-Vault-backed secret
// injection -- the app itself never needs AzureKeyVaultSecretProvider code for that, since the
// platform resolves the secret using the app's managed identity before the container starts.
//
// Persistence is memory-only in this template (NullPersistenceProvider, the core service's
// default) -- wiring AzureBlobPersistenceProvider into a live deployment needs Program.cs
// extracted into reusable extensions first. See ROADMAP.md.
//
// Deploy with (see README.md in this directory for the full walkthrough):
// az deployment group create --resource-group <rg> --template-file main.bicep --parameters main.bicepparam

@description('Azure region for all resources.')
param location string = resourceGroup().location

@description('Name prefix applied to all resources.')
param namePrefix string = 'riftcache'

@description('Container image to deploy, e.g. myregistry.azurecr.io/riftcache:latest. No image is published anywhere yet -- build and push your own from deployment/docker/Dockerfile.')
param containerImage string

@description('The API key clients must send via the X-RiftCache-Api-Key header. Stored in Key Vault, never in a plain app setting.')
@secure()
param riftCacheApiKey string

@description('Minimum number of replicas. A cache with zero warm replicas defeats its own purpose, so this defaults to 1 rather than allowing scale-to-zero.')
param minReplicas int = 1

@description('Maximum number of replicas.')
param maxReplicas int = 3

var logAnalyticsName = '${namePrefix}-logs'
var containerAppsEnvironmentName = '${namePrefix}-env'
var identityName = '${namePrefix}-identity'
var keyVaultName = '${namePrefix}-kv-${uniqueString(resourceGroup().id)}'
var containerAppName = '${namePrefix}-app'
var apiKeySecretName = 'RIFTCACHE-API-KEY' // Key Vault secret names don't allow underscores.
var keyVaultSecretsUserRoleId = '4633458b-17de-408a-b874-0445c86b69e6'

resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = {
name: logAnalyticsName
location: location
properties: {
sku: {
name: 'PerGB2018'
}
retentionInDays: 30
}
}

resource containerAppsEnvironment 'Microsoft.App/managedEnvironments@2024-03-01' = {
name: containerAppsEnvironmentName
location: location
properties: {
appLogsConfiguration: {
destination: 'log-analytics'
logAnalyticsConfiguration: {
customerId: logAnalytics.properties.customerId
sharedKey: logAnalytics.listKeys().primarySharedKey
}
}
}
}

resource identity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: identityName
location: location
}

resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: subscription().tenantId
enableRbacAuthorization: true
}
}

resource apiKeySecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: keyVault
name: apiKeySecretName
properties: {
value: riftCacheApiKey
}
}

resource keyVaultSecretsUserRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, identity.id, keyVaultSecretsUserRoleId)
scope: keyVault
properties: {
principalId: identity.properties.principalId
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', keyVaultSecretsUserRoleId)
}
}

resource containerApp 'Microsoft.App/containerApps@2024-03-01' = {
name: containerAppName
location: location
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${identity.id}': {}
}
}
properties: {
managedEnvironmentId: containerAppsEnvironment.id
configuration: {
ingress: {
external: true
targetPort: 8080
}
secrets: [
{
name: 'riftcache-api-key'
keyVaultUrl: apiKeySecret.properties.secretUri
identity: identity.id
}
]
}
template: {
containers: [
{
name: 'riftcache'
image: containerImage
resources: {
cpu: json('0.25')
memory: '0.5Gi'
}
env: [
{
name: 'RIFTCACHE_API_KEY'
secretRef: 'riftcache-api-key'
}
]
}
]
scale: {
minReplicas: minReplicas
maxReplicas: maxReplicas
}
}
}
dependsOn: [
keyVaultSecretsUserRoleAssignment
]
}

@description('Public URL of the deployed RiftCache Container App.')
output containerAppFqdn string = containerApp.properties.configuration.ingress.fqdn

@description('Key Vault name, for adding/rotating RIFTCACHE_API_KEY or configuring AzureKeyVaultSecretProvider later.')
output keyVaultName string = keyVault.name
19 changes: 19 additions & 0 deletions deployment/azure/bicep/main.bicepparam
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using 'main.bicep'

// Copy this file to main.local.bicepparam (already covered by .gitignore, so a filled-in copy
// with real values never gets committed by accident) and fill in the values below before
// deploying. See README.md in this directory for the full walkthrough.

param namePrefix = 'riftcache'

// Build and push your own image from deployment/docker/Dockerfile first -- nothing is published
// anywhere yet. e.g.: 'myregistry.azurecr.io/riftcache:latest'
param containerImage = ''

// The value callers must send back via the X-RiftCache-Api-Key header. Pick something real --
// this becomes a Key Vault secret, not a plaintext app setting, but the value you put here is
// still visible in your shell history / this file, so don't reuse a key from anywhere else.
param riftCacheApiKey = ''

param minReplicas = 1
param maxReplicas = 3
Loading