CS:GO Match API

Min kære bror fik arbejde ved North (FCKs CS:GO hold) og sagde i en indskudt sætning at det kunne være fedt at kunne hive Norths fremtidige kampe ind i Excel. Jeg kunne ikke dy mig for at lave noget som kunne netop dette, og jeg gik derfor i tænkeboks, hvilket kom disse ideér ud af:

  1. man kunne scrape kamp data fra hltv.org, gemme data i en database, og hive data ind i Excel via dens standard connector
  2. man kunne scrape kamp data fra hltv.org, gemme data i en database, lave et MVC API, sætte Cloudflare op foran, og servere det hele i json
  3. man kunne scrape kamp data fra hltv.org, gemme data i en blob container, eller table storage, og give adgang hertil

Jeg vidste allerede hvad jeg ville vælge, og gik derfor igang med nummer 2, hvilket der kom dette design ud af:

Servitr CS:GO arkitektur

Arkitekturen består af flere forskellige komponenter:

  1. En Azure function som kører hver nat kl 01, og parser alle kampe fra hltv.org
  2. En API som udstiller kamp data via et HTTP endpoint i JSON formatet
  3. Application Insights som tracker alt (både Azure functions og Web API)
  4. En logic app som kigger på Application Insights og notificere mig en gang om dagen hvis der er fundet nogle fejl
  5. VSTS som bygger på hver commit og releaser automatisk

