Nativní Azure Monitor a Microsoft Sentinel nově umí levnější logy a zabudovanou levnější archivaci - praxe (část 2)

V minulém díle jsem zkoumal nové vlastnosti Log Analytics pro Azure Monitor a Microsoft Sentinel, které mohou výrazně prolevnit celé řešení - basic logs, archive tier, search jobs a restore. Dnes si to vyzkoušíme prakticky.

V následujících textech budu pro některé operace používat přímo API. GUI pro nastavování archivací a search joby existuje v preview v rámci Sentinelu a nevím jak to bude ve finální podobě - API ale předpokládám zůstane, takže se na něj zaměřím.

Jedna z tabulek, která mě hodně zajímá, jsou logy z kontejnerů. Těch totiž může být hodně a nativní monitoring AKS tak mohou prodražovat, přitom nad nimi typicky nepotřebuji dělat nějaké složité analytické operace (na rozdíl třeba od bezpečnostních logů). Vytvořím tedy Azure Kubernetes Service s Azure Monitor a spustím kontejner, který mi bude generovat logy.

# Create resource group
az group create -n logs -l westeurope

# Create Log Analytics workspace
export workspaceName=tomaslogs$RANDOM
az monitor log-analytics workspace create -g logs -n $workspaceName

# Create AKS
export workspaceId=$(az monitor log-analytics workspace show -g logs -n $workspaceName --query id -o tsv)
az aks create -g logs -n logsaks -l westeurope -c1 -x -s Standard_B2s \
    -a monitoring --workspace-resource-id $workspaceId
az aks get-credentials -g logs -n logsaks --admin --overwrite-existing

# Start log generating app
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: loggen
spec:
  selector:
    matchLabels:
      app: loggen
  template:
    metadata:
      labels:
        app: loggen
    spec:
      containers:
      - name: loggen
        image: ubuntu
        command:
          - "/bin/bash"
        args:
          - "-c"
          - "while true; do echo type\$(expr \$RANDOM % 5): ip=1.1.1.\$(expr \$RANDOM % 254) port=\$(expr \$RANDOM % 65554); sleep 2; done"
EOF

Dalším krokem teď bude tabulku ContainerLog přepnout z výchozího analytického tieru do režimu basic. Ten má v ceně retenci 8 dní a to je pro mě málo, nicméně prodloužit ji, na rozdíl od analytického tieru, nemůžu. Nicméně můžu použít archivní tier. To udělám tak, že nastavím parametr totalRetentionInDays a vše nad 8 dní bude v archivu.

# Configure ContainerLogs table as Basic and enable archive for 180 days
export url="https://management.azure.com$workspaceId/tables/ContainerLog?api-version=2021-12-01-preview"
az rest --method patch --url $url --headers "Content-Type=application/json" \
    -b '{
            "properties": {
                "plan": "Basic",
                "totalRetentionInDays": 180
            }
        }'

V basic jsem schopen efektivně vyhledávat. Zkusím například query, které nejprve přidá nový nové sloupečky, které vzniknou regex parsováním logu, následně podle těchto ad-hoc vytvořených sloupečků budu filtrovat na množinu typů a porty menší než 1024. Na závěr odejmu z výstupu sloupce, které mě nezajímají.

ContainerLog 
| extend type = extract(@"^(.*?):", 1, LogEntry),
  ip = extract(@"ip=(.*?)\s", 1, LogEntry),
  port = extract(@"port=(.*?)$", 1, LogEntry)
| where type in ( "type1", "type3")
| where toint(port) < 1024
| project-away _ResourceId, LogEntry, LogEntrySource, _SubscriptionId

Nicméně pokud se pokusím o něco analytického, nebude to možné. Například zkusím jednoduše sumarizovat počet událostí za jednotlivé ip.

ContainerLog 
| extend type = extract(@"^(.*?):", 1, LogEntry),
  ip = extract(@"ip=(.*?)\s", 1, LogEntry),
  port = extract(@"port=(.*?)$", 1, LogEntry)
| where type in ( "type1", "type3")
| where toint(port) < 1024
| project-away _ResourceId, LogEntry, LogEntrySource, _SubscriptionId
| summarize count() by ip

