Skip to content

Commit

Permalink
Add config/map/token endpoint to support Data Catalog (#187)
Browse files Browse the repository at this point in the history
* Add map token endpoint to tiler service

The tiler service will generate a token for use against an azure maps
instance, using the identity of the tiler (when deployed) or the local
developer credentials (in local development).

A test has been added that requires a local identity, and this has been
skipped in CI, which does not have access to those kind of credentials.

This endpoint will be used by the Data Catalog app to avoid distributing
an azure maps key within that application.

* Remove unneeded role assignment

* Remove unused variables
  • Loading branch information
mmcfarland authored Mar 18, 2024
1 parent dcfd7c7 commit 1282fbb
Show file tree
Hide file tree
Showing 28 changed files with 290 additions and 30 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ After migrations and development database loading are in place, you can rebuild

#### Running the services

There is a local proxy service that facilitates a local "managed identity" functionality, run as your local identity. Make sure to run

```console
az login
```

To run the servers, use

```console
Expand Down
15 changes: 15 additions & 0 deletions auxiliary/az-cli-proxy/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM mcr.microsoft.com/azure-cli:cbl-mariner2.0

# URL used to download the packages from the CFS
ARG INDEX_URL
ENV PIP_INDEX_URL=$INDEX_URL

# Setup pip and server dependencies
RUN python3 -m ensurepip --upgrade
RUN pip3 install fastapi uvicorn[standard] azure-identity

WORKDIR /opt/src

COPY . /opt/src

CMD uvicorn main:app --host 0.0.0.0 --port 8086 --reload --log-level info
45 changes: 45 additions & 0 deletions auxiliary/az-cli-proxy/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import time
from typing import Any, Optional
from typing import Dict

from azure.core.credentials import AccessToken
from azure.identity import AzureCliCredential
from fastapi import FastAPI

app = FastAPI()


class TokenProvider:
_instance: Optional["TokenProvider"] = None

_tokens: Dict[str, Optional[AccessToken]] = {}

def __init__(self) -> None:
self._token = None

def get_token(self, resource: str) -> AccessToken:
token = self._tokens.get(resource)
if token is None or token.expires_on < time.time() - 5:
token = AzureCliCredential().get_token(resource)
self._tokens[resource] = token
assert token is not None # neede for mypy
return token

@classmethod
def get_instance(cls) -> "TokenProvider":
if cls._instance is None:
cls._instance = cls()
return cls._instance


@app.get("/dev/token")
async def cli_token(resource: str = "") -> Dict[str, Any]:
"""Uses the az cli credential to get a token for the given resource. This is
meant to mimic the behavior of using managed identities in other spatio
services in the development environment."""
accessToken = TokenProvider.get_instance().get_token(resource)
return {
"access_token": accessToken.token,
"expires_on": accessToken.expires_on,
"resource": resource,
}
8 changes: 7 additions & 1 deletion deployment/helm/deploy-values.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ tiler:
replicaCount: "{{ tf.tiler_replica_count }}"
podAnnotations:
"pc/gitsha": "{{ env.GIT_COMMIT }}"
useWorkloadIdentity: true

serviceAccount:
annotations:
"azure.workload.identity/client-id": {{ tf.cluster_tiler_identity_client_id }}
"azure.workload.identity/tenant-id": {{ tf.tenant_id }}

default_max_items_per_tile: 5
appRootPath: "/data"
Expand Down Expand Up @@ -152,4 +158,4 @@ secretProvider:
tenantId: "{{ env.AZURE_TENANT }}"
keyvaultName: "{{ env.KEYVAULT_NAME }}"
keyvaultCertificateName: "{{ env.SECRET_PROVIDER_KEYVAULT_SECRET }}"
kubernetesCertificateSecretName: "{{ env.SECRET_PROVIDER_KEYVAULT_SECRET }}"
kubernetesCertificateSecretName: "{{ env.SECRET_PROVIDER_KEYVAULT_SECRET }}"
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ app.kubernetes.io/instance: {{ .Release.Name }}
Common labels
*/}}
{{- define "pctiler.labels" -}}
azure.workload.identity/use: {{ .Values.tiler.deploy.useWorkloadIdentity | quote}}
helm.sh/chart: {{ include "pctiler.chart" . }}
{{ include "pctiler.selectorLabels" . }}
{{- if .Chart.AppVersion }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
labels:
{{- include "pctiler.selectorLabels" . | nindent 8 }}
{{- include "pctiler.labels" . | nindent 8 }}
spec:
{{- with .Values.tiler.deploy.imagePullSecrets }}
imagePullSecrets:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ metadata:
name: {{ include "pctiler.serviceAccountName" . }}
labels:
{{- include "pctiler.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
{{- with .Values.tiler.deploy.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ tiler:
affinity: {}
autoscaling:
enabled: false
useWorkloadIdentity: false
serviceAccount:
annotations: {}

stac_api_url: ""
stac_api_href: ""
Expand Down
30 changes: 28 additions & 2 deletions deployment/terraform/resources/aks.tf
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ resource "azurerm_kubernetes_cluster" "pc" {
key_vault_secrets_provider {
secret_rotation_enabled = true
}
oidc_issuer_enabled = true

# https://learn.microsoft.com/en-us/azure/aks/auto-upgrade-cluster#use-cluster-auto-upgrade
oidc_issuer_enabled = true
workload_identity_enabled = true

# https://learn.microsoft.com/en-us/azure/aks/auto-upgrade-cluster#use-cluster-auto-upgrade
automatic_channel_upgrade = "rapid"

# https://learn.microsoft.com/en-us/azure/aks/auto-upgrade-node-os-image
Expand Down Expand Up @@ -42,6 +44,30 @@ resource "azurerm_kubernetes_cluster" "pc" {
}
}

# Workload Identity for tiler access to the Azure Maps account
resource "azurerm_user_assigned_identity" "tiler" {
name = "id-${local.prefix}"
location = var.region
resource_group_name = azurerm_resource_group.pc.name
}

resource "azurerm_federated_identity_credential" "tiler" {
name = "federated-id-${local.prefix}"
resource_group_name = azurerm_resource_group.pc.name
audience = ["api://AzureADTokenExchange"]
issuer = azurerm_kubernetes_cluster.pc.oidc_issuer_url
subject = "system:serviceaccount:pc:planetary-computer-tiler"
parent_id = azurerm_user_assigned_identity.tiler.id
timeouts {}
}

resource "azurerm_role_assignment" "cluster-identity-maps-render-token" {
scope = azurerm_maps_account.azmaps.id
role_definition_name = "Azure Maps Search and Render Data Reader"
principal_id = azurerm_user_assigned_identity.tiler.principal_id

}

# add the role to the identity the kubernetes cluster was assigned
resource "azurerm_role_assignment" "network" {
scope = azurerm_resource_group.pc.id
Expand Down
6 changes: 6 additions & 0 deletions deployment/terraform/resources/functions.tf
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ resource "azurerm_function_app" "pcfuncs" {
allowed_origins = ["*"]
}
}

lifecycle {
ignore_changes = [
tags
]
}
}

# Note: this must be in the same subscription as the rest of the deployed infrastructure
Expand Down
5 changes: 5 additions & 0 deletions deployment/terraform/resources/maps.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "azurerm_maps_account" "azmaps" {
name = "azmaps-${local.prefix}"
resource_group_name = azurerm_resource_group.pc.name
sku_name = "G2"
}
6 changes: 5 additions & 1 deletion deployment/terraform/resources/output.tf
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ output "cluster_cert_server" {
value = var.cluster_cert_server
}

output "cluster_tiler_identity_client_id" {
value = azurerm_user_assigned_identity.tiler.client_id
}

## Ingress

output "ingress_ip" {
Expand Down Expand Up @@ -134,4 +138,4 @@ output "redis_port" {

output "function_app_name" {
value = azurerm_function_app.pcfuncs.name
}
}
2 changes: 1 addition & 1 deletion deployment/terraform/resources/rg.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ resource "azurerm_resource_group" "pc" {
tags = {
"ringValue" = "r0"
}
}
}
36 changes: 18 additions & 18 deletions deployment/terraform/resources/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -53,47 +53,47 @@ variable "k8s_version" {
# -- Postgres

variable "pg_host" {
type = string
type = string
default = "pct-stacdb.postgres.database.azure.com"
}

variable "pg_port" {
type = string
type = string
default = "5432"
}

variable "pg_user" {
type = string
default ="planetarycomputertest"
type = string
default = "planetarycomputertest"
}

variable "pg_database" {
type = string
type = string
default = "postgres"
}

variable "pg_password_secret_name" {
type = string
type = string
description = "The secret name in the KeyVault that holds the db password"
default = "pct-db-password"
default = "pct-db-password"
}

variable "pc_sdk_subscription_key_secret_name" {
type = string
type = string
description = "The secret name in the KeyVault that holds the PC subscription key used by the tiler"
default = "pct-tiler-sdk-subscription-key"
default = "pct-tiler-sdk-subscription-key"
}

variable "secret_provider_keyvault_name" {
type = string
type = string
description = "The name of the KeyVault that holds the secrets"
default = "pc-deploy-secrets"
default = "pc-deploy-secrets"
}

variable "secret_provider_keyvault_secret" {
type = string
type = string
description = "The name of the certificate in the KeyVault for TLS ingress"
default = "planetarycomputer-test-certificate"
default = "planetarycomputer-test-certificate"
}

# -- Functions --
Expand All @@ -107,7 +107,7 @@ variable "output_container_name" {
}

variable "funcs_tile_request_concurrency" {
type = number
type = number
default = 10
}

Expand All @@ -131,8 +131,8 @@ variable "image_output_storage_url" {
# Local variables

locals {
stack_id = "pct-apis"
location = lower(replace(var.region, " ", ""))
prefix = "${local.stack_id}-${local.location}-${var.environment}"
nodash_prefix = replace("${local.stack_id}${var.environment}", "-", "")
stack_id = "pct-apis"
location = lower(replace(var.region, " ", ""))
prefix = "${local.stack_id}-${local.location}-${var.environment}"
nodash_prefix = replace("${local.stack_id}${var.environment}", "-", "")
}
2 changes: 1 addition & 1 deletion deployment/terraform/staging/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ module "resources" {
environment = "staging"
region = "West Europe"

k8s_version = "1.25.6"
k8s_version = "1.28.5"

cluster_cert_issuer = "letsencrypt"
cluster_cert_server = "https://acme-v02.api.letsencrypt.org/directory"
Expand Down
1 change: 1 addition & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ services:
- PCAPIS_REDIS_SSL=FALSE
volumes:
- .:/opt/src
- ~/.azure:/root/.azure
command: >
/bin/bash
depends_on:
Expand Down
15 changes: 15 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ services:
context: .
dockerfile: pctiler/Dockerfile
env_file: ${PC_TILER_ENV_FILE:-./pc-tiler.dev.env}
environment:
# Allow proxied managed identity requests in dev
- IDENTITY_ENDPOINT=http://token-proxy:8086/dev/token
- IMDS_ENDPOINT=active
ports:
- "8082:8082"
volumes:
Expand Down Expand Up @@ -84,6 +88,17 @@ services:
volumes:
- pc-apis-pgdata:/var/lib/postgresql/data

token-proxy:
image: pc-aux-token-proxy
build:
context: ./auxiliary/az-cli-proxy
dockerfile: Dockerfile
ports:
- 8086:8086
volumes:
- ./auxiliary/az-cli-proxy:/opt/src/
- ~/.azure:/root/.azure

azurite:
container_name: pcapis-azurite
image: mcr.microsoft.com/azure-storage/azurite:3.17.1
Expand Down
28 changes: 28 additions & 0 deletions pccommon/pccommon/credential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import threading
from typing import Any

from azure.core.credentials import AccessToken
from azure.identity import DefaultAzureCredential


class PcDefaultAzureCredential:
"""Singleton wrapper around DefaultAzureCredential to share in memory cache
between requests and threads. Assumption of thread safety for method calls is
based on:
https://github.com/Azure/azure-sdk-for-python/issues/28665
"""

_instance = None
_lock = threading.Lock()

@classmethod
def get_token(cls, *scopes: str, **kwargs: Any) -> AccessToken:
return cls.get_credential().get_token(*scopes, **kwargs)

@classmethod
def get_credential(cls) -> DefaultAzureCredential:
if cls._instance is None:
with cls._lock:
cls._instance = DefaultAzureCredential()
return cls._instance
2 changes: 2 additions & 0 deletions pctiler/Dockerfile.dev
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
FROM pc-apis-tiler

RUN curl -sL https://aka.ms/InstallAzureCLIDeb | bash

COPY requirements-dev.txt requirements-dev.txt

RUN python3 -m pip install -r requirements-dev.txt
Expand Down
1 change: 1 addition & 0 deletions pctiler/pctiler/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class Settings(BaseSettings):

title: str = "Preview of Tile Access Services"
openapi_url: str = "/openapi.json"
configuration_endpoint_prefix: str = "/config"
item_endpoint_prefix: str = "/item"
mosaic_endpoint_prefix: str = "/mosaic"
legend_endpoint_prefix: str = "/legend"
Expand Down
Loading

0 comments on commit 1282fbb

Please sign in to comment.