Release sker først til dev miljøet, hvis der ikke sker nogle exceptionelle fejl, så releases der automatisk til produktionsmiljøet efter 30 minutter. Begge miljøer er sat op via en ARM template (dog har jeg valgt at sætte Logic App'en op manuelt)

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environment": {
      "type": "string",
      "defaultValue": "dev"
    },
    "mssqlPassword": { "type": "string" },
    "mssqlUsername": { "type": "string" },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]",
      "metadata": {
        "description": "Location for all resources."
      }
    }

  },
  "variables": {
    "logicAppName": "[concat('esport-watcher-logic-app-',parameters('environment'))]",
    "webApiName": "[concat('esport-watcher-web-api-',parameters('environment'))]",
    "websiteName": "[concat('esport-watcher-website-',parameters('environment'))]",
    "mssqlServerName": "[concat('esport-watcher-mssql-server-',parameters('environment'))]",
    "mssqlDatabaseName": "[concat('esport-watcher-mssql-database-',parameters('environment'))]",
    "azureFunctionsName": "[concat('esport-watcher-import-functions-',parameters('environment'))]",
    "azureFunctionsHostingName": "[concat('esport-watcher-import-functions-hosting-plan-',parameters('environment'))]",
    "webApiHostingName": "[concat('esport-watcher-web-api-hosting-plan-',parameters('environment'))]",
    "azureFunctionsStorageAccountName": "[concat(uniquestring(resourceGroup().id), 'azfunctions')]",
    "storageAccountid": "[concat(resourceGroup().id,'/providers/','Microsoft.Storage/storageAccounts/', variables('azureFunctionsStorageAccountName'))]",
    "applicationInsightsName": "[concat('application-insights-',parameters('environment'))]",
    "websitePlan": "[concat('website-plan-',parameters('environment'))]"
  },
  "resources": [
    //Logic App
    //{
    //  "type": "Microsoft.Logic/workflows",
    //  "apiVersion": "2016-06-01",
    //  "name": "[variables('logicAppName')]",
    //  "location": "[parameters('location')]",
    //  "properties": {
    //    "definition": {
    //      "$schema": "https://schema.management.azure.com/schemas/2016-06-01/Microsoft.Logic.json",
    //      "contentVersion": "1.0.0.0",
    //      "parameters": {},
    //      "triggers": {},
    //      "actions": {},
    //      "outputs": {}
    //    }
    //  }
    //},
    // Website
    {
      "name": "[variables('websiteName')]",
      "type": "Microsoft.Web/sites",
      "location": "[parameters('location')]",
      "apiVersion": "2016-08-01",
      "properties": {
        "serverFarmId": "[variables('websitePlan')]",
        "name": "[variables('websiteName')]",
        "siteConfig": {
          "appSettings": [
            {
              "name": "ApplicationInsights:InstrumentationKey",
              "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey]"
            }
          ]
        }
      },
      "dependsOn": [
        "[concat('Microsoft.Web/serverFarms/', variables('websitePlan'))]"
      ]
    },

    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2015-08-01",
      "name": "[variables('websitePlan')]",
      "location": "[parameters('location')]",
      "properties": {},
      "sku": {
        "name": "D1",
        "tier": "Standard",
        "capacity": 1
      }
    },

    //MSSQL server
    {
      "name": "[variables('mssqlServerName')]",
      "type": "Microsoft.Sql/servers",
      "apiVersion": "2015-05-01-preview",
      "location": "[parameters('location')]",
      "tags": {},
      "identity": {
        "type": "SystemAssigned"
      },
      "properties": {
        "administratorLogin": "[parameters('mssqlUsername')]",
        "administratorLoginPassword": "[parameters('mssqlPassword')]"
      },

      "resources": [
        {
          "name": "[concat(variables('mssqlServerName'),'/',variables('mssqlDatabaseName'))]",
          "type": "Microsoft.Sql/servers/databases",
          "apiVersion": "2014-04-01",
          "tags": {},
          "location": "[parameters('location')]",
          "properties": {
            "collation": "SQL_Latin1_General_CP1_CI_AS",
            "maxSizeBytes": "1073741824",
            "requestedServiceObjectiveName": "S0",
            "edition": "Standard"
          },
          "dependsOn": [ "[resourceId('Microsoft.Sql/servers', variables('mssqlServerName'))]" ]
        }
      ]
    },
    //Application Insights
    {
      "name": "[variables('applicationInsightsName')]",
      "type": "microsoft.insights/components",
      "apiVersion": "2015-05-01",
      "location": "[parameters('location')]",
      "tags": {},
      "kind": "web",
      "properties": {
        "Application_Type": "web",
        "Flow_Type": "Bluefield",
        "Request_Source": "rest"
      }
    },
    //Web API
    {
      "apiVersion": "2016-03-01",
      "name": "[variables('webApiHostingName')]",
      "type": "Microsoft.Web/serverfarms",
      "location": "[resourceGroup().location]",
      "properties": {
      },
      "sku": {
        "name": "D1",
        "tier": "Standard",
        "size": "1",
        "family": "D",
        "capacity": "1"
      }
    },
    {
      "apiVersion": "2016-03-01",
      "name": "[variables('webApiName')]",
      "type": "Microsoft.Web/sites",
      "location": "[resourceGroup().location]",
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverFarms',variables('webApiHostingName'))]",
        "siteConfig": {
          "appSettings": [
            {
              "name": "ConnectionStringEfCore",
              "value": "[concat('Server=tcp:',reference(variables('mssqlServerName')).fullyQualifiedDomainName,',1433;Initial Catalog=',variables('mssqlDatabaseName'),';Persist Security Info=False;User ID=',parameters('mssqlUsername'),';Password=',parameters('mssqlPassword'),';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;')]"
            },
            {
              "name": "ApplicationInsights:InstrumentationKey",
              "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey]"
            }
          ]
        }
      },
      "dependsOn": [
        "[concat('Microsoft.Web/serverFarms/',variables('webApiHostingName'))]"
      ]
    },

    //Azure Functions
    {
      "type": "Microsoft.Storage/storageAccounts",
      "name": "[variables('azureFunctionsStorageAccountName')]",
      "apiVersion": "2016-12-01",
      "location": "[parameters('location')]",
      "kind": "Storage",
      "sku": {
        "name": "Standard_RAGRS"
      }
    },
    {
      "type": "Microsoft.Web/serverfarms",
      "apiVersion": "2015-04-01",
      "name": "[variables('azureFunctionsHostingName')]",
      "location": "[parameters('location')]",
      "properties": {
        "computeMode": "Dynamic",
        "sku": "Dynamic"
      }
    },
    {
      "apiVersion": "2015-08-01",
      "type": "Microsoft.Web/sites",
      "name": "[variables('azureFunctionsName')]",
      "location": "[parameters('location')]",
      "kind": "functionapp",
      "dependsOn": [
        "[resourceId('Microsoft.Web/serverfarms', variables('azureFunctionsHostingName'))]",
        "[resourceId('Microsoft.Storage/storageAccounts', variables('azureFunctionsStorageAccountName'))]"
      ],
      "properties": {
        "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', variables('azureFunctionsHostingName'))]",
        "siteConfig": {
          "appSettings": [
            {
              "name": "ConnectionStringEfCore",
              "value": "[concat('Server=tcp:',reference(variables('mssqlServerName')).fullyQualifiedDomainName,',1433;Initial Catalog=',variables('mssqlDatabaseName'),';Persist Security Info=False;User ID=',parameters('mssqlUsername'),';Password=',parameters('mssqlPassword'),';MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;')]"
            },
            {
              "name": "AzureWebJobsDashboard",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('azureFunctionsStorageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
            },
            {
              "name": "AzureWebJobsStorage",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('azureFunctionsStorageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
            },
            {
              "name": "WEBSITE_CONTENTAZUREFILECONNECTIONSTRING",
              "value": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('azureFunctionsStorageAccountName'), ';AccountKey=', listKeys(variables('storageAccountid'),'2015-05-01-preview').key1)]"
            },
            {
              "name": "WEBSITE_CONTENTSHARE",
              "value": "[toLower(variables('azureFunctionsName'))]"
            },
            {
              "name": "FUNCTIONS_EXTENSION_VERSION",
              "value": "~2"
            },
            {
              "name": "WEBSITE_NODE_DEFAULT_VERSION",
              "value": "6.5.0"
            },
            {
              "name": "APPINSIGHTS_INSTRUMENTATIONKEY",
              "value": "[reference(resourceId('Microsoft.Insights/components', variables('applicationInsightsName')), '2015-05-01').InstrumentationKey]"
            }
          ]
        }
      }
    }

  ],
  "outputs": {}
}