Protože v basic (a také v archivu) platím za to, jak velký objem dat musí engine projít, bude zajímavé o dotazu získat nějakou statistiku. V rámci API si stačí statistické informace vyžádat při zadávání dotazu.

# When quering we can requests stats to understand volume of scanned data
export workspaceCustomer=$(az monitor log-analytics workspace show -g logs -n $workspaceName --query customerId -o tsv)

az rest -u "https://api.loganalytics.io/v1/workspaces/$workspaceCustomer/search?timespan=P1D" \
    --headers "Content-Type=application/json" \
    --headers "Prefer=include-statistics=true" \
    --method post \
    --body '{"query":"ContainerLog"}' \
    | jq .statistics

Tohle je výstup.

{
  "query": {
    "datasetStatistics": [
      {
        "tableRowCount": 43087,
        "tableSize": 14598614
      }
    ],
    "executionTime": 0.5937856,
    "inputDatasetStatistics": {
      "extents": {
        "scanned": 1,
        "scannedMaxDatetime": "2022-02-26T12:41:19.2909747Z",
        "scannedMinDatetime": "2022-02-25T11:03:02.2175447Z",
        "total": 1
      },
      "rows": {
        "scanned": 36656,
        "total": 36656
      },
      "rowstores": {
        "scannedRows": 12323,
        "scannedValuesSize": 4308574
      },
      "shards": {
        "queriesGeneric": 1,
        "queriesSpecialized": 0
      }
    },
    "resourceUsage": {
      "cache": {
        "disk": {
          "hits": 0,
          "misses": 0,
          "total": 0
        },
        "memory": {
          "hits": 0,
          "misses": 0,
          "total": 0
        },
        "shards": {
          "bypassbytes": 0,
          "cold": {
            "hitbytes": 0,
            "missbytes": 0,
            "retrievebytes": 0
          },
          "hot": {
            "hitbytes": 682106,
            "missbytes": 0,
            "retrievebytes": 0
          }
        }
      },
      "cpu": {
        "kernel": "00:00:00",
        "totalCpu": "00:00:00.0468750",
        "user": "00:00:00.0468750"
      },
      "memory": {
        "peakPerNode": 104100080
      },
      "network": {
        "crossClusterTotalBytes": 0,
        "interClusterTotalBytes": 34897588
      }
    }
  }
}

Přímé volání query, kdy dostáváme velmi rychle výsledek, je možné v basic a analytickém tier, ale archiv funguje jinak. Ten bude procházet studená data, která jsou uložena levněji (= pomaleji) a bude obcházet větší množství nodů a odpověď skládat. To co trvalo vteřiny, může teď trvat třeba minuty.

Dejme tedy tomu, že si připravím query na rozparsování zprávy na nové sloupce a v jednom z nich hledám “type2”.

ContainerLog 
| extend type = extract(@"^(.*?):", 1, LogEntry),
  ip = extract(@"ip=(.*?)\s", 1, LogEntry),
  port = extract(@"port=(.*?)$", 1, LogEntry)
| where type == "type2"
| project-away _ResourceId, LogEntry, LogEntrySource, _SubscriptionId

Jak to funguje? Search job poběží na pozadí a výsledky bude ukládat do nové analytické tabulky, která musí mít příponu _SRCH. Použije se API na založení tabulky, které nově podporuje vlastnost searchResults, ve kterém jsem specifikoval výše uvedené query, potom limit (může být vhodná ochrana před nějakou chybou v zadání, protože nechci, aby výsledná tabulka, u které platím za ingesting, měla třeba 1 TB, to by stálo moc peněz) a časové okno hledání.

# Search job - parse and query type2 logs

