Code-first serverless azure solutions

A Collection of Interesting Ideas,

Editors:
(EPAM Systems)

Abstract

Руководство участника практического курса.

1. Пререквизиты

Для начала работы на вашем компьютере должны быть:
  1. Установлен Visual studio 2017+. Скачать

  2. Установлен Azure development workload в Visual studio.

  3. Установлен Azure CLI и обновлен до последней версии. Скачать.

  4. Azure subscription. Если у вас её нет, то создайте бесплатный аккаунт. Важно: можно участвовать и без подписки с помощью локальной отладки функций.

  5. Альтернативно, вы можете участвовать используя Visual Studio Code или Azure portal, однако демонстрация воркшопа будет в Visual studio.

2. Создание ресурсов Azure

  1. Откроем shell. Bash или https://shell.azure.com/

  2. Войдём в azure

2.1. Snippet 1

az login
  1. Создадим ресурсную группу

2.2. Snippet 2

GROUP=workshop-serverless-rg
az group create --location westeurope \
  --name $GROUP \
  --output table
  1. Создадим Storage account

2.3. Snippet 3

STORAGE="workshop${RANDOM}sa"
az storage account create --name $STORAGE \
  -g $GROUP \
  -l westeurope \
  --sku Standard_LRS \
  --output table
  1. Создадим Functions App

2.4. Snippet 4

FUNCTION=workshop-fapp-$RANDOM
az functionapp create --name $FUNCTION \
  -g $GROUP \
  -s $STORAGE \
  --consumption-plan-location westeurope \
  --functions-version 3 \
  --os-type Windows \
  --runtime dotnet
  1. Заметьте, автоматическое создание Application Insights в output

3. Создание проекта Azure Functions в Visual studio

  1. Откроем Visual studio.

  2. Создадим новый проект по шаблону Azure Functions.

  3. Укажем имя, путь к проекту.

  4. В меню "Create a new Azure Functions application" укажем:

    • Azure funtions v3 (.NET Core)

    • HTTP trigger

    • В качестве storage account выберем ранее создный через Browse... в выпадающем меню

    • Authorization level: Anonymous

  5. Нажмем Create

4. Обновим автоматически сгенерированный код

  1. Обновим значение атрибута FunctionName c Function1 на GetEricLippertBlogArticle.

  2. Удалим из тела функции Run всё, кроме логирования.

  3. После изменений код будет выглядеть так:

4.1. Snippet 5

namespace EricLippertBlogRoulette
{
    public static class Function1
    {
        [FunctionName("GetEricLippertBlogArticle")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");
        }
    }
}

5. Добавим обращение к web crawler блога

После записи в лог добавим обращение к функции и десериализацию в массив:

5.1. Snippet 6

using var webClient = new WebClient();
string linksJson = await webClient.DownloadStringTaskAsync(
    "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==");
var links = JsonConvert.DeserializeObject<string[]>(linksJson);

6. Добавим код выбора случайной статьи и Redirect

  1. Добавим private static поле для Random

6.1. Snippet 7

private static Random rng = new Random();
  1. Добавим выбор случайной статьи и возврат RedirectResult

6.2. Snippet 8

var randomLink = links[rng.Next(links.Length)];

return new RedirectResult(randomLink);
  1. Итоговый код после этого этапа выглядит так:

6.3. Snippet 9

namespace EricLippertBlogRoulette
{
    public static class Function1
    {
        private static Random rng = new Random();

        [FunctionName("GetEricLippertBlogArticle")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            using var webClient = new WebClient();
            string linksJson = await webClient.DownloadStringTaskAsync(
                "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==");
            var links = JsonConvert.DeserializeObject<string[]>(linksJson);
            var randomLink = links[rng.Next(links.Length)];

            return new RedirectResult(randomLink);
        }
    }
}

7. Publish в Azure

  1. Создадим Publish профайл, нажав на кнопку Publish в контекстном меню проекта:

    • Target: Azure

    • Specific target: Azure Functions App (Windows)

    • Functions instance: ранее созданную workshop-fapp-{number} (Consumption)

  2. Выполним Publish

  3. Для получения URL вернемся в bash и выполним команду:

7.1. Snippet 10

az functionapp function show -g $GROUP \
  --name $FUNCTION \
  --function-name GetEricLippertBlogArticle \
  --query "invokeUrlTemplate" -o tsv
  1. Скопируем адрес и откроем в браузере.

  2. Заметьте, первый вызов функции медленный из-за Cold start

8. Добавим учёт посещенных статей с ипользованием bindings

  1. Подключим Nuget пакет Microsoft.Azure.WebJobs.Extensions.Storage

  2. Добавим параметры в метод Run для Blog биндинга:

8.1. Snippet 11

[Blob("functions-data/visited", FileAccess.Read)] TextReader inputBlob,
[Blob("functions-data/visited", FileAccess.Write)] TextWriter outputBlob,
  1. Добавим код считывания всех посещенных статей и их учёт при выборе случайной.