parameters er sat til:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "mssqlPassword": { "value": "" },
    "mssqlUsername": { "value": "" },
    "environment": {"value": "dev"}
  }
}

Alt er fuld automatisk, og jeg skal blot udvikle. Jeg har valgt at teste EF Core til at gemme de parsede kampe med. Jeg plejer at bruge Fluent Migrations og Dapper, dog ville jeg teste alt det nye som er kommet i EF siden sidst jeg arbejdede med det (som er ved at være en del år siden).

Logic app'en smider exceptions i en kanal alt afhængig af miljø:

  1. Exceptions-dev (for udviklingsmiljøet)
  2. Exception-prod (for produktionsmiljøet)

Jeg vælger altid at blive notified om sådanne ting via noget andet end email da jeg ved at alarm mails og exception mails hurtigt drukner i alle mulige andre mails.

Du kan tilgå produktionsmiljøet på følgende link: https://www.servitr.io, som beskriver lidt omkring API'et på engelsk. Vil du direkte til API'et, så kan du tilgå den via https://www.servitr.io. Du vil her blive mødt af en swagger side, som beskriver hvordan API'et virker, og hvor du kan teste den.

Man kan hive kampe ud af API'et og fx importere dem i Excel via Power Query, eller bruge det som grundlag i ens egen service. API'et er gratis så længe det er oppe!

Vil du have adgang til kildekoden så kontakt mig. Så kan jeg smide det på github til deling.

Velbekom!

Skrevet af Martin Slot den 6/22/2018