export resultTable=type2logs
export url="https://management.azure.com$workspaceId/tables/${resultTable}_SRCH?api-version=2021-12-01-preview"
az rest --method put --url $url --headers "Content-Type=application/json" \
    -b '{
            "properties": { 
                "searchResults": {
                        "query": "ContainerLog \r\n| extend type = extract(@\"^(.*?):\", 1, LogEntry),\r\n  ip = extract(@\"ip=(.*?)\\s\", 1, LogEntry),\r\n  port = extract(@\"port=(.*?)$\", 1, LogEntry)\r\n| where type == \"type2\"\r\n| project-away _ResourceId, LogEntry, LogEntrySource, _SubscriptionId",
                        "limit": 10000,
                        "startSearchTime": "2022-02-21T00:00:00Z",
                        "endSearchTime": "2022-02-26T00:00:00Z"
                    }
            }
        }'

Job běží na pozadí, což si můžu zkontrolovat GETem na stejnou URL

# Get job status
az rest --method get --url $url --headers "Content-Type=application/json"
{
  "id": "/subscriptions/a0f4a733-4fce-4d49-b8a8-d30541fc1b45/resourcegroups/logs/providers/Microsoft.OperationalInsights/workspaces/tomaslogs7613/tables/type2logs_SRCH",     
  "name": "type2logs_SRCH",
  "properties": {
    "archiveRetentionInDays": 0,
    "createdBy": "7424fb4c-5e9f-45cd-9f7d-453d45655e75",
    "lastPlanModifiedDate": "2022-02-26T14:24:18.747Z",
    "plan": "Analytics",
    "provisioningState": "Updating",
    "retentionInDays": 30,
    "totalRetentionInDays": 30
  },
  "systemData": {
    "createdBy": "7424fb4c-5e9f-45cd-9f7d-453d45655e75"
  }
}

Vidím, že analytická tabulka s výsledky se právě vytváří. Po nějaké době GET zopakuji a už vidím strukturu tabulky, z kterého query vzešla a tak podobně.

{
  "id": "/subscriptions/a0f4a733-4fce-4d49-b8a8-d30541fc1b45/resourcegroups/logs/providers/Microsoft.OperationalInsights/workspaces/tomaslogs7613/tables/type2logs_SRCH",     
  "name": "type2logs_SRCH",
  "properties": {
    "archiveRetentionInDays": 0,
    "createdBy": "7424fb4c-5e9f-45cd-9f7d-453d45655e75",
    "lastPlanModifiedDate": "2022-02-26T14:24:18.747Z",
    "plan": "Analytics",
    "provisioningState": "InProgress",
    "resultStatistics": {
      "ingestedRecords": 0,
      "progress": 0.0,
      "scannedGb": 0.0
    },
    "retentionInDays": 30,
    "schema": {
      "columns": [
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "SourceSystem",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "TimeGenerated",
          "type": "datetime"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "Computer",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "TimeOfCommand",
          "type": "datetime"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "ContainerID",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "Image",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "ImageTag",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "Repository",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "Name",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "Id",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "ip",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "port",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "_OriginalType",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "_OriginalItemId",
          "type": "string"
        },
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "_OriginalTimeGenerated",
          "type": "datetime"
        }
      ],
      "name": "type2logs_SRCH",
      "searchResults": {
        "azureAsyncOperationId": "557f8f12-23e2-486c-9861-adf82274d850",
        "description": "This table was created using a Search Job with the following query: 'ContainerLog \r\n| extend type = extract(@\"^(.*?):\", 1, LogEntry),\r\n  ip = extract(@\"ip=(.*?)\\s\", 1, LogEntry),\r\n  port = extract(@\"port=(.*?)$\", 1, LogEntry)\r\n| where type == \"type2\"\r\n| project-away _ResourceId, LogEntry, LogEntrySource, _SubscriptionId'.",
        "endSearchTime": "2022-02-26T00:00:00Z",
        "limit": 10000,
        "query": "ContainerLog \r\n| extend type = extract(@\"^(.*?):\", 1, LogEntry),\r\n  ip = extract(@\"ip=(.*?)\\s\", 1, LogEntry),\r\n  port = extract(@\"port=(.*?)$\", 1, LogEntry)\r\n| where type == \"type2\"\r\n| project-away _ResourceId, LogEntry, LogEntrySource, _SubscriptionId",
        "sourceTable": "ContainerLog",
        "startSearchTime": "2022-02-21T00:00:00Z"
      },
      "solutions": [
        "LogManagement"
      ],
      "standardColumns": [
        {
          "isDefaultDisplay": false,
          "isHidden": false,
          "name": "TenantId",
          "type": "guid"
        }
      ],
      "tableSubType": "DataCollectionRuleBased",
      "tableType": "SearchResults"
    },
    "totalRetentionInDays": 30
  },
  "systemData": {
    "createdBy": "7424fb4c-5e9f-45cd-9f7d-453d45655e75"
  }
}

