Skip to content

Commit 12ac8a3

Browse files
author
Justin Pflueger
authored
Merge pull request #15 from Azure-Samples/justin-cosmosdb-demo
cosmosdb sample app
2 parents b465774 + 62e650d commit 12ac8a3

32 files changed

+2561
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -328,3 +328,6 @@ ASALocalRun/
328328

329329
# MFractors (Xamarin productivity tool) working folder
330330
.mfractor/
331+
332+
.env
333+
.vscode/

azure-votes-cosmosdb/README.md

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# CosmosDB Sample Application
2+
This is a very simple voting application to illustrate how to integrate different Azure resources into a Kubernetes application using the [Azure Service Operator](https://github.com/Azure/azure-service-operator).
3+
4+
## Pre-requisites
5+
- Azure Subscription
6+
- Azure CLI
7+
- Helm
8+
- Kubernetes Cluster >= 1.16.0
9+
- Docker
10+
- Environment Variables
11+
- `AZURE_TENANT_ID` - this value must be set in your environment to your Azure Tenant ID for some scripts and commands to work properly. You can view this if you are logged into the Azure CLI using `az account show`
12+
- `AZURE_SUBSCRIPTION_ID` - this value must be set in your environment to your Azure Subscription ID for some scripts and commands to work properly. You can view this if you are logged into the Azure CLI using `az account show`
13+
14+
## Outline
15+
1. Creating a basic AKS cluster
16+
2. Deploying the Azure Service Operator to the AKS cluster
17+
3. Deploying the sample application
18+
4. Setting up managed identity for authorization to Azure resources
19+
20+
## Resources
21+
- Scripts
22+
- [shared.sh](scripts/shared.sh) - this script sets up names and values that are shared between the scripts. If you want to customize the names of resources deployed for this application you will need to edit this file for the other scripts to function properly.
23+
- [gen_helm_values.sh](scripts/gen_helm_values.sh) - this script will use the Azure CLI to generate the content for the helm chart's values.yaml.
24+
- [step1_create_cluster.sh](scripts/step1_create_cluster.sh) - this script will use the Azure CLI to create a basic AKS cluster that we'll use to deploy our application to.
25+
- [step2_install_operator.sh](scripts/step2_install_operator.sh) - this script will deploy the Azure Service Operator and it's dependencies to an AKS cluster.
26+
- [step3_deploy_application.sh](scripts/step3_deploy_application.sh) - this script will deploy the application using the Helm chart located [here](charts/azure-votes-cosmosdb).
27+
- [step4_create_app_identity.sh](scripts/step4_create_app_identity.sh) - this script will use the Azure CLI to create a managed identity that our application will use to access Azure resources securely.
28+
29+
## Creating a basic AKS cluster
30+
You can use the [script](scripts/step1_create_cluster.sh) to create the AKS or you can enter these commands if you'd like to customize them yourself.
31+
32+
The first thing we need is a resource group to put the AKS cluster resource into.
33+
```
34+
az group create -n rg-aso-sample-infra -l eastus
35+
```
36+
37+
Next we create the AKS cluster. You can customize this command to suite your needs but in order to use the csi-secret-store-provider to mount Azure KeyVault secrets the cluster will need to be Kubernetes 1.16 or greater.
38+
```
39+
az aks create \
40+
--resource-group rg-aso-sample-infra \
41+
--name aks-aso-sample-infra \
42+
--kubernetes-version 1.16.7 \
43+
--generate-ssh-keys
44+
```
45+
46+
## Install Azure Service Operator
47+
You can use the [script](scripts/step2_install_operator.sh) to install the Azure Service Operator into an AKS cluster using the Helm chart.
48+
49+
### Create a Managed Identity for the Azure Service Operator
50+
For this sample we will use Azure AD Pod Identity to authorize the service operator to create Azure resources. In order to configure AAD Pod Identity we need to give the AKS Service Principal permissions to manage the Managed Identity that the operator will be running as. You can get the client id of the AKS Service Principal using the following command:
51+
```
52+
az aks show -g rg-aso-sample-infra -n aks-aso-sample-infra --query "servicePrincipalProfile.clientId"
53+
```
54+
55+
For this next command, make sure to copy the resource ID and client ID from the output. Create a Managed Identity for the Azure Service Operator:
56+
```
57+
az identity create -g rg-aso-sample-infra -n aso-manager-identity
58+
```
59+
60+
Create a role assignment for the AKS Service Principal to access the Managed Identity for the Azure Service Principal:
61+
```
62+
az role assignment create --role "Managed Identity Operator" --assignee "<paste-aks-service-principal-client-id>" --scope "<paste-managed-identity-resource-id>"
63+
```
64+
65+
Create a role assignment for the Azure Service Operator to be able to create resources in Azure:
66+
```
67+
az role assignment create --role "Contributor" --assignee "<paste-managed-identity-client-id>" --scope "/subscriptions/$AZURE_SUBSCRIPTION_ID"
68+
```
69+
70+
### Deploy Dependencies
71+
First get the credentials for the AKS cluster we have already created:
72+
```
73+
az aks get-credentials -g rg-aso-sample-infra -n aks-aso-sample-infra
74+
```
75+
76+
We need to deploy cert-manager as a requirement for the Azure Service Operator:
77+
```
78+
kubectl create namespace cert-manager
79+
kubectl label namespace cert-manager cert-manager.io/disable-validation=true
80+
kubectl apply --validate=false -f https://github.com/jetstack/cert-manager/releases/download/v0.12.0/cert-manager.yaml
81+
```
82+
83+
We need to deploy csi-secrets-store-provider-azure for our application, this component will mount KeyVault secrets as Volumes to our application's container.
84+
```
85+
helm install csi-secrets-store-provider-azure \
86+
csi-secrets-store-provider-azure/csi-secrets-store-provider-azure
87+
```
88+
89+
### Deploy Azure Service Operator
90+
This sample uses the Helm Chart to install the Azure Service Operator and you can see more details about deploy the helm chart [here](https://github.com/Azure/azure-service-operator)
91+
92+
```
93+
helm install aso "azure-service-operator/azure-service-operator" \
94+
--set azureSubscriptionID=$AZURE_SUBSCRIPTION_ID \
95+
--set azureTenantID=$AZURE_TENANT_ID \
96+
--set azureUseMI=True \
97+
--set aad-pod-identity.azureIdentity.resourceID=<paste-managed-identity-resource-id> \
98+
--set aad-pod-identity.azureIdentity.clientID=<paste-managed-identity-client-id>
99+
```
100+
101+
## Deploy the Application
102+
This sample application can be deployed using the helm chart. You will need to supply your own values in the [values.yaml](charts/azure-votes-cosmosdb/values.yaml). The [gen_helm_values.sh](scripts/gen_helm_values.sh) script can use the Azure CLI to generate most of the values for you except for the AKS Virtual Network name, that you will need to look up in the Azure Portal.
103+
104+
Component Templates:
105+
- [Resource Group](charts/azure-votes-cosmosdb/templates/resourcegroup.yaml)
106+
- [App Insights](charts/azure-votes-cosmosdb/templates/appinsights.yaml)
107+
- [KeyVault](charts/azure-votes-cosmosdb/templates/keyvault.yaml)
108+
- [CosmosDB](charts/azure-votes-cosmosdb/templates/cosmosdb.yaml)
109+
- [AAD Identity](charts/azure-votes-cosmosdb/templates/azureidentity.yaml)
110+
- [AAD Identity Binding](charts/azure-votes-cosmosdb/templates/azureidentitybinding.yaml)
111+
- [Secret Provider](charts/azure-votes-cosmosdb/templates/secretprovider.yaml)
112+
- [App Deployment](charts/azure-votes-cosmosdb/templates/app_deployment.yaml)
113+
- [App Service](charts/azure-votes-cosmosdb/templates/app_service.yaml)
114+
115+
### Build and push the sample application
116+
In order for the application to run you will need to build and push the docker container for it. You can use the docker image tag provided or build and push the container to a registry of your choosing by editing [this script](api/build_and_push.sh). If you do this you will need to change the `app.image` value in [values.yaml](charts/azure-votes-cosmosdb/values.yaml). The pod for our application won't come up until a later step when we setup the Managed Identity for the sample application.
117+
118+
### Helm Install
119+
```
120+
helm install app ./charts/azure-votes-cosmosdb
121+
```
122+
123+
## Configure Application Managed Identity
124+
In this sample we use AAD Pod Identity to read and mount Azure KeyVault secrets into our pod's containers. To do that we create a managed identity that our Pod's containers can bind to and authorize with. The steps required are written out by [this script](scripts/step4_create_app_identity.sh) but not executed because you may need to retry some of the commands. Before we can run these steps we need to verify that the resource group and keyvault resource have been provisioned using kubectl and looking the "Successfully provisioned" message.
125+
126+
Verify Resource Group has been provisioned:
127+
```
128+
kubectl describe resourcegroup rg-aso-votes-app
129+
```
130+
131+
Verify KeyVault has been provisioned:
132+
```
133+
kubectl describe keyvault kv-aso-votes-app
134+
```
135+
136+
### Create Managed Identity
137+
Again, make sure to copy the resource ID and client ID of the managed identity from the terminal output. Create the identity:
138+
```
139+
az identity create -g rg-aso-votes-app -n mi-aso-votes-app
140+
```
141+
142+
Create a role assignment for the AKS Service Principal to access the Managed Identity for the sample application:
143+
```
144+
az role assignment create --role "Managed Identity Operator" --assignee ${AKS_SP_ID} --scope \$APP_IDENTITY_ID
145+
```
146+
147+
Create a role assignment for the Managed Identity of the application to read KeyVault secrets:
148+
```
149+
az role assignment create --role Reader --assignee <paste-managed-identity-client-id> --scope "/subscriptions/${AZURE_SUBSCRIPTION_ID}/resourcegroups/rg-aso-votes-app/providers/Microsoft.KeyVault/vaults/kv-aso-votes-app"
150+
```
151+
152+
Add a policy to KeyVault to be able to get secrets using the Managed Identity:
153+
```
154+
az keyvault set-policy -n kv-aso-votes-app --secret-permissions get --spn <paste-managed-identity-client-id>
155+
```
156+
157+
### Update Helm Chart
158+
Now that we have the proper managed identity name and client ID we can update the [values.yaml](charts/azure-votes-cosmosdb/values.yaml) file to fix the pod for our application. Update the following fields in your values.yaml file using the name and ID from the previous step:
159+
```
160+
app:
161+
identity:
162+
name: <paste-managed-identity-name>
163+
clientID: <paste-managed-identity-clientid>
164+
```
165+
166+
Deploy the updated helm chart:
167+
```
168+
helm upgrade aso ./charts/azure-votes-cosmosdb
169+
```
170+
171+
After a few minutes the Pod should be able to authorize with KeyVault and mount the secret for CosmosDB to the container.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
npm-debug.log
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM node:13.12.0
2+
3+
# Create app directory
4+
WORKDIR /usr/src/app
5+
6+
# Install app dependencies
7+
# A wildcard is used to ensure both package.json AND package-lock.json are copied
8+
# where available (npm@5+)
9+
COPY package*.json ./
10+
11+
RUN npm ci --only=production
12+
13+
# Bundle app source
14+
COPY . .
15+
16+
EXPOSE 8080
17+
CMD [ "node", "server.js" ]
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
3+
tag="docker.io/jupflueg/aso-votes-app:latest"
4+
5+
docker build -t $tag .
6+
docker push $tag
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
const CosmosClient = require("@azure/cosmos").CosmosClient;
2+
3+
const { readJsonFile } = require('./util');
4+
5+
6+
class CosmosContainerClient {
7+
constructor(endpoint, key, databaseId, containerId) {
8+
this.client = new CosmosClient({
9+
endpoint: endpoint,
10+
key: key
11+
});;
12+
this.database = this.client.database(databaseId);
13+
this.container = this.database.container(containerId);
14+
}
15+
16+
async getAll() {
17+
//TODO: paging
18+
const resp = await this.container.items.readAll().fetchAll();
19+
return resp.resources;
20+
}
21+
22+
async get(id) {
23+
const resp = await this.container.item(id).read();
24+
return resp.resource;
25+
}
26+
27+
async insert(item) {
28+
const resp = await this.container.items.create(item);
29+
return resp.resource;
30+
}
31+
32+
async update(id, item) {
33+
const oldItem = await this.get(id);
34+
const newItem = {
35+
...oldItem,
36+
...item
37+
};
38+
const resp = await this.container.item(id).replace(newItem);
39+
return resp.resource;
40+
}
41+
42+
async delete(id) {
43+
const resp = await this.container.item(id).delete();
44+
return resp.resource;
45+
}
46+
47+
async query(query) {
48+
const resp = await this.container.items.query(query).fetchAll();
49+
return resp.resources;
50+
}
51+
}
52+
53+
const readSecretFile = async (secretPath) => {
54+
const secret = await readJsonFile(secretPath);
55+
return {
56+
endpoint: Buffer.from(secret["primaryEndpoint"], "base64").toString(),
57+
key: Buffer.from(secret["primaryMasterKey"], "base64").toString(),
58+
};
59+
};
60+
61+
/*
62+
* This function ensures that the database is setup and populated correctly
63+
*/
64+
async function createCosmos(config) {
65+
const {
66+
secretPath,
67+
databaseId,
68+
containerId,
69+
partitionKey,
70+
} = config;
71+
const {
72+
endpoint,
73+
key,
74+
} = await readSecretFile(secretPath);
75+
const client = new CosmosClient({ endpoint, key });
76+
77+
/**
78+
* Create the database if it does not exist
79+
*/
80+
const { database } = await client.databases.createIfNotExists({
81+
id: databaseId
82+
});
83+
console.log(`Created database:\n${database.id}\n`);
84+
85+
/**
86+
* Create the container if it does not exist
87+
*/
88+
const { container } = await client
89+
.database(config.databaseId)
90+
.containers.createIfNotExists(
91+
{ id: containerId, partitionKey },
92+
{ offerThroughput: 400 }
93+
);
94+
console.log(`Created container:\n${container.id}\n`);
95+
96+
return new CosmosContainerClient(endpoint, key, databaseId, containerId);
97+
}
98+
99+
function create(config) {
100+
const kind = (config.kind || "cosmos").toLowerCase();
101+
if (kind == "cosmos") {
102+
return createCosmos(config);
103+
}
104+
return null;
105+
}
106+
107+
module.exports = { create };

0 commit comments

Comments
 (0)