8.2. Snippet 12

var visited = new List<string>();
if (inputBlob != null)
{
    string visitedLink;
    while ((visitedLink = inputBlob.ReadLine()) != null)
    {
        visited.Add(visitedLink);
    }
}

var links = JsonConvert.DeserializeObject<string[]>(linksJson)
    .Except(visited)
    .ToArray();
  1. Добавим вставку выбранной статьи в visited blob

8.3. Snippet 13

visited.Add(randomLink);
foreach (var link in visited)
{
    outputBlob.WriteLine(link);
}
  1. Итоговый код выглядит так:

8.4. Snippet 14

namespace EricLippertBlogRoulette
{
    public static class Function1
    {
        private static Random rng = new Random();

        [FunctionName("GetEricLippertBlogArticle")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = null)] HttpRequest req,
            [Blob("functions-data/visited", FileAccess.Read)] TextReader inputBlob,
            [Blob("functions-data/visited", FileAccess.Write)] TextWriter outputBlob,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            using var webClient = new WebClient();
            string linksJson = await webClient.DownloadStringTaskAsync(
                "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==");
            var visited = new List<string>();
            if (inputBlob != null)
            {
                string visitedLink;
                while ((visitedLink = inputBlob.ReadLine()) != null)
                {
                    visited.Add(visitedLink);
                }
            }

            var links = JsonConvert.DeserializeObject<string[]>(linksJson)
                .Except(visited)
                .ToArray();
            var randomLink = links[rng.Next(links.Length)];

            visited.Add(randomLink);
            foreach (var link in visited)
            {
                outputBlob.WriteLine(link);
            }

            return new RedirectResult(randomLink);
        }
    }
}

9. Аутентификация

  1. В методе Run класса Function измените в атрибутe HttpTrigger параметр AuthorizationLevel c Anonymous на Function.

  2. Выполните Publish функции

  3. Вызовете функцию в браузере (snippet 10)

  4. Заметьте: теперь в ответе 401 ошибка (unauthorized)

  5. Выведете в консоль default ключ функции командой

9.1. Snippet 15

az functionapp function keys list -g $GROUP \
  -n $FUNCTION \
  --function-name GetEricLippertBlogArticle \
  --query "default"
  1. Скопируйте и дополните в браузере адрес функции как параметр ?code=<скопированный ключ>

  2. Наконец, финальная версия функции:

9.2. Snippet 16

namespace EricLippertBlogRoulette
{
    public static class Function1
    {
        private static Random rng = new Random();

        [FunctionName("GetEricLippertBlogArticle")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            [Blob("functions-data/visited", FileAccess.Read)] TextReader inputBlob,
            [Blob("functions-data/visited", FileAccess.Write)] TextWriter outputBlob,
            ILogger log)
        {
            log.LogInformation("C# HTTP trigger function processed a request.");

            using var webClient = new WebClient();
            string linksJson = await webClient.DownloadStringTaskAsync(
                "https://learning-fapp-4928.azurewebsites.net/api/GetEricLippertBlogArticles?code=1gLGWOODXcmJHVs6PLQSBcSyM0dHL/JPt1NTwgUJTvLHTurY61yUbg==");
            var visited = new List<string>();
            if (inputBlob != null)
            {
                string visitedLink;
                while ((visitedLink = inputBlob.ReadLine()) != null)
                {
                    visited.Add(visitedLink);
                }
            }

            var links = JsonConvert.DeserializeObject<string[]>(linksJson)
                .Except(visited)
                .ToArray();
            var randomLink = links[rng.Next(links.Length)];

            visited.Add(randomLink);
            foreach (var link in visited)
            {
                outputBlob.WriteLine(link);
            }

            return new RedirectResult(randomLink);
        }
    }
}

10. Очистка

  1. Удалите ресурсную группу (удалятся все ресурсы внутри)

10.1. Snippet 17

az group delete -g $GROUP --no-wait --yes
  1. Удалите Azure CLI по инструкции

  2. Удалите Visual Studio по инструкции

Conformance

Conformance requirements are expressed with a combination of descriptive assertions and RFC 2119 terminology. The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in the normative parts of this document are to be interpreted as described in RFC 2119. However, for readability, these words do not appear in all uppercase letters in this specification.

All of the text of this specification is normative except sections explicitly marked as non-normative, examples, and notes. [RFC2119]

Examples in this specification are introduced with the words “for example” or are set apart from the normative text with class="example", like this:

This is an example of an informative example.

Informative notes begin with the word “Note” and are set apart from the normative text with class="note", like this:

Note, this is an informative note.

References

Normative References

[RFC2119]
S. Bradner. Key words for use in RFCs to Indicate Requirement Levels. March 1997. Best Current Practice. URL: https://tools.ietf.org/html/rfc2119