Pokud bych něco takového chtěl dělat graficky z portálu, takhle vypadá UI v Microsoft Sentinel.

Pěkně je, že výsledky našeho hledání v archivu jsou teď v analytické tabulce, takže nad nimi můžeme pokračovat dál s analytikou - například sumarizaci podle IP.

type2logs_SRCH
| extend TimeGenerated = _OriginalTimeGenerated
| project-away _OriginalTimeGenerated
| summarize count() by ip

Místo search jobu můžeme také přistoupit k zavlažení archivních logů. Jak jsem minule popisoval, funguje to tak, že se data na nějakou dobu přesunou do analytického tieru a můžete se pak v nich prohrabávat naplno. Platíte podle objemu zavlažených dat a to za každý načatý den, kdy je chcete držet v analytice. Všimněte si, že v následujícím API volání nemůžeme specifikovat query, jen časové okno (minimum jsou dva dny). Pokud tedy hledám něco dost specifického, co mi z tabulky vrátí méně jak 4% dat, je výhodnější dát search job a zaplatit ingesting. Pokud ale nevím přesně co hledám a budu dat vracet významnější procento, bude výhodnější je zavlažit. V API stačí opět vytvořit novou tabulku tentokrát s příponou _RST a v properties specifikovat resourceLogs s časovým oknem a zdrojovou tabulkou.

# Restore archive logs to analytics tier
export url="https://management.azure.com$workspaceId/tables/ContainerLog_RST?api-version=2021-12-01-preview"
az rest --method put --url $url --headers "Content-Type=application/json" \
    -b '{
            "properties":  {
                "restoredLogs":  {
                    "startRestoreTime":  "2022-01-21T00:00:00Z",
                    "endRestoreTime":  "2022-01-25T00:00:00Z",
                    "sourceTable":  "ContainerLog"
                }
            }
        }'

Takhle to vypadá v UI Sentinelu.

Na závěr - basic logs neumožňují joiny a jiné složité operace a to ovlivnilo UI Container Insights v Azure Monitor, které joinuje ContainerLogs s několika dalšími tabulkami s inventářem (jak se jmenoval Pod a tak).

Možná právě proto je v private preview obohacování dat přímo na agentovi, takže v tabulce najdete i informaci o Podu nebo namespace. Znamená to samozřejmě, že takový denormalizovaný přístup generuje větší objem dat, ale na druhou stranu pak nepotřebujete složité join operace a search zvládnete v basic nebo archive tieru v pohodě. Aktuálně už je dokumentace datové struktury tabulky ContainerLogV2 - nicméně vyzkoušet zatím nelze, snad to bude brzy hotové.

Tolik tedy zatím k využití nových basic a archiv logů v praxi a metodám prohledávání nebo zavlažování archivních logů. Jsem zvědavý hlavně jak se bude rozvíjet podpora v jednotlivých řešeních typu různých Insights v Azure Monitor, protože UI v Microsoft Sentinel už teď vypadá myslím velmi dobře. Zkuste si to!



Azure Kubernetes Service v kombinaci s Private Link Service - například pro privátní doručení na Front Door nebo k vašim Azure klientům Kubernetes
Federace tokenů GitHub Actions s Azure Active Directory pro přístup z vaší CI/CD do Azure bez hesel Security
Nativní Azure Monitor a Microsoft Sentinel nově umí levnější logy a zabudovanou levnější archivaci - analýza nákladů (část 1) Monitoring Security Kubernetes
Federace vnitřních Kubernetes identit s Azure Active Directory pro přístup k cloudovým službám bez hesel Kubernetes Security
Použití lokální storage v Azure Kubernetes Service Kubernetes