This repository has been archived by the owner on Jun 19, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
/
template.json
176 lines (176 loc) · 15.7 KB
/
template.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
{
"$schema": "http://schema.management.azure.com/schemas/2014-04-01-preview/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"functionsAppServiceName": {
"type": "string",
"defaultValue": "[concat('cosmoscheck', uniqueString(subscription().subscriptionId, resourceGroup().id))]",
"metadata": {
"description": "The name of the Azure Functions application to create. This must be unique."
}
},
"functionSchedule": {
"type": "string",
"defaultValue": "0 0 * */1 * *",
"metadata": {
"description": "The schedule on which to run the function. This should be a CRON expression."
}
},
"storageAccountName": {
"type": "string",
"defaultValue": "[concat('cosmoscheck', uniqueString(subscription().subscriptionId, resourceGroup().id))]",
"metadata": {
"description": "The name of the Azure Storage account to store function logs in."
}
},
"storageAccountType": {
"type": "string",
"defaultValue": "Standard_LRS",
"allowedValues": [
"Standard_LRS",
"Standard_GRS",
"Standard_RAGRS",
"Standard_ZRS",
"Premium_LRS"
],
"metadata": {
"description": "The account type of the Azure Storage blob account for the Azure Functions app."
}
},
"applicationInsightsLocation": {
"type": "string",
"defaultValue": "westus2",
"metadata": {
"description": "The location into which Application Insights should be deployed."
}
},
"sendGridApiKey": {
"type": "securestring",
"metadata": {
"description": "The API key for the SendGrid account to use to send emails."
}
},
"alertFromAddress": {
"type": "string",
"metadata": {
"description": "The email address that Cosmos DB provisioning alerts should be sent from."
}
},
"alertToAddress": {
"type": "string",
"metadata": {
"description": "The email address that Cosmos DB provisioning alerts should be sent to."
}
}
},
"variables": {
"functionsAppServicePlanName": "CosmosChecker",
"applicationInsightsName": "CosmosChecker"
},
"resources": [
{
"name": "[variables('functionsAppServicePlanName')]",
"type": "Microsoft.Web/serverfarms",
"location": "[resourceGroup().location]",
"apiVersion": "2016-09-01",
"sku": {
"name": "Y1",
"tier": "Dynamic",
"size": "Y1",
"family": "Y",
"capacity": 0
},
"kind": "functionapp"
},
{
"name": "[parameters('functionsAppServiceName')]",
"type": "Microsoft.Web/sites",
"location": "[resourceGroup().location]",
"apiVersion": "2016-08-01",
"kind": "functionapp",
"properties": {
"enabled": true,
"serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('functionsAppServicePlanName'))]",
"reserved": false
},
"identity": {
"type": "SystemAssigned"
},
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2014-11-01",
"properties": {
"APPINSIGHTS_INSTRUMENTATIONKEY": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2014-04-01').InstrumentationKey]",
"AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', parameters('storageAccountName'), ';AccountKey=', listkeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2015-05-01-preview').key1, ';')]",
"AzureWebJobsDashboard": "[concat('DefaultEndpointsProtocol=https;AccountName=', parameters('storageAccountName'), ';AccountKey=', listkeys(resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName')), '2015-05-01-preview').key1, ';')]",
"AlertFromAddress": "[parameters('alertFromAddress')]",
"AlertToAddress": "[parameters('alertToAddress')]",
"SendGridKey": "[parameters('sendGridApiKey')]"
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionsAppServiceName'))]",
"[resourceId('Microsoft.Insights/components', variables('applicationInsightsName'))]"
]
},
{
"name": "ComosChecker",
"type": "functions",
"apiVersion": "2015-08-01",
"properties": {
"config": {
"bindings": [
{
"name": "myTimer",
"type": "timerTrigger",
"direction": "in",
"schedule": "[parameters('functionSchedule')]"
},
{
"name": "$return",
"type": "sendGrid",
"direction": "out",
"apiKey": "SendGridKey",
"to": "%AlertToAddress%",
"from": "%AlertFromAddress%",
"subject": "Overprovisioned Cosmos DB Collections"
}
],
"disabled": false
},
"files": {
"run.csx": "using System;\r\nusing System.Collections.Generic;\r\nusing System.Linq;\r\nusing System.Net;\r\nusing System.Net.Http;\r\nusing System.Threading.Tasks;\r\nusing Microsoft.Azure.Documents;\r\nusing Microsoft.Azure.Documents.Client;\r\nusing Microsoft.Azure.Management.Fluent;\r\nusing Microsoft.Azure.Management.ResourceManager.Fluent;\r\nusing Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;\r\nusing Microsoft.Azure.WebJobs;\r\nusing Microsoft.Azure.WebJobs.Host;\r\nusing Newtonsoft.Json;\r\nusing SendGrid.Helpers.Mail;\r\n\r\nprivate const int DefaultMaximumQuota = 2000;\r\n\r\npublic static async Task<Mail> Run(TimerInfo myTimer, TraceWriter log)\r\n{\r\n \/\/ list the Cosmos DB accounts available\r\n var accounts = await ListCosmosDBAccountsAsync(log);\r\n\r\n \/\/ get the alerts to fire for each Cosmos DB account\r\n var tasks = new List<Task<List<Alert>>>();\r\n foreach (var accountDetails in accounts)\r\n {\r\n tasks.Add(CheckCosmosDBAccountAsync(accountDetails.EndpointUri, accountDetails.ReadOnlyKey, log));\r\n }\r\n await Task.WhenAll(tasks);\r\n var allAlerts = new List<Alert>();\r\n foreach (var completedTask in tasks)\r\n {\r\n allAlerts.AddRange(completedTask.Result);\r\n }\r\n\r\n \/\/ send the alerts, if any\r\n log.Info($\"Found {allAlerts.Count} alert(s) to fire.\");\r\n if (! allAlerts.Any())\r\n {\r\n return null;\r\n }\r\n \r\n return CreateEmailAlert(allAlerts, log);\r\n}\r\n\r\nprivate static Mail CreateEmailAlert(List<Alert> allAlerts, TraceWriter log)\r\n{\r\n var fullAlertString = \"The following Cosmos DB collections may be overprovisioned:\\n\";\r\n foreach (var alert in allAlerts)\r\n {\r\n fullAlertString += $\"* {alert.ToString()}\\n\";\r\n }\r\n \r\n var message = new Mail();\r\n message.AddContent(new Content\r\n {\r\n Type = \"text\/plain\",\r\n Value = fullAlertString\r\n });\r\n return message;\r\n}\r\n\r\nprivate static async Task<IEnumerable<CosmosDBAccount>> ListCosmosDBAccountsAsync(TraceWriter log)\r\n{\r\n var accountList = new List<CosmosDBAccount>();\r\n\r\n \/\/ get the function's credentials from its managed service identity\r\n var credentials = new AzureCredentialsFactory()\r\n .FromMSI(new MSILoginInformation(MSIResourceType.AppService), AzureEnvironment.AzureGlobalCloud);\r\n\r\n \/\/ get a list of all subscription IDs accessible to the logged in principal\r\n var azure = Azure.Configure()\r\n .Authenticate(credentials);\r\n var subscriptions = await azure\r\n .Subscriptions\r\n .ListAsync();\r\n var subscriptionIds = subscriptions.Select(s => s.SubscriptionId);\r\n \r\n \/\/ find all Cosmos DB accounts within each subscription\r\n var tasks = new List<Task<IEnumerable<CosmosDBAccount>>>();\r\n foreach (var subscriptionId in subscriptionIds)\r\n {\r\n tasks.Add(ListCosmosDBAccountsInSubscriptionAsync(subscriptionId, credentials, log));\r\n }\r\n await Task.WhenAll(tasks);\r\n foreach (var task in tasks)\r\n {\r\n accountList.AddRange(task.Result);\r\n }\r\n\r\n return accountList;\r\n}\r\n\r\nprivate static async Task<IEnumerable<CosmosDBAccount>> ListCosmosDBAccountsInSubscriptionAsync(string subscriptionId, AzureCredentials credentials, TraceWriter log)\r\n{\r\n var accountList = new List<CosmosDBAccount>();\r\n\r\n \/\/ connect to the Azure subscription\r\n var azure = Azure\r\n .Configure()\r\n .Authenticate(credentials)\r\n .WithSubscription(subscriptionId);\r\n log.Verbose($\"Checking subscription '{subscriptionId}'\");\r\n \r\n \/\/ list all Cosmos DB accounts within the subscription\r\n var accounts = await azure.CosmosDBAccounts.ListAsync();\r\n foreach (var account in accounts)\r\n {\r\n \/\/ get the account endpoint URI and read-only key\r\n var endpointUri = account.DocumentEndpoint;\r\n var authKeys = await account.ListReadOnlyKeysAsync();\r\n\r\n accountList.Add(new CosmosDBAccount\r\n {\r\n EndpointUri = endpointUri,\r\n ReadOnlyKey = authKeys.PrimaryReadonlyMasterKey\r\n });\r\n }\r\n\r\n return accountList;\r\n}\r\n\r\nprivate static async Task<List<Alert>> CheckCosmosDBAccountAsync(string endpointUri, string authKeyString, TraceWriter log)\r\n{\r\n var alerts = new List<Alert>();\r\n\r\n \/\/ connect to the Cosmos DB account\r\n var client = new DocumentClient(new Uri(endpointUri), authKeyString);\r\n var account = await client.GetDatabaseAccountAsync();\r\n log.Verbose($\"Scanning Cosmos DB account '{account.Id}'\");\r\n\r\n \/\/ get a list of databases in the account\r\n var databases = await client.ReadDatabaseFeedAsync();\r\n\r\n \/\/ get a list of offers, each of which represent the throughput of a collection\r\n var offers = await client.ReadOffersFeedAsync();\r\n\r\n foreach (var database in databases)\r\n {\r\n \/\/ get a list of collections within the database\r\n var collections = await client.ReadDocumentCollectionFeedAsync(database.CollectionsLink);\r\n\r\n foreach (var collection in collections)\r\n {\r\n log.Verbose($\"Checking collection '{collection.Id}' in database '{database.Id}'\");\r\n long quota;\r\n\r\n \/\/ find the quota (throughput) for the collection\r\n var collectionOffer = offers.SingleOrDefault(o => o.ResourceLink == collection.SelfLink);\r\n if (collectionOffer is OfferV2)\r\n {\r\n quota = ((OfferV2)collectionOffer).Content.OfferThroughput;\r\n }\r\n else\r\n {\r\n var offer = await client.ReadOfferAsync(collectionOffer.SelfLink);\r\n quota = offer.CollectionQuota;\r\n }\r\n \r\n \/\/ check the throughput against the policy for the collection\r\n var alert = CreateAlert(quota, account.Id, database.Id, collection.Id, log);\r\n if (alert != null)\r\n {\r\n alerts.Add(alert);\r\n }\r\n }\r\n }\r\n\r\n return alerts;\r\n}\r\n\r\nprivate static Alert CreateAlert(long quota, string accountId, string databaseId, string collectionId, TraceWriter log)\r\n{\r\n var maximumQuota = GetMaximumQuotaForCollection(accountId, databaseId, collectionId);\r\n\r\n if (quota > maximumQuota)\r\n {\r\n log.Info($\"Firing alert for collection '{collectionId}' in database '{databaseId}' in account '{accountId}'. Expected maximum throughput to be {maximumQuota}, actual throughput {quota}.\");\r\n\r\n return new Alert\r\n { \r\n ActualQuota = quota,\r\n MaximumQuota = maximumQuota,\r\n AccountId = accountId,\r\n DatabaseId = databaseId,\r\n CollectionId = collectionId\r\n };\r\n }\r\n\r\n return null;\r\n}\r\n\r\nprivate static long GetMaximumQuotaForCollection(string accountId, string databaseId, string collectionId)\r\n{\r\n var settingName = $\"MaximumThroughput:{accountId}:{databaseId}:{collectionId}\";\r\n var quotaSetting = System.Environment.GetEnvironmentVariable(settingName, EnvironmentVariableTarget.Process);\r\n\r\n if (quotaSetting != null && long.TryParse(quotaSetting, out var quota))\r\n {\r\n return quota;\r\n }\r\n else\r\n {\r\n return DefaultMaximumQuota;\r\n } \r\n}\r\n\r\nclass CosmosDBAccount\r\n{\r\n public string EndpointUri { get; set; }\r\n public string ReadOnlyKey { get; set; }\r\n}\r\n\r\nclass Alert\r\n{\r\n public long ActualQuota { get; set; }\r\n public long MaximumQuota { get; set; }\r\n public string AccountId { get; set; }\r\n public string DatabaseId { get; set; }\r\n public string CollectionId { get; set; }\r\n\r\n public override string ToString() => $\"`{CollectionId}` (in `{AccountId}\/{DatabaseId}`) - expected maximum {MaximumQuota} RU\/s, currently {ActualQuota} RU\/s\";\r\n}\r\n",
"project.json": "{\r\n \"frameworks\": {\r\n \"net46\":{\r\n \"dependencies\": {\r\n \"Microsoft.Azure.DocumentDB\": \"1.22.0\",\r\n \"Microsoft.Azure.Management.Fluent\": \"1.13.0\",\r\n \"Microsoft.Azure.WebJobs.Extensions.SendGrid\": \"2.2.0\"\r\n }\r\n }\r\n }\r\n}\r\n"
}
},
"dependsOn": [
"[resourceId('Microsoft.Web/sites', parameters('functionsAppServiceName'))]"
]
}
],
"dependsOn": [
"[resourceId('Microsoft.Web/serverfarms', variables('functionsAppServicePlanName'))]"
]
},
{
"type": "Microsoft.Storage/storageAccounts",
"name": "[parameters('storageAccountName')]",
"apiVersion": "2015-06-15",
"location": "[resourceGroup().location]",
"properties": {
"accountType": "[parameters('storageAccountType')]"
}
},
{
"name": "[variables('applicationInsightsName')]",
"type": "Microsoft.Insights/components",
"location": "[parameters('applicationInsightsLocation')]",
"apiVersion": "2014-04-01",
"kind": "other",
"properties": {
"applicationId": "[variables('applicationInsightsName')]"
}
}
]
}